This commit is contained in:
Martin Donnelly 2017-12-15 18:34:59 +00:00
commit 365a10420d
27 changed files with 39493 additions and 0 deletions

55
.eslintrc.json Normal file
View File

@ -0,0 +1,55 @@
{
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": false
}
},
"env": {
"browser": true,
"node": true,
"es6": true
},
"rules": {
"arrow-spacing": "error",
"block-scoped-var": "error",
"block-spacing": "error",
"brace-style": ["error", "stroustrup", {}],
"camelcase": "error",
"comma-dangle": ["error", "never"],
"comma-spacing": ["error", { "before": false, "after": true }],
"comma-style": [1, "last"],
"consistent-this": [1, "_this"],
"curly": [1, "multi"],
"eol-last": 1,
"eqeqeq": 1,
"func-names": 1,
"indent": ["error", 2, { "SwitchCase": 1 }],
"lines-around-comment": ["error", { "beforeBlockComment": true, "allowArrayStart": true }],
"max-len": [1, 120, 2], // 2 spaces per tab, max 80 chars per line
"new-cap": 1,
"newline-before-return": "error",
"no-array-constructor": 1,
"no-inner-declarations": [1, "both"],
"no-mixed-spaces-and-tabs": 1,
"no-multi-spaces": 2,
"no-new-object": 1,
"no-shadow-restricted-names": 1,
"object-curly-spacing": ["error", "always"],
"padded-blocks": ["error", { "blocks": "never", "switches": "always" }],
"prefer-const": "error",
"prefer-template": "error",
"one-var": 0,
"quote-props": ["error", "always"],
"quotes": [1, "single"],
"radix": 1,
"semi": [1, "always"],
"space-before-blocks": [1, "always"],
"space-infix-ops": 1,
"vars-on-top": 1,
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 1 }],
"spaced-comment": ["error", "always", { "markers": ["/"] }]
}
}

153
.gitignore vendored Normal file
View File

@ -0,0 +1,153 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Elastic Beanstalk Files
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
/src/bundle.js
/src/bundle.js.map
/src/react/bundle.js
/src/backbone/bundle.js
/src/react/bundle.js.map
/src/es2016/bundle.js
/src/es2016/bundle.js.map
/src/backbone/bundle.js.map
/live/*

35
gulp/backbone.js Normal file
View File

@ -0,0 +1,35 @@
'use strict';
const browserify = require('browserify');
const gulp = require('gulp');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const uglify = require('gulp-uglify-es').default;
const sourcemaps = require('gulp-sourcemaps');
const gutil = require('gulp-util');
const rename = require('gulp-rename');
gulp.task('bundleBackbone', function () {
// set up the browserify instance on a task basis
const b = browserify({
'debug': true,
'entries': './src/js/app.js'
});
return b.bundle()
.pipe(source('app.js'))
.pipe(buffer())
.pipe(rename('bundle.js'))
.pipe(sourcemaps.init({ 'loadMaps': true }))
// Add transformation tasks to the pipeline here.
.pipe(uglify())
.on('error', gutil.log)
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('./live/js'));
});
gulp.task('buildBackbone', ['bundleBackbone'], function() {
gulp.watch('src/js/**/*.js', ['bundleBackbone']);
});

65
gulp/build.js Normal file
View File

@ -0,0 +1,65 @@
const gulp = require('gulp'),
autoprefixer = require('gulp-autoprefixer'),
cssnano = require('gulp-cssnano'),
uglify = require('gulp-uglify'),
rename = require('gulp-rename'),
concat = require('gulp-concat'),
cache = require('gulp-cache'),
htmlmin = require('gulp-htmlmin'),
inject = require('gulp-inject'),
del = require('del'),
htmlreplace = require('gulp-html-replace');
const scss = require('gulp-scss');
const sass = require('gulp-sass');
const googleWebFonts = require('gulp-google-webfonts');
const fontOptions = { };
gulp.task('styles', function() {
return gulp.src(['src/css/common.css'])
.pipe(autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'))
/* .pipe(gulp.dest('dist/css'))*/
/* .pipe(rename({suffix: '.min'}))*/
.pipe(cssnano())
.pipe(gulp.dest('live/css'));
});
gulp.task('copy', function() {
gulp.src(['src/img/**/*']).pipe(gulp.dest('live/img'));
gulp.src(['src/browserconfig.xml', 'src/manifest.json', 'src/service-worker.js']).pipe(gulp.dest('live'));
gulp.src(['src/index.html']).pipe(gulp.dest('live'));
});
gulp.task('clean', function() {
return del(['live']);
});
gulp.task('customMUI', function() {
return gulp.src(['src/css/custom.scss'])
.pipe(sass({ 'outputStyle': 'compressed' }).on('error', sass.logError))
// .pipe(cssnano())
.pipe(rename('mui.custom.css'))
// .pipe(gulp.dest(`${dest}/css`));
.pipe(gulp.dest('live/css'));
});
gulp.task('vendor', function() {
return gulp.src([
'node_modules/muicss/dist/js/mui.min.js'
])
.pipe(concat('vendor.js'))
/* .pipe(uglify({ 'mangle': false }))*/
.pipe(gulp.dest(`live/js`));
});
gulp.task('fonts', function() {
return gulp.src('src/fonts.list')
.pipe(googleWebFonts(fontOptions))
.pipe(gulp.dest(`live/fonts`))
;
});

7
gulpfile.js Normal file
View File

@ -0,0 +1,7 @@
const gulp = require('gulp');
const requireDir = require('require-dir');
requireDir('./gulp');
gulp.task('BuildAll', ['bundleBackbone', 'styles', 'copy', 'customMUI', 'vendor', 'fonts']);

13914
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

64
package.json Normal file
View File

@ -0,0 +1,64 @@
{
"name": "traintimespwa",
"version": "1.0.0",
"description": "Train Times Progressive Web App",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"apicache": "^1.2.0",
"babelify": "^8.0.0",
"backbone": "^1.3.3",
"browserify": "^14.5.0",
"es6-promise": "^4.1.1",
"express": "^4.16.2",
"jquery": "^3.2.1",
"log4js": "^2.4.1",
"minibus": "^3.1.0",
"muicss": "^0.9.33",
"underscore": "^1.8.3"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"del": "^3.0.0",
"eslint": "^4.12.0",
"eslint-plugin-react": "^7.4.0",
"expect.js": "^0.3.1",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^4.0.0",
"gulp-babel": "^7.0.0",
"gulp-browserify": "^0.5.1",
"gulp-cache": "^1.0.1",
"gulp-concat": "^2.6.1",
"gulp-cssnano": "^2.1.2",
"gulp-google-webfonts": "0.0.14",
"gulp-html-replace": "^1.6.2",
"gulp-htmlmin": "^3.0.0",
"gulp-inject": "^4.3.0",
"gulp-jshint": "^2.0.4",
"gulp-rename": "^1.2.2",
"gulp-sass": "^3.1.0",
"gulp-scss": "^1.4.0",
"gulp-sourcemaps": "^2.6.1",
"gulp-streamify": "^1.0.2",
"gulp-tasks": "0.0.2",
"gulp-uglify": "^3.0.0",
"gulp-uglify-es": "^0.1.3",
"gulp-util": "^3.0.8",
"gulp-webpack": "^1.5.0",
"lodash.assign": "^4.2.0",
"mocha": "^3.5.3",
"require-dir": "^0.3.2",
"sinon": "^4.1.1",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"watchify": "^3.9.0"
}
}

28
server.js Normal file
View File

@ -0,0 +1,28 @@
const express = require('express');
const path = require('path');
const apicache = require('apicache');
const logger = require('log4js').getLogger('Server');
const train = require('./server/lib/train');
logger.level = 'debug';
const app = express();
const port = process.env.PORT || 3000;
const sitePath = 'live';
apicache.options({ 'debug': true });
const cache = apicache.middleware;
app.use(express.static(path.join(__dirname, sitePath)));
app.use('/gettrains', train.getTrainTimes);
app.use('/getnexttraintimes', train.getNextTrainTimes);
app.use('/getroute', train.getRoute);
app.listen(port, (err) => {
if (err)
return logger.error('Server error:', err);
logger.info(`TrainTime Server is listening on ${port}`);
});

169
server/lib/train.js Normal file
View File

@ -0,0 +1,169 @@
// train.js
const http = require('http');
const logger = require('log4js').getLogger('train');
const trainCache = {
'last': {},
'data': {}
};
module.exports = {
'dbe_glq': function (req, res) {
logger.info('DBE:GLQ request');
const now = new Date();
const nowSeconds = (now.getHours() * (60 * 60)) + (now.getMinutes() * 60);
if (trainCache.last.dbeglq === null || nowSeconds !== trainCache.last.dbeglq)
Query(function (a, b) {
const ts = a.departures[0].service;
const output = {};
logger.debug(ts);
logger.debug(ts.sta);
output.sta = ts.sta;
output.eta = ts.eta;
trainCache.data.dbeglq = output;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(trainCache.data.dbeglq));
}, res, 'huxley.apphb.com', '/next/dbe/to/glq/1?accessToken=215b99fe-b237-4a01-aadc-cf315d6756d8');
},
'glq_dbe': function (req, res) {
logger.info('GLQ:DBE request');
const now = new Date();
const nowSeconds = (now.getHours() * (60 * 60)) + (now.getMinutes() * 60);
if (trainCache.last.glqdbe === null || nowSeconds !== trainCache.last.dbeglq)
Query(function (a, b) {
const ts = a.departures[0].service;
const output = {};
logger.debug(ts);
// GLOBAL.lastcheck = now;
logger.debug(ts.sta);
// logger.debug(toSeconds(ts.sta));
output.sta = ts.sta;
output.eta = ts.eta;
trainCache.data.glqdbe = output;
// trainCache.last.glqdbe = toSeconds(ts.sta);
// console.log(ts);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(trainCache.data.glqdbe));
}, res, 'huxley.apphb.com', '/next/glq/to/dbe/1?accessToken=215b99fe-b237-4a01-aadc-cf315d6756d8');
},
'getTrainTimes': function (req, res) {
// console.log(req);
logger.info(`getTrainTimes: ${ JSON.stringify(req.query)}`);
if (req.query.hasOwnProperty('from') && req.query.hasOwnProperty('from')) {
const url = `/all/${ req.query.from }/to/${ req.query.to }/10?accessToken=215b99fe-b237-4a01-aadc-cf315d6756d8`;
Query(function (a, b) {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(a));
}, res, 'huxley.apphb.com', url);
}
else {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({}));
}
},
'getNextTrainTimes': function (req, res) {
logger.info(`getNextTrainTimes: ${ JSON.stringify(req.query)}`);
let trainFrom, trainTo, trainToken, url;
if (req.query.hasOwnProperty('from') && req.query.hasOwnProperty('from')) {
trainFrom = req.query.from;
trainTo = req.query.to;
trainToken = trainFrom + trainTo;
url = `/next/${ trainFrom }/to/${ trainTo }/1?accessToken=215b99fe-b237-4a01-aadc-cf315d6756d8`;
logger.info(`Requesting latest time for : ${ trainToken}`);
const now = new Date();
const nowSeconds = (now.getHours() * (60 * 60)) + (now.getMinutes() * 60);
logger.info(`Now Seconds: ${ nowSeconds}`);
if (trainCache.last[trainToken] === null || nowSeconds !== trainCache.last[trainToken])
Query(function (a, b) {
const output = {};
const ts = a.departures[0].service;
if (ts !== null) {
// console.log(ts);
// GLOBAL.lastcheck = now;
logger.debug(ts.sta, ts.std);
// logger.debug(toSeconds(ts.sta));
output.sta = (ts.sta !== null) ? ts.sta : ts.std;
output.eta = (ts.eta !== null ? ts.eta : ts.etd);
// trainCache.last.glqdbe = toSeconds(ts.sta);
// console.log(ts);
}
else {
logger.warn('*** NO SERVICE');
output.sta = 'No Service';
output.eta = 'No Service';
}
trainCache.data[trainToken] = output;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(trainCache.data[trainToken]));
}, res, 'huxley.apphb.com', url);
}
}, 'getRoute': function (req, res) {
logger.info(`getRoute: ${ JSON.stringify(req.query)}`);
let routeID;
let data = {};
if (req.query.hasOwnProperty('route')) {
routeID = req.query.route;
Query(function (a, b) {
if (a !== null && a.message === null)
data = a;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(data));
}, res, 'huxley.apphb.com', `/service/${ routeID }?accessToken=215b99fe-b237-4a01-aadc-cf315d6756d8`);
}
}
};
function toSeconds(inval) {
console.log('inval', typeof inval);
if (typeof inval === 'string') {
const a = inval.split(':');
return ((parseInt(a[0]) * (60 * 60)) + (parseInt(a[1]) * 60));
}
return '';
}
function Query(callback, r, host, path) {
logger.debug(path);
const req = r;
const options = {
'host': host,
// port: 80,
'path': path,
// method: 'GET',
'headers': {}
};
try {
http.request(options).on('response', function (response) {
let data = '';
response.on('data', function (chunk) {
data += chunk;
});
response.on('end', function () {
callback(JSON.parse(data), r);
});
response.on('error', function (e) {
console.error(e);
});
}).end();
}
catch (e) {
console.log(e);
}
}

9
src/browserconfig.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/img/mstile-150x150.png"/>
<TileColor>#2b5797</TileColor>
</tile>
</msapplication>
</browserconfig>

88
src/css/common.css Normal file
View File

@ -0,0 +1,88 @@
body {
background-color: #eee;
}
.card {
position: relative;
background-color: #fff;
min-height:48px;
margin: 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);
}
.mui--text-display4, .mui--text-display3 {
font-family: "Roboto Slab", "Helvetica Neue", Helvetica, Arial;
}
.temp0, .temp1, .temp2, .temp3, .temp4, .temp5 {
color: rgb(80,181,221)
}
.temp6 {
color: rgb(78,178,206)
}
.temp7 {
color: rgb(76, 176, 190)
}
.temp8 {
color: rgb(73, 173, 175)
}
.temp9 {
color: rgb(72, 171, 159)
}
.temp10 {
color: rgb(70, 168, 142)
}
.temp11 {
color: rgb(68, 166, 125)
}
.temp12 {
color: rgb(66, 164, 108)
}
.temp13 {
color: rgb(102, 173, 94)
}
.temp14 {
color: rgb(135, 190, 64)
}
.temp15 {
color: rgb(179, 204, 26)
}
.temp16 {
color: rgb(214, 213, 28)
}
.temp17 {
color: rgb(249, 202, 3)
}
.temp18 {
color: rgb(246, 181, 3)
}
.temp19 {
color: rgb(244, 150, 26)
}
.temp20 {
color: rgb(236, 110, 5)
}
.day {
font-family: "Roboto Slab", "Helvetica Neue", Helvetica, Arial, sans-serif;
text-transform: uppercase;
}
.summary::first-letter {
text-transform: capitalize
}

93
src/css/custom.scss Normal file
View File

@ -0,0 +1,93 @@
// import MUI colors
@import "./node_modules/muicss/lib/sass/mui/colors";
// customize MUI variables
$mui-primary-color: mui-color('blue-grey', '500');
$mui-primary-color-dark: mui-color('blue-grey', '700');
$mui-primary-color-light: mui-color('blue-grey', '100');
$mui-accent-color: mui-color('deep-purple', '900');
$mui-accent-color-dark: mui-color('indigo', 'A100');
$mui-accent-color-light: mui-color('indigo', 'A400');
$mui-base-font-family: 'Roboto Slab', "Helvetica Neue", Helvetica, Arial, Verdana,"Trebuchet MS";
// import MUI SASS
@import "./node_modules/muicss/lib/sass/mui";
////
body {
background-color:mui-color('grey', '100');
}
#header {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 2;
transition: left 0.2s;
}
ul {
margin: 0;
padding: 0;
}
li {
display: inline;
margin: 0;
padding: 0 4px 0 0;
}
.dates {
padding: 2px;
border: solid 1px #80007e;
background-color: #ffffff;
}
#btc, #fx, #trend {
font-size: 85%;
}
.up, .ontime, .trendUp {
color: mui-color('green') !important;
}
.down, .delayed, .trendDown {
color: mui-color('red') !important;
}
.nochange {
color: #000000;
}
.password {
border: 1px solid mui-color('grey','400');
background-color: mui-color('grey','200');
font-family: monospace;
white-space: pre;
}
.trendUp:before {
content: "";
}
.trendDown:before{content:''}
.card {
position: relative;
background-color: #fff;
min-height:48px;
margin: 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);
}
.entry {
height: 36px;
margin: 6px 0;
vertical-align: middle;
}

2
src/fonts.list Normal file
View File

@ -0,0 +1,2 @@
Roboto+Slab
Roboto+Condensed

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
src/img/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

BIN
src/img/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
src/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/img/mstile-150x150.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,33 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="192.000000pt" height="192.000000pt" viewBox="0 0 192.000000 192.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,192.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M440 1789 c-80 -4 -104 -10 -149 -35 -63 -34 -117 -94 -145 -162 -19
-45 -20 -74 -20 -637 l0 -590 25 -56 c32 -67 91 -128 156 -160 l48 -23 605 -1
605 0 53 28 c69 35 118 86 151 156 l26 56 0 585 c-1 534 -2 590 -18 637 -33
94 -133 180 -231 199 -47 8 -969 11 -1106 3z m389 -79 c21 0 51 -44 51 -74 l0
-34 80 -1 c79 -1 80 -1 80 23 0 40 21 74 51 85 57 20 112 -21 112 -83 0 -17 5
-26 16 -26 68 0 143 -50 172 -115 17 -37 19 -74 19 -455 0 -265 -4 -429 -11
-454 -6 -23 -28 -55 -55 -81 l-44 -43 85 -89 c91 -94 101 -118 63 -153 -41
-38 -61 -28 -194 105 l-125 125 -168 0 -167 0 -128 -125 c-136 -134 -157 -145
-196 -103 -35 38 -26 60 65 155 l85 88 -35 30 c-19 16 -44 48 -55 70 -18 38
-19 65 -19 468 -1 404 1 431 19 467 32 63 105 110 171 110 11 0 16 8 14 23 -3
48 53 104 93 91 8 -2 17 -4 21 -4z"/>
<path d="M819 1532 c-48 -15 -65 -75 -31 -106 16 -14 43 -16 172 -17 169 0
190 7 190 62 0 25 -21 57 -38 60 -22 4 -282 4 -293 1z"/>
<path d="M735 1342 c-93 -5 -94 -6 -94 -192 0 -143 2 -160 19 -175 18 -16 48
-18 301 -18 l282 -1 18 23 c17 20 19 42 19 176 0 133 -2 154 -18 168 -15 14
-52 17 -242 19 -124 2 -252 2 -285 0z"/>
<path d="M685 741 c-35 -22 -50 -67 -35 -107 12 -32 55 -57 93 -56 44 1 88 50
81 89 -9 52 -15 63 -43 78 -34 17 -63 16 -96 -4z"/>
<path d="M1138 745 c-49 -27 -58 -97 -18 -137 55 -54 139 -31 154 43 7 38 -9
71 -44 92 -34 21 -56 21 -92 2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

42
src/index.html Normal file
View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TestMVC</title>
<link href="fonts/fonts.css" rel="stylesheet">
<link href="css/mui.custom.css" rel="stylesheet" type="text/css"/>
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<link rel="mask-icon" href="/img/safari-pinned-tab.svg" color="#5bbad5">
<meta name="apple-mobile-web-app-title" content="Train Times">
<meta name="application-name" content="Train Times">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<header id="header">
<div class="mui-appbar mui--appbar-line-height mui--z2">
<div class='mui-col-xs-8 mui-col-md-8 mui--appbar-height'>Train Times</div>
<div class='mui-col-xs-4 mui-col-md-4 mui--appbar-height'></div>
</div>
</header>
<div class="mui--appbar-height"></div>
<div class="mui-container">
<div id="trains"></div>
<div id='trainResults' class="mui--hide"></div>
</div>
<!--<script src="//cdn.muicss.com/mui-0.9.26/js/mui.min.js"></script>-->
<script src="js/vendor.js" async></script>
<script src="js/bundle.js" async></script>
</body>
</html>

47
src/js/app.js Normal file
View File

@ -0,0 +1,47 @@
require('muicss');
const { TrainModel, TrainView } = require('./train');
const { RouteModel, RouteView } = require('./route');
const Minibus = require('minibus');
(function () {
const bus = Minibus.create();
const app = {
'routes' : [
{ 'from': 'dbe', 'to': 'glq' },
{ 'from': 'glq', 'to': 'dbe' },
{ 'from': 'glq', 'to': 'hym' },
{ 'from': 'hym', 'to': 'glq' }
],
'views':{}
};
const routeView = new RouteView( { 'model': new RouteModel() });
app.createViews = function() {
for (const route of this.routes) {
console.log(route);
var key = Symbol(route.from + route.to);
console.log(key);
this.views[key] = new TrainView({ 'model': new TrainModel(route), 'routeView': routeView });
}
};
/*if ('serviceWorker' in navigator)
navigator.serviceWorker
.register('./service-worker.js')
.then(function() {
console.log('Service Worker Registered');
});*/
app.createViews();
/* app.views.dbqglqView = new TrainView({ 'model': new TrainModel({ 'from': 'dbe', 'to': 'glq' }) });
app.views.glqdbeView = new TrainView({ 'model': new TrainModel({ 'from': 'glq', 'to': 'dbe' }) });
app.views.glqhymView = new TrainView({ 'model': new TrainModel({ 'from': 'glq', 'to': 'hym' }) });
app.views.hymglqView = new TrainView({ 'model': new TrainModel({ 'from': 'hym', 'to': 'glq' }) });*/
})();

106
src/js/route.js Normal file
View File

@ -0,0 +1,106 @@
const $ = require('jquery');
const _ = require('underscore');
const Backbone = require('backbone');
const RouteModel = Backbone.Model.extend({
'initialize': function () {
const fromStation = this.get('from');
const toStation = this.get('to');
const routeUrl = `/gettrainsbob?from=${ this.get('from') }&to=${ this.get('to')}`;
const target = this.get('from') + this.get('to');
this.set('url', routeUrl);
this.set('routeUrl', routeUrl);
this.set('target', target);
this.set('visible', false);
this.set('trainData', { 'eta':'OFF', 'sta': 'OFF' });
this.update();
},
'update': function () {
const now = new Date;
const hours = now.getHours();
const limit = (hours < 6) ? 3600000 : 60000;
const mod = limit - (now.getTime() % limit);
if (hours >= 6)
this.getRoute();
else
this.set('trainData', { 'eta':'OFF', 'sta': 'OFF' });
const routeUpdateFn = function () {
this.update();
};
setTimeout(routeUpdateFn.bind(this), mod + 10);
},
'getRoute': function () {
const url = this.get('routeUrl');
const self = this;
if (this.get('visible') === true)
this.set('visible', false);
else
$.ajax({
'type': 'GET',
'url': url,
'data': '',
'dataType': 'json',
'timeout': 10000,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'PUT, GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
},
'success': function (data) {
// getTrainsCB(data);
// console.log('Got', data);
self.set('route', data);
self.set('visible', true);
},
'error': function (xhr, type) {
console.error('ajax error');
console.log('readyState', xhr.readyState);
console.log('status', xhr.status);
console.error(type);
}
});
},
'setRoute': function(from, to) {
this.set('from', from);
this.set('to', to);
}
});
const RouteView = Backbone.View.extend({
'tagName': 'div',
'initialize': function () {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.$trains = $('#trains');
this.$traininfo = $('#traininfo');
this.$traintext = $('#trainResults');
this.$el = this.$traintext;
this.initView();
},
'events': {
'click': 'showTrains'
},
'render': function () {
},
'initView': function () {
},
'showTrains': function () {
console.log('Show train');
},
'viewRoute': function(from, to) {
console.log('View route', from, to);
}
});
module.exports = { RouteModel, RouteView };

217
src/js/train.js Normal file
View File

@ -0,0 +1,217 @@
/**
*
* User: Martin Donnelly
* Date: 2016-10-03
* Time: 14:20
*
*/
const $ = require('jquery');
const _ = require('underscore');
const Backbone = require('backbone');
const TrainModel = Backbone.Model.extend({
'initialize': function () {
const fromStation = this.get('from');
const toStation = this.get('to');
const url = `/getnexttraintimes?from=${ this.get('from') }&to=${ this.get('to')}`;
const routeUrl = `/gettrains?from=${ this.get('from') }&to=${ this.get('to')}`;
const target = this.get('from') + this.get('to');
this.set('url', url);
this.set('routeUrl', routeUrl);
this.set('target', target);
this.set('visible', false);
this.set('trainData', { 'eta':'OFF', 'sta': 'OFF' });
this.update();
},
'update': function () {
const now = new Date;
const hours = now.getHours();
const limit = (hours < 6) ? 3600000 : 60000;
const mod = limit - (now.getTime() % limit);
if (hours >= 6)
this.getTrain();
else
this.set('trainData', { 'eta':'OFF', 'sta': 'OFF' });
const trainUpdateFn = function () {
this.update();
};
setTimeout(trainUpdateFn.bind(this), mod + 10);
},
'getTrain': function () {
const url = this.get('url');
const self = this;
$.ajax({
'type': 'GET',
'url': url,
'data': '',
'dataType': 'json',
'timeout': 10000,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'PUT, GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
},
'success': function (data) {
self.set('trainData', data);
},
'error': function (xhr, type) {
console.log('ajax error');
console.log('readyState', xhr.readyState);
console.log('status', xhr.status);
console.log(type);
}
});
},
'getRoute': function () {
const url = this.get('routeUrl');
const self = this;
if (this.get('visible') === true)
this.set('visible', false);
else
$.ajax({
'type': 'GET',
'url': url,
'data': '',
'dataType': 'json',
'timeout': 10000,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'PUT, GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
},
'success': function (data) {
// getTrainsCB(data);
// console.log('Got', data);
self.set('route', data);
self.set('visible', true);
},
'error': function (xhr, type) {
console.error('ajax error');
console.error(xhr);
console.error(type);
}
});
}
});
const TrainView = Backbone.View.extend({
'tagName': 'div',
'initialize': function () {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.$trains = $('#trains');
this.$traininfo = $('#traininfo');
this.$traintext = $('#trainResults');
this.$el = this.$trains;
this.initView();
// console.log(this.get('routeView'));
},
'events': {
'click': 'showTrains'
},
'render': function () {
const obj = this.model.get('trainData');
const visible = this.model.get('visible');
const route = this.model.get('route');
const output = (obj.eta.toLowerCase() === 'on time') ? obj.sta : obj.eta;
const status = (obj.eta.toLowerCase() === 'on time') ? 'ontime' : 'delayed';
this.$button.html(output);
this.$button.removeClass('delayed').removeClass('ontime').addClass(status);
if (visible) {
let ws = `<div>${route.locationName} TO ${route.filterLocationName}</div>
<table class="mui-table mui-table-bordered">
<tr><th>Destination</th>
<th>Time</th>
<th>Status</th>
<th>Platform</th></tr>
`;
const services = [];
if (typeof route.trainServices === 'object' && route.trainServices !== null)
for (const item of route.trainServices) {
const dest = item.destination[0];
const via = dest.via !== null ? `<em class="mui--text-accent">${dest.via}</em>` : '';
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':'train' });
if (!item.isCancelled)
ws = `${ws }<tr><td>${dest.locationName} ${via}</td>
<td>${time}</td>
<td>${status}</td>
<td>${platform}</td>
</tr>`;
else
ws = `${ws }<tr><td>${dest.locationName} ${via}</td><td>${time}</td>
<td colspan="2"> ${item.cancelReason}</td></tr>`;
}
if (typeof route.busServices === 'object' && route.busServices !== null)
for (const item of route.busServices) {
const dest = item.destination[0];
const via = dest.via !== null ? `<em>${dest.via}</em>` : '';
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' });
ws = `${ws }<tr><td>🚌 ${dest.locationName} ${via}</td><td>${time}</td><td>${status}</td><td>${platform}</td></tr>`;
}
ws = `${ws }</table>`;
this.$traintext.empty().html(ws);
this.$traintext.removeClass('mui--hide').addClass('mui--show');
}
else
this.$traintext.removeClass('mui--show').addClass('mui--hide');
},
'initView': function () {
const self = this;
const target = this.model.get('target');
const html = `
<div class="mui-row card">
<div class='mui-col-xs-8 mui-col-md-8 entry'>${target.toUpperCase()}</div>
<div class='mui-col-xs-4 mui-col-md-4'>
<button class="mui-btn mui-btn--flat" id="${target}"></button>
</div>
</div>
`;
this.$html = $(html);
this.$html.on('click', function () {
// console.log(self)
self.model.getRoute();
});
this.$trains.append(this.$html);
this.$button = $(`#${target}`);
const output = 'OFF';
const status = (output === 'on time') ? 'ontime' : 'delayed';
this.$button.html(output);
this.$button.removeClass('delayed').removeClass('ontime').addClass(status);
const cevent = `click #${target}`;
this.events[cevent] = 'showTrains';
},
'showTrains': function () {
console.log('Show train');
}
});
module.exports = { TrainModel, TrainView };

15
src/manifest.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "Train Times",
"icons": [
{
"src": "/img/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"start_url": "/index.html",
"imgdisplay": "standalone",
"display": "standalone"
}

91
src/service-worker.js Normal file
View File

@ -0,0 +1,91 @@
// Copyright 2016 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
var dataCacheName = 'traintimesData-v1';
var cacheName = 'traintimePWA-final-1';
var filesToCache = [
'/',
'/index.html',
'/js/bundle.js',
'/css/common.css'
];
self.addEventListener('install', function(e) {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(filesToCache);
})
);
});
self.addEventListener('activate', function(e) {
console.log('[ServiceWorker] Activate');
e.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if (key !== cacheName && key !== dataCacheName) {
console.log('[ServiceWorker] Removing old cache', key);
return caches.delete(key);
}
}));
})
);
/*
* Fixes a corner case in which the app wasn't returning the latest data.
* You can reproduce the corner case by commenting out the line below and
* then doing the following steps: 1) load app for first time so that the
* initial New York City data is shown 2) press the refresh button on the
* app 3) go offline 4) reload the app. You expect to see the newer NYC
* data, but you actually see the initial data. This happens because the
* service worker is not yet activated. The code below essentially lets
* you activate the service worker faster.
*/
return self.clients.claim();
});
self.addEventListener('fetch', function(e) {
console.log('[Service Worker] Fetch', e.request.url);
var dataUrl = '/getnexttraintimes?';
if (e.request.url.indexOf(dataUrl) > -1) {
console.log('!');
/*
* When the request URL contains dataUrl, the app is asking for fresh
* weather data. In this case, the service worker always goes to the
* network and then caches the response. This is called the "Cache then
* network" strategy:
* https://jakearchibald.com/2014/offline-cookbook/#cache-then-network
*/
e.respondWith(
caches.open(dataCacheName).then(function(cache) {
return fetch(e.request).then(function(response){
cache.put(e.request.url, response.clone());
return response;
});
})
);
} else {
/*
* The app is asking for app shell files. In this scenario the app uses the
* "Cache, falling back to the network" offline strategy:
* https://jakearchibald.com/2014/offline-cookbook/#cache-falling-back-to-network
*/
e.respondWith(
caches.match(e.request).then(function(response) {
return response || fetch(e.request);
})
);
}
});

24260
src/work/stations.json Normal file

File diff suppressed because it is too large Load Diff