Browse Source

First commit

master
Stéphane Bérubé 1 year ago
commit
a4dd737f24

+ 2
- 0
.gitignore View File

@@ -0,0 +1,2 @@
1
+config.json
2
+*.swp

+ 15
- 0
README.md View File

@@ -0,0 +1,15 @@
1
+# OC Transpo GPS Bus Tracker
2
+
3
+Using the [OC Transpo API](http://www.octranspo.com/index.php/developers) to
4
+track the next buses' GPS location at my morning bus stop. This way, I can
5
+spend the least amount of time waiting for the bus outside in the winter. I'm
6
+also using the [navitia API](https://www.navitia.io/) to get the scheduled
7
+times for the upcoming buses.
8
+
9
+## Thanks
10
+
11
+* [ionicons](https://github.com/ionic-team/ionicons)
12
+* [leaflet](https://github.com/Leaflet/Leaflet)
13
+* [leaflet.awesome-markers](https://github.com/lvoogdt/Leaflet.awesome-markers)
14
+* [OpenStreetMap](https://www.openstreetmap.org)
15
+

+ 49
- 0
classes/Api.php View File

@@ -0,0 +1,49 @@
1
+<?php
2
+
3
+// TODO: Abstract, probably
4
+class Api {
5
+    protected $prefix = '';
6
+    protected $dailyLimit = 0;
7
+    protected $last_time_we_accessed_api;
8
+    protected $nb_requests_made_today;
9
+
10
+    public function __construct() {
11
+        $this->last_time_we_accessed_api = apcu_fetch($this->prefix . 'last_time_we_accessed_api') ?: null;
12
+        $this->nb_requests_made_today = apcu_fetch($this->prefix . 'nb_requests_made_today') ?: 0;
13
+
14
+        // Reset counter if last time we fetch from api wasn't today
15
+        // or if we don't know when the last time API was accessed
16
+        if ($this->last_time_we_accessed_api === null || !$this->is_today($this->last_time_we_accessed_api)) {
17
+            $this->setProp('nb_requests_made_today', 0);
18
+        }
19
+    }
20
+
21
+    // TODO: Utils
22
+    public function is_today($timestamp) {
23
+        return date('Ymd') === date('Ymd', $timestamp);
24
+    }
25
+
26
+    public function apiCallsRemainingToday() {
27
+        return $this->dailyLimit - $this->nb_requests_made_today;
28
+    }
29
+
30
+    public function isWaitTimeExpired() {
31
+        $last = $this->last_time_we_accessed_api;
32
+        $waitTime = $this->waitTime();
33
+
34
+        return ($waitTime + $last < time());
35
+    }
36
+
37
+    public function waitTime() {
38
+        $api_connections_left_today = $this->apiCallsRemainingToday();
39
+        $seconds_left_before_midnight = strtotime('tomorrow') - time();
40
+
41
+        return max( ($seconds_left_before_midnight / $api_connections_left_today), 1 );
42
+    }
43
+
44
+    public function setProp($propName, $value) {
45
+        apcu_store($this->prefix . $propName, $value);
46
+        $this->$propName = $value;
47
+    }
48
+}
49
+

+ 58
- 0
classes/Navitia.php View File

@@ -0,0 +1,58 @@
1
+<?php
2
+
3
+require_once(dirname(__FILE__) . '/Api.php');
4
+
5
+class Navitia extends Api {
6
+    // Cache prefix
7
+    protected $prefix = 'nav-';
8
+
9
+    // 3,000 API calls per day
10
+    // The ToS for the free account says 90,000 per month.
11
+    // Let's just say it's 3,000 per day for now (assumes a 30-day month)
12
+    protected $dailyLimit = 3000;
13
+
14
+    // Credentials
15
+    protected $token;
16
+
17
+    public function __construct($token) {
18
+        parent::__construct();
19
+
20
+        $this->token = $token;
21
+    }
22
+
23
+    public function getStopSchedule($stopId) {
24
+        if ($this->isWaitTimeExpired()) {
25
+            $last_data_received_from_api = $this->_getStopSchedule($stopId);
26
+
27
+            $this->setProp('last_data_received_from_api', $last_data_received_from_api);
28
+            $this->setProp('last_time_we_accessed_api', time());
29
+            $this->setProp('nb_requests_made_today', $this->nb_requests_made_today += 1);
30
+        } else {
31
+            $last_data_received_from_api = apcu_fetch($this->prefix . 'last_data_received_from_api');
32
+        }
33
+
34
+        return $last_data_received_from_api;
35
+    }
36
+
37
+    private function _getStopSchedule($stopId) {
38
+        // TODO: Error handling
39
+        $ch = curl_init();
40
+        curl_setopt($ch, CURLOPT_URL, "https://" . $this->token . "@api.navitia.io/v1/coverage/ca-on/stop_points/stop_point:" . $stopId  . "/stop_schedules");
41
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
42
+        $response = curl_exec($ch);
43
+        curl_close($ch);
44
+
45
+        // TODO: Error handling
46
+        $json = json_decode($response);
47
+
48
+        $tmp_dateTimes = $json->stop_schedules[0]->date_times;
49
+
50
+        $dateTime = array();
51
+        foreach($tmp_dateTimes as $tmp_dateTime) {
52
+            $dateTime[] = strtotime($tmp_dateTime->date_time);
53
+        }
54
+
55
+        return json_encode($dateTime);
56
+    }
57
+}
58
+

+ 81
- 0
classes/Octranspo.php View File

@@ -0,0 +1,81 @@
1
+<?php
2
+
3
+require_once(dirname(__FILE__) . '/Api.php');
4
+
5
+class OCTranspo extends Api {
6
+    // Cache prefix
7
+    protected $prefix = 'oc-';
8
+
9
+    // 10,000 API calls per day
10
+    // The ToS say 10,000 while the account plan
11
+    // says 20,000. Assume it's 10,000 to be safe.
12
+    protected $dailyLimit = 10000;
13
+
14
+    // Credentials
15
+    private $app_id;
16
+    private $api_key;
17
+
18
+    public function __construct($appId, $apiKey) {
19
+        parent::__construct();
20
+
21
+        $this->app_id = $appId;
22
+        $this->api_key = $apiKey;
23
+    }
24
+
25
+    public function getNextTrips($stopNo, $routeNo) {
26
+        if ($this->isWaitTimeExpired()) {
27
+            $last_data_received_from_api = $this->_getNextTrips($stopNo, $routeNo);
28
+
29
+            $this->setProp('last_data_received_from_api', $last_data_received_from_api);
30
+            $this->setProp('last_time_we_accessed_api', time());
31
+            $this->setProp('nb_requests_made_today', $this->nb_requests_made_today += 1);
32
+        } else {
33
+            $last_data_received_from_api = apcu_fetch($this->prefix . 'last_data_received_from_api');
34
+        }
35
+
36
+        return $last_data_received_from_api;
37
+    }
38
+
39
+    private function _getNextTrips($stopNo, $routeNo) {
40
+        $data = [
41
+            'appID' => $this->app_id,
42
+            'apiKey' => $this->api_key,
43
+            'routeNo' => $routeNo,
44
+            'stopNo' => $stopNo
45
+        ];
46
+
47
+        // TODO: Error handling
48
+        $ch = curl_init();
49
+        curl_setopt($ch, CURLOPT_URL, "https://api.octranspo1.com/v1.2/GetNextTripsForStop");
50
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
51
+        curl_setopt($ch, CURLOPT_POST, true);
52
+        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
53
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
54
+        $response = curl_exec($ch);
55
+        curl_close($ch);
56
+
57
+        $coords = $this->parse_xml_response($response);
58
+
59
+        return json_encode($coords);
60
+    }
61
+
62
+    // TODO: Utils
63
+    private function parse_xml_response($response) {
64
+        $coords = [];
65
+
66
+        // TODO: Error handling
67
+        $xmlParser = new SimpleXMLElement($response);
68
+        $xmlParser->registerXPathNamespace('tempuri', 'http://tempuri.org/');
69
+        $trips = $xmlParser->xpath('//tempuri:Trip');
70
+
71
+        foreach ($trips as $trip) {
72
+            $coords[] = [
73
+                'longitude' => (string)$trip->Longitude,
74
+                'latitude' => (string)$trip->Latitude
75
+            ];
76
+        }
77
+
78
+        return $coords;
79
+    }
80
+}
81
+

+ 13
- 0
config.json.dist View File

@@ -0,0 +1,13 @@
1
+{
2
+    "octranspo": {
3
+        "appId": "",
4
+        "apiKey": "",
5
+        "busNo": 0,
6
+        "stopNo": 0
7
+    },
8
+    "navitia": {
9
+        "token": "",
10
+        "stopId": ""
11
+    }
12
+}
13
+

+ 77
- 0
public/css/styles.css View File

@@ -0,0 +1,77 @@
1
+body,
2
+html {
3
+    margin: 0;
4
+    padding: 0;
5
+}
6
+
7
+summary {
8
+    cursor: pointer;
9
+}
10
+
11
+.container {
12
+    position: relative;
13
+}
14
+
15
+.schedule {
16
+    background: rgba(255, 255, 255, 0.75);
17
+    margin-left: 20px;
18
+    padding: 15px;
19
+    position: absolute;
20
+    z-index: 401;
21
+}
22
+
23
+.schedule__title {
24
+    font-size: 24px;
25
+    font-weight: bold;
26
+    margin: 0;
27
+}
28
+
29
+.schedule__times {
30
+    margin: 15px 0 0 15px;
31
+    padding: 0;
32
+}
33
+
34
+.schedule__item:nth-child(n+4) {
35
+    display: none;
36
+}
37
+
38
+.schedule__times li {
39
+    margin-bottom: 15px;
40
+}
41
+
42
+.schedule__item--expired {
43
+    opacity: 0.5;
44
+}
45
+
46
+.schedule__time:last-child {
47
+    margin-bottom: 0;
48
+}
49
+
50
+#map {
51
+    height: 100vh;
52
+}
53
+
54
+/* https://github.com/twbs/bootstrap/blob/29d58fb758683db42c2d716ac654dea3ab6063c7/scss/mixins/_screen-reader.scss#L6 */
55
+.custom-checkbox__input {
56
+    clip: rect(0, 0, 0, 0);
57
+    clip-path: inset(50%);
58
+    border: 0;
59
+    height: 1px;
60
+    overflow: hidden;
61
+    padding: 0;
62
+    position: absolute;
63
+    white-space: nowrap;
64
+    width: 1px;
65
+}
66
+
67
+.custom-checkbox__label {
68
+    border: 1px solid #bee5eb;
69
+    background: #d1ecf1;
70
+    padding: 10px 5px;
71
+}
72
+
73
+.custom-checkbox__input:checked + .custom-checkbox__label,
74
+.custom-checkbox__input:focus + .custom-checkbox__label {
75
+    background: #fff;
76
+}
77
+

+ 11
- 0
public/extlib/ionicons/css/ionicons.min.css
File diff suppressed because it is too large
View File


BIN
public/extlib/ionicons/fonts/ionicons.eot View File


+ 2230
- 0
public/extlib/ionicons/fonts/ionicons.svg
File diff suppressed because it is too large
View File


BIN
public/extlib/ionicons/fonts/ionicons.ttf View File


BIN
public/extlib/ionicons/fonts/ionicons.woff View File


BIN
public/extlib/leaflet.awesome-markers/images/markers-matte.png View File


BIN
public/extlib/leaflet.awesome-markers/images/markers-matte@2x.png View File


BIN
public/extlib/leaflet.awesome-markers/images/markers-plain.png View File


BIN
public/extlib/leaflet.awesome-markers/images/markers-shadow.png View File


BIN
public/extlib/leaflet.awesome-markers/images/markers-shadow@2x.png View File


BIN
public/extlib/leaflet.awesome-markers/images/markers-soft.png View File


BIN
public/extlib/leaflet.awesome-markers/images/markers-soft@2x.png View File


+ 124
- 0
public/extlib/leaflet.awesome-markers/leaflet.awesome-markers.css View File

@@ -0,0 +1,124 @@
1
+/*
2
+Author: L. Voogdt
3
+License: MIT
4
+Version: 1.0
5
+*/
6
+
7
+/* Marker setup */
8
+.awesome-marker {
9
+  background: url('images/markers-soft.png') no-repeat 0 0;
10
+  width: 35px;
11
+  height: 46px;
12
+  position:absolute;
13
+  left:0;
14
+  top:0;
15
+  display: block;
16
+  text-align: center;
17
+}
18
+
19
+.awesome-marker-shadow {
20
+  background: url('images/markers-shadow.png') no-repeat 0 0;
21
+  width: 36px;
22
+  height: 16px;
23
+}
24
+
25
+/* Retina displays */
26
+@media (min--moz-device-pixel-ratio: 1.5),(-o-min-device-pixel-ratio: 3/2),
27
+(-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5),(min-resolution: 1.5dppx) {
28
+ .awesome-marker {
29
+  background-image: url('images/markers-soft@2x.png');
30
+  background-size: 720px 46px;
31
+ }
32
+ .awesome-marker-shadow {
33
+  background-image: url('images/markers-shadow@2x.png');
34
+  background-size: 35px 16px;
35
+ }
36
+}
37
+
38
+.awesome-marker i {
39
+  color: #333;
40
+  margin-top: 10px;
41
+  display: inline-block;
42
+  font-size: 14px;
43
+}
44
+
45
+.awesome-marker .icon-white {
46
+  color: #fff;
47
+}
48
+
49
+/* Colors */
50
+.awesome-marker-icon-red {
51
+  background-position: 0 0;
52
+}
53
+
54
+.awesome-marker-icon-darkred {
55
+  background-position: -180px 0;
56
+}
57
+
58
+.awesome-marker-icon-lightred {
59
+  background-position: -360px 0;
60
+}
61
+
62
+.awesome-marker-icon-orange {
63
+  background-position: -36px 0;
64
+}
65
+
66
+.awesome-marker-icon-beige {
67
+  background-position: -396px 0;
68
+}
69
+
70
+.awesome-marker-icon-green {
71
+  background-position: -72px 0;
72
+}
73
+
74
+.awesome-marker-icon-darkgreen {
75
+  background-position: -252px 0;
76
+}
77
+
78
+.awesome-marker-icon-lightgreen {
79
+  background-position: -432px 0;
80
+}
81
+
82
+.awesome-marker-icon-blue {
83
+  background-position: -108px 0;
84
+}
85
+
86
+.awesome-marker-icon-darkblue {
87
+  background-position: -216px 0;
88
+}
89
+
90
+.awesome-marker-icon-lightblue {
91
+  background-position: -468px 0;
92
+}
93
+
94
+.awesome-marker-icon-purple {
95
+  background-position: -144px 0;
96
+}
97
+
98
+.awesome-marker-icon-darkpurple {
99
+  background-position: -288px 0;
100
+}
101
+
102
+.awesome-marker-icon-pink {
103
+  background-position: -504px 0;
104
+}
105
+
106
+.awesome-marker-icon-cadetblue {
107
+  background-position: -324px 0;
108
+}
109
+
110
+.awesome-marker-icon-white {
111
+  background-position: -574px 0;
112
+}
113
+
114
+.awesome-marker-icon-gray {
115
+  background-position: -648px 0;
116
+}
117
+
118
+.awesome-marker-icon-lightgray {
119
+  background-position: -612px 0;
120
+}
121
+
122
+.awesome-marker-icon-black {
123
+  background-position: -682px 0;
124
+}

+ 7
- 0
public/extlib/leaflet.awesome-markers/leaflet.awesome-markers.min.js View File

@@ -0,0 +1,7 @@
1
+/*
2
+  Leaflet.AwesomeMarkers, a plugin that adds colorful iconic markers for Leaflet, based on the Font Awesome icons
3
+  (c) 2012-2013, Lennard Voogdt
4
+
5
+  http://leafletjs.com
6
+  https://github.com/lvoogdt
7
+*//*global L*/(function(e,t,n){"use strict";L.AwesomeMarkers={};L.AwesomeMarkers.version="2.0.1";L.AwesomeMarkers.Icon=L.Icon.extend({options:{iconSize:[35,45],iconAnchor:[17,42],popupAnchor:[1,-32],shadowAnchor:[10,12],shadowSize:[36,16],className:"awesome-marker",prefix:"glyphicon",spinClass:"fa-spin",icon:"home",markerColor:"blue",iconColor:"white"},initialize:function(e){e=L.Util.setOptions(this,e)},createIcon:function(){var e=t.createElement("div"),n=this.options;n.icon&&(e.innerHTML=this._createInner());n.bgPos&&(e.style.backgroundPosition=-n.bgPos.x+"px "+ -n.bgPos.y+"px");this._setIconStyles(e,"icon-"+n.markerColor);return e},_createInner:function(){var e,t="",n="",r="",i=this.options;i.icon.slice(0,i.prefix.length+1)===i.prefix+"-"?e=i.icon:e=i.prefix+"-"+i.icon;i.spin&&typeof i.spinClass=="string"&&(t=i.spinClass);i.iconColor&&(i.iconColor==="white"||i.iconColor==="black"?n="icon-"+i.iconColor:r="style='color: "+i.iconColor+"' ");return"<i "+r+"class='"+i.prefix+" "+e+" "+t+" "+n+"'></i>"},_setIconStyles:function(e,t){var n=this.options,r=L.point(n[t==="shadow"?"shadowSize":"iconSize"]),i;t==="shadow"?i=L.point(n.shadowAnchor||n.iconAnchor):i=L.point(n.iconAnchor);!i&&r&&(i=r.divideBy(2,!0));e.className="awesome-marker-"+t+" "+n.className;if(i){e.style.marginLeft=-i.x+"px";e.style.marginTop=-i.y+"px"}if(r){e.style.width=r.x+"px";e.style.height=r.y+"px"}},createShadow:function(){var e=t.createElement("div");this._setIconStyles(e,"shadow");return e}});L.AwesomeMarkers.icon=function(e){return new L.AwesomeMarkers.Icon(e)}})(this,document);

+ 632
- 0
public/extlib/leaflet/leaflet.css View File

@@ -0,0 +1,632 @@
1
+/* required styles */
2
+
3
+.leaflet-pane,
4
+.leaflet-tile,
5
+.leaflet-marker-icon,
6
+.leaflet-marker-shadow,
7
+.leaflet-tile-container,
8
+.leaflet-pane > svg,
9
+.leaflet-pane > canvas,
10
+.leaflet-zoom-box,
11
+.leaflet-image-layer,
12
+.leaflet-layer {
13
+	position: absolute;
14
+	left: 0;
15
+	top: 0;
16
+	}
17
+.leaflet-container {
18
+	overflow: hidden;
19
+	}
20
+.leaflet-tile,
21
+.leaflet-marker-icon,
22
+.leaflet-marker-shadow {
23
+	-webkit-user-select: none;
24
+	   -moz-user-select: none;
25
+	        user-select: none;
26
+	  -webkit-user-drag: none;
27
+	}
28
+/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
29
+.leaflet-safari .leaflet-tile {
30
+	image-rendering: -webkit-optimize-contrast;
31
+	}
32
+/* hack that prevents hw layers "stretching" when loading new tiles */
33
+.leaflet-safari .leaflet-tile-container {
34
+	width: 1600px;
35
+	height: 1600px;
36
+	-webkit-transform-origin: 0 0;
37
+	}
38
+.leaflet-marker-icon,
39
+.leaflet-marker-shadow {
40
+	display: block;
41
+	}
42
+/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
43
+/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
44
+.leaflet-container .leaflet-overlay-pane svg,
45
+.leaflet-container .leaflet-marker-pane img,
46
+.leaflet-container .leaflet-shadow-pane img,
47
+.leaflet-container .leaflet-tile-pane img,
48
+.leaflet-container img.leaflet-image-layer {
49
+	max-width: none !important;
50
+	}
51
+
52
+.leaflet-container.leaflet-touch-zoom {
53
+	-ms-touch-action: pan-x pan-y;
54
+	touch-action: pan-x pan-y;
55
+	}
56
+.leaflet-container.leaflet-touch-drag {
57
+	-ms-touch-action: pinch-zoom;
58
+	}
59
+.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
60
+	-ms-touch-action: none;
61
+	touch-action: none;
62
+}
63
+.leaflet-container {
64
+	-webkit-tap-highlight-color: transparent;
65
+}
66
+.leaflet-container a {
67
+	-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
68
+}
69
+.leaflet-tile {
70
+	filter: inherit;
71
+	visibility: hidden;
72
+	}
73
+.leaflet-tile-loaded {
74
+	visibility: inherit;
75
+	}
76
+.leaflet-zoom-box {
77
+	width: 0;
78
+	height: 0;
79
+	-moz-box-sizing: border-box;
80
+	     box-sizing: border-box;
81
+	z-index: 800;
82
+	}
83
+/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
84
+.leaflet-overlay-pane svg {
85
+	-moz-user-select: none;
86
+	}
87
+
88
+.leaflet-pane         { z-index: 400; }
89
+
90
+.leaflet-tile-pane    { z-index: 200; }
91
+.leaflet-overlay-pane { z-index: 400; }
92
+.leaflet-shadow-pane  { z-index: 500; }
93
+.leaflet-marker-pane  { z-index: 600; }
94
+.leaflet-tooltip-pane   { z-index: 650; }
95
+.leaflet-popup-pane   { z-index: 700; }
96
+
97
+.leaflet-map-pane canvas { z-index: 100; }
98
+.leaflet-map-pane svg    { z-index: 200; }
99
+
100
+.leaflet-vml-shape {
101
+	width: 1px;
102
+	height: 1px;
103
+	}
104
+.lvml {
105
+	behavior: url(#default#VML);
106
+	display: inline-block;
107
+	position: absolute;
108
+	}
109
+
110
+
111
+/* control positioning */
112
+
113
+.leaflet-control {
114
+	position: relative;
115
+	z-index: 800;
116
+	pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
117
+	pointer-events: auto;
118
+	}
119
+.leaflet-top,
120
+.leaflet-bottom {
121
+	position: absolute;
122
+	z-index: 1000;
123
+	pointer-events: none;
124
+	}
125
+.leaflet-top {
126
+	top: 0;
127
+	}
128
+.leaflet-right {
129
+	right: 0;
130
+	}
131
+.leaflet-bottom {
132
+	bottom: 0;
133
+	}
134
+.leaflet-left {
135
+	left: 0;
136
+	}
137
+.leaflet-control {
138
+	float: left;
139
+	clear: both;
140
+	}
141
+.leaflet-right .leaflet-control {
142
+	float: right;
143
+	}
144
+.leaflet-top .leaflet-control {
145
+	margin-top: 10px;
146
+	}
147
+.leaflet-bottom .leaflet-control {
148
+	margin-bottom: 10px;
149
+	}
150
+.leaflet-left .leaflet-control {
151
+	margin-left: 10px;
152
+	}
153
+.leaflet-right .leaflet-control {
154
+	margin-right: 10px;
155
+	}
156
+
157
+
158
+/* zoom and fade animations */
159
+
160
+.leaflet-fade-anim .leaflet-tile {
161
+	will-change: opacity;
162
+	}
163
+.leaflet-fade-anim .leaflet-popup {
164
+	opacity: 0;
165
+	-webkit-transition: opacity 0.2s linear;
166
+	   -moz-transition: opacity 0.2s linear;
167
+	     -o-transition: opacity 0.2s linear;
168
+	        transition: opacity 0.2s linear;
169
+	}
170
+.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
171
+	opacity: 1;
172
+	}
173
+.leaflet-zoom-animated {
174
+	-webkit-transform-origin: 0 0;
175
+	    -ms-transform-origin: 0 0;
176
+	        transform-origin: 0 0;
177
+	}
178
+.leaflet-zoom-anim .leaflet-zoom-animated {
179
+	will-change: transform;
180
+	}
181
+.leaflet-zoom-anim .leaflet-zoom-animated {
182
+	-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
183
+	   -moz-transition:    -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
184
+	     -o-transition:      -o-transform 0.25s cubic-bezier(0,0,0.25,1);
185
+	        transition:         transform 0.25s cubic-bezier(0,0,0.25,1);
186
+	}
187
+.leaflet-zoom-anim .leaflet-tile,
188
+.leaflet-pan-anim .leaflet-tile {
189
+	-webkit-transition: none;
190
+	   -moz-transition: none;
191
+	     -o-transition: none;
192
+	        transition: none;
193
+	}
194
+
195
+.leaflet-zoom-anim .leaflet-zoom-hide {
196
+	visibility: hidden;
197
+	}
198
+
199
+
200
+/* cursors */
201
+
202
+.leaflet-interactive {
203
+	cursor: pointer;
204
+	}
205
+.leaflet-grab {
206
+	cursor: -webkit-grab;
207
+	cursor:    -moz-grab;
208
+	}
209
+.leaflet-crosshair,
210
+.leaflet-crosshair .leaflet-interactive {
211
+	cursor: crosshair;
212
+	}
213
+.leaflet-popup-pane,
214
+.leaflet-control {
215
+	cursor: auto;
216
+	}
217
+.leaflet-dragging .leaflet-grab,
218
+.leaflet-dragging .leaflet-grab .leaflet-interactive,
219
+.leaflet-dragging .leaflet-marker-draggable {
220
+	cursor: move;
221
+	cursor: -webkit-grabbing;
222
+	cursor:    -moz-grabbing;
223
+	}
224
+
225
+/* marker & overlays interactivity */
226
+.leaflet-marker-icon,
227
+.leaflet-marker-shadow,
228
+.leaflet-image-layer,
229
+.leaflet-pane > svg path,
230
+.leaflet-tile-container {
231
+	pointer-events: none;
232
+	}
233
+
234
+.leaflet-marker-icon.leaflet-interactive,
235
+.leaflet-image-layer.leaflet-interactive,
236
+.leaflet-pane > svg path.leaflet-interactive {
237
+	pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
238
+	pointer-events: auto;
239
+	}
240
+
241
+/* visual tweaks */
242
+
243
+.leaflet-container {
244
+	background: #ddd;
245
+	outline: 0;
246
+	}
247
+.leaflet-container a {
248
+	color: #0078A8;
249
+	}
250
+.leaflet-container a.leaflet-active {
251
+	outline: 2px solid orange;
252
+	}
253
+.leaflet-zoom-box {
254
+	border: 2px dotted #38f;
255
+	background: rgba(255,255,255,0.5);
256
+	}
257
+
258
+
259
+/* general typography */
260
+.leaflet-container {
261
+	font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
262
+	}
263
+
264
+
265
+/* general toolbar styles */
266
+
267
+.leaflet-bar {
268
+	box-shadow: 0 1px 5px rgba(0,0,0,0.65);
269
+	border-radius: 4px;
270
+	}
271
+.leaflet-bar a,
272
+.leaflet-bar a:hover {
273
+	background-color: #fff;
274
+	border-bottom: 1px solid #ccc;
275
+	width: 26px;
276
+	height: 26px;
277
+	line-height: 26px;
278
+	display: block;
279
+	text-align: center;
280
+	text-decoration: none;
281
+	color: black;
282
+	}
283
+.leaflet-bar a,
284
+.leaflet-control-layers-toggle {
285
+	background-position: 50% 50%;
286
+	background-repeat: no-repeat;
287
+	display: block;
288
+	}
289
+.leaflet-bar a:hover {
290
+	background-color: #f4f4f4;
291
+	}
292
+.leaflet-bar a:first-child {
293
+	border-top-left-radius: 4px;
294
+	border-top-right-radius: 4px;
295
+	}
296
+.leaflet-bar a:last-child {
297
+	border-bottom-left-radius: 4px;
298
+	border-bottom-right-radius: 4px;
299
+	border-bottom: none;
300
+	}
301
+.leaflet-bar a.leaflet-disabled {
302
+	cursor: default;
303
+	background-color: #f4f4f4;
304
+	color: #bbb;
305
+	}
306
+
307
+.leaflet-touch .leaflet-bar a {
308
+	width: 30px;
309
+	height: 30px;
310
+	line-height: 30px;
311
+	}
312
+.leaflet-touch .leaflet-bar a:first-child {
313
+	border-top-left-radius: 2px;
314
+	border-top-right-radius: 2px;
315
+	}
316
+.leaflet-touch .leaflet-bar a:last-child {
317
+	border-bottom-left-radius: 2px;
318
+	border-bottom-right-radius: 2px;
319
+	}
320
+
321
+/* zoom control */
322
+
323
+.leaflet-control-zoom-in,
324
+.leaflet-control-zoom-out {
325
+	font: bold 18px 'Lucida Console', Monaco, monospace;
326
+	text-indent: 1px;
327
+	}
328
+
329
+.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out  {
330
+	font-size: 22px;
331
+	}
332
+
333
+
334
+/* layers control */
335
+
336
+.leaflet-control-layers {
337
+	box-shadow: 0 1px 5px rgba(0,0,0,0.4);
338
+	background: #fff;
339
+	border-radius: 5px;
340
+	}
341
+.leaflet-control-layers-toggle {
342
+	background-image: url(images/layers.png);
343
+	width: 36px;
344
+	height: 36px;
345
+	}
346
+.leaflet-retina .leaflet-control-layers-toggle {
347
+	background-image: url(images/layers-2x.png);
348
+	background-size: 26px 26px;
349
+	}
350
+.leaflet-touch .leaflet-control-layers-toggle {
351
+	width: 44px;
352
+	height: 44px;
353
+	}
354
+.leaflet-control-layers .leaflet-control-layers-list,
355
+.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
356
+	display: none;
357
+	}
358
+.leaflet-control-layers-expanded .leaflet-control-layers-list {
359
+	display: block;
360
+	position: relative;
361
+	}
362
+.leaflet-control-layers-expanded {
363
+	padding: 6px 10px 6px 6px;
364
+	color: #333;
365
+	background: #fff;
366
+	}
367
+.leaflet-control-layers-scrollbar {
368
+	overflow-y: scroll;
369
+	overflow-x: hidden;
370
+	padding-right: 5px;
371
+	}
372
+.leaflet-control-layers-selector {
373
+	margin-top: 2px;
374
+	position: relative;
375
+	top: 1px;
376
+	}
377
+.leaflet-control-layers label {
378
+	display: block;
379
+	}
380
+.leaflet-control-layers-separator {
381
+	height: 0;
382
+	border-top: 1px solid #ddd;
383
+	margin: 5px -10px 5px -6px;
384
+	}
385
+
386
+/* Default icon URLs */
387
+.leaflet-default-icon-path {
388
+	background-image: url(images/marker-icon.png);
389
+	}
390
+
391
+
392
+/* attribution and scale controls */
393
+
394
+.leaflet-container .leaflet-control-attribution {
395
+	background: #fff;
396
+	background: rgba(255, 255, 255, 0.7);
397
+	margin: 0;
398
+	}
399
+.leaflet-control-attribution,
400
+.leaflet-control-scale-line {
401
+	padding: 0 5px;
402
+	color: #333;
403
+	}
404
+.leaflet-control-attribution a {
405
+	text-decoration: none;
406
+	}
407
+.leaflet-control-attribution a:hover {
408
+	text-decoration: underline;
409
+	}
410
+.leaflet-container .leaflet-control-attribution,
411
+.leaflet-container .leaflet-control-scale {
412
+	font-size: 11px;
413
+	}
414
+.leaflet-left .leaflet-control-scale {
415
+	margin-left: 5px;
416
+	}
417
+.leaflet-bottom .leaflet-control-scale {
418
+	margin-bottom: 5px;
419
+	}
420
+.leaflet-control-scale-line {
421
+	border: 2px solid #777;
422
+	border-top: none;
423
+	line-height: 1.1;
424
+	padding: 2px 5px 1px;
425
+	font-size: 11px;
426
+	white-space: nowrap;
427
+	overflow: hidden;
428
+	-moz-box-sizing: border-box;
429
+	     box-sizing: border-box;
430
+
431
+	background: #fff;
432
+	background: rgba(255, 255, 255, 0.5);
433
+	}
434
+.leaflet-control-scale-line:not(:first-child) {
435
+	border-top: 2px solid #777;
436
+	border-bottom: none;
437
+	margin-top: -2px;
438
+	}
439
+.leaflet-control-scale-line:not(:first-child):not(:last-child) {
440
+	border-bottom: 2px solid #777;
441
+	}
442
+
443
+.leaflet-touch .leaflet-control-attribution,
444
+.leaflet-touch .leaflet-control-layers,
445
+.leaflet-touch .leaflet-bar {
446
+	box-shadow: none;
447
+	}
448
+.leaflet-touch .leaflet-control-layers,
449
+.leaflet-touch .leaflet-bar {
450
+	border: 2px solid rgba(0,0,0,0.2);
451
+	background-clip: padding-box;
452
+	}
453
+
454
+
455
+/* popup */
456
+
457
+.leaflet-popup {
458
+	position: absolute;
459
+	text-align: center;
460
+	margin-bottom: 20px;
461
+	}
462
+.leaflet-popup-content-wrapper {
463
+	padding: 1px;
464
+	text-align: left;
465
+	border-radius: 12px;
466
+	}
467
+.leaflet-popup-content {
468
+	margin: 13px 19px;
469
+	line-height: 1.4;
470
+	}
471
+.leaflet-popup-content p {
472
+	margin: 18px 0;
473
+	}
474
+.leaflet-popup-tip-container {
475
+	width: 40px;
476
+	height: 20px;
477
+	position: absolute;
478
+	left: 50%;
479
+	margin-left: -20px;
480
+	overflow: hidden;
481
+	pointer-events: none;
482
+	}
483
+.leaflet-popup-tip {
484
+	width: 17px;
485
+	height: 17px;
486
+	padding: 1px;
487
+
488
+	margin: -10px auto 0;
489
+
490
+	-webkit-transform: rotate(45deg);
491
+	   -moz-transform: rotate(45deg);
492
+	    -ms-transform: rotate(45deg);
493
+	     -o-transform: rotate(45deg);
494
+	        transform: rotate(45deg);
495
+	}
496
+.leaflet-popup-content-wrapper,
497
+.leaflet-popup-tip {
498
+	background: white;
499
+	color: #333;
500
+	box-shadow: 0 3px 14px rgba(0,0,0,0.4);
501
+	}
502
+.leaflet-container a.leaflet-popup-close-button {
503
+	position: absolute;
504
+	top: 0;
505
+	right: 0;
506
+	padding: 4px 4px 0 0;
507
+	border: none;
508
+	text-align: center;
509
+	width: 18px;
510
+	height: 14px;
511
+	font: 16px/14px Tahoma, Verdana, sans-serif;
512
+	color: #c3c3c3;
513
+	text-decoration: none;
514
+	font-weight: bold;
515
+	background: transparent;
516
+	}
517
+.leaflet-container a.leaflet-popup-close-button:hover {
518
+	color: #999;
519
+	}
520
+.leaflet-popup-scrolled {
521
+	overflow: auto;
522
+	border-bottom: 1px solid #ddd;
523
+	border-top: 1px solid #ddd;
524
+	}
525
+
526
+.leaflet-oldie .leaflet-popup-content-wrapper {
527
+	zoom: 1;
528
+	}
529
+.leaflet-oldie .leaflet-popup-tip {
530
+	width: 24px;
531
+	margin: 0 auto;
532
+
533
+	-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
534
+	filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
535
+	}
536
+.leaflet-oldie .leaflet-popup-tip-container {
537
+	margin-top: -1px;
538
+	}
539
+
540
+.leaflet-oldie .leaflet-control-zoom,
541
+.leaflet-oldie .leaflet-control-layers,
542
+.leaflet-oldie .leaflet-popup-content-wrapper,
543
+.leaflet-oldie .leaflet-popup-tip {
544
+	border: 1px solid #999;
545
+	}
546
+
547
+
548
+/* div icon */
549
+
550
+.leaflet-div-icon {
551
+	background: #fff;
552
+	border: 1px solid #666;
553
+	}
554
+
555
+
556
+/* Tooltip */
557
+/* Base styles for the element that has a tooltip */
558
+.leaflet-tooltip {
559
+	position: absolute;
560
+	padding: 6px;
561
+	background-color: #fff;
562
+	border: 1px solid #fff;
563
+	border-radius: 3px;
564
+	color: #222;
565
+	white-space: nowrap;
566
+	-webkit-user-select: none;
567
+	-moz-user-select: none;
568
+	-ms-user-select: none;
569
+	user-select: none;
570
+	pointer-events: none;
571
+	box-shadow: 0 1px 3px rgba(0,0,0,0.4);
572
+	}
573
+.leaflet-tooltip.leaflet-clickable {
574
+	cursor: pointer;
575
+	pointer-events: auto;
576
+	}
577
+.leaflet-tooltip-top:before,
578
+.leaflet-tooltip-bottom:before,
579
+.leaflet-tooltip-left:before,
580
+.leaflet-tooltip-right:before {
581
+	position: absolute;
582
+	pointer-events: none;
583
+	border: 6px solid transparent;
584
+	background: transparent;
585
+	content: "";
586
+	}
587
+
588
+/* Directions */
589
+
590
+.leaflet-tooltip-bottom {
591
+	margin-top: 6px;
592
+}
593
+.leaflet-tooltip-top {
594
+	margin-top: -6px;
595
+}
596
+.leaflet-tooltip-bottom:before,
597
+.leaflet-tooltip-top:before {
598
+	left: 50%;
599
+	margin-left: -6px;
600
+	}
601
+.leaflet-tooltip-top:before {
602
+	bottom: 0;
603
+	margin-bottom: -12px;
604
+	border-top-color: #fff;
605
+	}
606
+.leaflet-tooltip-bottom:before {
607
+	top: 0;
608
+	margin-top: -12px;
609
+	margin-left: -6px;
610
+	border-bottom-color: #fff;
611
+	}
612
+.leaflet-tooltip-left {
613
+	margin-left: -6px;
614
+}
615
+.leaflet-tooltip-right {
616
+	margin-left: 6px;
617
+}
618
+.leaflet-tooltip-left:before,
619
+.leaflet-tooltip-right:before {
620
+	top: 50%;
621
+	margin-top: -6px;
622
+	}
623
+.leaflet-tooltip-left:before {
624
+	right: 0;
625
+	margin-right: -12px;
626
+	border-left-color: #fff;
627
+	}
628
+.leaflet-tooltip-right:before {
629
+	left: 0;
630
+	margin-left: -12px;
631
+	border-right-color: #fff;
632
+	}

+ 5
- 0
public/extlib/leaflet/leaflet.js
File diff suppressed because it is too large
View File


+ 33
- 0
public/index.html View File

@@ -0,0 +1,33 @@
1
+<!DOCTYPE html>
2
+<html>
3
+<head>
4
+    <meta charset="utf-8" />
5
+
6
+    <title></title>
7
+    <link rel="stylesheet" href="extlib/leaflet/leaflet.css" />
8
+    <link rel="stylesheet" href="extlib/ionicons/css/ionicons.min.css" />
9
+    <link rel="stylesheet" href="extlib/leaflet.awesome-markers/leaflet.awesome-markers.css" />
10
+    <link rel="stylesheet" href="css/styles.css" />
11
+</head>
12
+<body>
13
+    <div class="container">
14
+        <details class="schedule" open>
15
+            <summary class="schedule__title">
16
+                Next trips
17
+            </summary>
18
+
19
+            <div>
20
+                <ul class="schedule__times">
21
+                </ul>
22
+            </div>
23
+        </details>
24
+
25
+        <div id="map"></div>
26
+    </div>
27
+
28
+    <script src="extlib/leaflet/leaflet.js"></script>
29
+    <script src="extlib/leaflet.awesome-markers/leaflet.awesome-markers.min.js"></script>
30
+    <script src="js/custom.js"></script>
31
+</body>
32
+</html>
33
+

+ 350
- 0
public/js/custom.js View File

@@ -0,0 +1,350 @@
1
+/**
2
+ * Map, live tracking
3
+ */
4
+( function() {
5
+    "use strict";
6
+
7
+    var map, busStopIcon, updateBusMarkers, busStopLocation,
8
+        busMarkers = [], busIcons = [], busStopMarker,
9
+        audio = new Audio( "../ping.ogg" );
10
+
11
+    map = L.map( "map", {
12
+        center: [ 45.2449019, -75.7364839 ],
13
+        zoom: 18,
14
+        zoomControl: false
15
+    } );
16
+
17
+    L.tileLayer(
18
+        "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
19
+        {
20
+            attribution: "&copy; <a href='http://osm.org/copyright'>OpenStreetMap</a> contributors"
21
+        }
22
+    ).addTo( map );
23
+
24
+    L.AwesomeMarkers.Icon.prototype.options.prefix = "ion";
25
+
26
+    /*
27
+     * Bus stop
28
+     */
29
+    busStopIcon = L.AwesomeMarkers.icon( {
30
+        icon: "ios-location",
31
+        markerColor: "red"
32
+    } );
33
+
34
+    busStopLocation = new L.LatLng( 45.24501, -75.73672 );
35
+    busStopMarker = L.marker( [ 45.24501, -75.73672 ], { icon: busStopIcon } ).addTo( map );
36
+
37
+    /*
38
+     * Bus icons
39
+     */
40
+    busIcons.push( L.AwesomeMarkers.icon( {
41
+        icon: "android-bus",
42
+        markerColor: "orange"
43
+    } ) );
44
+
45
+    busIcons.push( L.AwesomeMarkers.icon( {
46
+        icon: "android-bus",
47
+        markerColor: "green"
48
+    } ) );
49
+
50
+    busIcons.push( L.AwesomeMarkers.icon( {
51
+        icon: "android-bus",
52
+        markerColor: "blue"
53
+    } ) );
54
+
55
+    /*
56
+     * Bus markers
57
+     */
58
+    busMarkers = busIcons.map( ( busIcon ) => {
59
+        return L.marker( [ 0, 0 ], { icon: busIcon } );
60
+    } );
61
+
62
+    updateBusMarkers = function() {
63
+        fetch( "/live.php" )
64
+            .then( ( response ) => {
65
+                return response.json();
66
+            } )
67
+            .then( ( coordsArr ) => {
68
+                var activeMarkers = [ busStopMarker ];
69
+
70
+                coordsArr.forEach( ( coords, i ) => {
71
+                    var marker = busMarkers[ i ],
72
+                        busLocation,
73
+                        distance;
74
+
75
+                    if ( coords.latitude && coords.longitude ) {
76
+                        busLocation = new L.LatLng( coords.latitude, coords.longitude );
77
+                        distance = busLocation.distanceTo( busStopLocation );
78
+
79
+                        marker.setLatLng( busLocation );
80
+                        marker.addTo( map );
81
+
82
+                        activeMarkers.push( marker );
83
+
84
+                        // If the bus is between 800 and 500 meters, play
85
+                        // play audio sound. The > 500 condition is a hack
86
+                        // since we can't flag that we've played the audio
87
+                        // for a particular bus at the moment.
88
+                        // TODO: We could flag a specific bus by catching its
89
+                        //       "TripStartTime", I suppose. We'd need our API
90
+                        //       to return that data along with the coords.
91
+                        if ( distance <= 800 && distance > 500 ) {
92
+                            audio.play();
93
+                        }
94
+                    } else {
95
+                        marker.remove();
96
+                    }
97
+                } );
98
+
99
+                map.fitBounds( new L.featureGroup( activeMarkers ).getBounds() );
100
+                setTimeout( updateBusMarkers, 1000 ); // 1s
101
+            } );
102
+    };
103
+
104
+    // Kick-off update marker loop
105
+    updateBusMarkers();
106
+}() );
107
+
108
+/**
109
+ * Schedule
110
+ */
111
+( function() {
112
+    var updateSchedule,
113
+        container = document.querySelector( ".schedule__times" );
114
+
115
+    container.addEventListener( "countdown-expired", ( e ) => {
116
+        var countdownElm = e.target;
117
+
118
+        // Apply "expired" styles
119
+        countdownElm
120
+          .closest( ".schedule__item" )
121
+          .classList.add( "schedule__item--expired" );
122
+
123
+        // Change wording
124
+        countdownElm
125
+          .closest( ".schedule__time-remaining" )
126
+          .textContent = "(passed)";
127
+
128
+        // Remove "countdown" object -- this doesn't affect the DOM
129
+        e.detail.countdown.remove();
130
+    } );
131
+
132
+    updateSchedule = function() {
133
+        var rootDocFragment = document.createRange().createContextualFragment( "" );
134
+
135
+        fetch( "/times.php" )
136
+            .then( ( response ) => {
137
+                return response.json();
138
+            } )
139
+            .then( ( epochs ) => {
140
+                // TODO: error handling
141
+                epochs.forEach( ( epoch, i ) => {
142
+                    // * 1000 because PHP's strtotime returns seconds
143
+                    // and JS needs milliseconds.
144
+                    // NOTE: We're losing precision. May or may not be an issue.
145
+                    var epochMs = parseInt( epoch, 10 ) * 1000,
146
+                        autoPlay = ( i < 3 ),
147
+                        countdown = new Countdown( epochMs, autoPlay ),
148
+                        dateTime = new Date( epochMs ),
149
+                        isoDateTime = dateTime.toISOString(),
150
+                        time, html, docFragment;
151
+
152
+                    // Force Eastern Timezone
153
+                    time = dateTime.toLocaleTimeString(
154
+                        "en-CA",
155
+                        {
156
+                            "timeZone": "America/Toronto",
157
+                            "hour": "2-digit",
158
+                            "minute": "2-digit"
159
+                        }
160
+                    );
161
+
162
+                    html = "<li class='schedule__item'>" +
163
+                        "<time class='schedule__time' datetime='" +
164
+                        isoDateTime + "' title='" + isoDateTime + "'>" + time +
165
+                        "</time><br /> <span class='schedule__time-remaining'>" +
166
+                        "(in about <span class='countdown-container'></span>)" +
167
+                        "</span></li>";
168
+
169
+                    docFragment = document.createRange().createContextualFragment( html );
170
+                    docFragment.querySelector( ".countdown-container" ).appendChild( countdown.elm );
171
+                    rootDocFragment.appendChild( docFragment );
172
+                } );
173
+
174
+                container.appendChild( rootDocFragment );
175
+            } );
176
+    };
177
+
178
+    updateSchedule();
179
+}() );
180
+
181
+( function() {
182
+    var countdowns = {
183
+            "active": [],
184
+            "inactive": []
185
+        },
186
+        interval = null,
187
+         startInterval, stopInterval;
188
+
189
+    startInterval = function() {
190
+        interval = setInterval( () => {
191
+            countdowns.active.forEach( ( countdown ) => {
192
+                countdown.update();
193
+            } );
194
+        }, 1000 );
195
+    };
196
+
197
+    stopInterval = function() {
198
+        clearInterval( interval );
199
+
200
+        interval = null;
201
+    };
202
+
203
+    var Countdown = class Countdown {
204
+        constructor( target, autoPlay ) {
205
+            var elm = document.createElement( "span" ),
206
+                activeCountdowns = countdowns.active,
207
+                shouldAutoPlay = ( autoPlay === undefined || autoPlay === true ); // cast-to-bool
208
+
209
+            elm.classList.add( "countdown" );
210
+
211
+            this.elm = elm;
212
+            this.target = target;
213
+            this.expired = false;
214
+            this.paused = !shouldAutoPlay; // FIXME: Always set to true and let [1] reset, if needed(?)
215
+            this.events = {
216
+                "expired": new CustomEvent(
217
+                    "countdown-expired",
218
+                    {
219
+                        "detail": {
220
+                            "countdown": this
221
+                        },
222
+                        "bubbles": true
223
+                    }
224
+                )
225
+            };
226
+
227
+            // Make sure we have data as soon as possible, otherwise we might
228
+            // have to wait an entire interval cycle before being able to
229
+            // display the countdown
230
+            this.update( true );
231
+
232
+            // Default to inactive collection
233
+            countdowns.inactive.push( this );
234
+
235
+            // If auto-play, start now. This will move the countdown to the
236
+            // 'active' collection and start the interval, if not already
237
+            // running.
238
+            if ( shouldAutoPlay ) {
239
+                this.play();
240
+            }
241
+        }
242
+
243
+        play() {
244
+            var targetCountdown = [];
245
+
246
+            countdowns.inactive.every( ( countdown, i ) => {
247
+                if ( this === countdown ) {
248
+                    targetCountdown = countdowns.inactive.splice( i, 1 );
249
+
250
+                    return false;
251
+                }
252
+            } );
253
+
254
+            if ( targetCountdown.length > 0 ) {
255
+                targetCountdown = targetCountdown[ 0 ];
256
+                targetCountdown.paused = false;
257
+                countdowns.active.push( targetCountdown );
258
+            }
259
+
260
+            // Start the interval once we have our first active countdown
261
+            // Make sure we don't have one that's already running for
262
+            // whatever reason.
263
+            if ( countdowns.active.length === 1 && interval === null ) {
264
+                startInterval();
265
+            }
266
+        }
267
+
268
+        pause() {
269
+            var targetCountdown = [];
270
+
271
+            countdowns.active.every( ( countdown, i ) => {
272
+                if ( this === countdown ) {
273
+                    targetCountdown = countdowns.active.splice( i, 1 );
274
+
275
+                    return false;
276
+                }
277
+            } );
278
+
279
+            if ( targetCountdown.length > 0 ) {
280
+                targetCountdown = targetCountdown[ 0 ];
281
+                targetCountdown.paused = true;
282
+                countdowns.inactive.push( targetCountdown );
283
+            }
284
+
285
+            if ( countdowns.active.length <= 0 ) {
286
+                stopInterval();
287
+            }
288
+        }
289
+
290
+        remove() {
291
+            countdowns.active.every( ( countdown, i ) => {
292
+                if ( this === countdown ) {
293
+                    countdowns.active.splice( i, 1 );
294
+
295
+                    return false;
296
+                }
297
+            } );
298
+
299
+            if ( countdowns.active.length <= 0 ) {
300
+                stopInterval();
301
+            }
302
+        }
303
+
304
+        // FIXME: The isInit param is a bit of a hack to ensure we don't
305
+        //        trigger an "expired" event and remove the countdown from the
306
+        //        'active' collection before the countdown is attached to the
307
+        //        DOM (if the countdown `target` is already in the past when
308
+        //        initializing the countdown.
309
+        //
310
+        // TODO:  We may have to create an init() method that's the same as
311
+        //        update(); except don't do the "expire" stuff? Seems redundant
312
+        update( isInit ) {
313
+            var browserTime = new Date(),
314
+                x = Math.floor( ( this.target - browserTime ) / 1000 ),
315
+                seconds, minutes, hours;
316
+
317
+            if ( !isInit && this.expired === false && x <= 0 ) {
318
+                this.expired = true;
319
+
320
+                this.elm.dispatchEvent( this.events.expired );
321
+            }
322
+
323
+            seconds = x % 60;
324
+
325
+            x = Math.floor( x / 60 );
326
+            minutes = x % 60;
327
+
328
+            x = Math.floor( x / 60 );
329
+            hours = x % 24;
330
+
331
+            if ( hours === 0 ) {
332
+                hours = "";
333
+
334
+                if ( minutes === 0 ) {
335
+                    minutes = "";
336
+                } else {
337
+                    minutes += "m";
338
+                }
339
+            } else {
340
+                hours += "h";
341
+                minutes += "m";
342
+            }
343
+
344
+            this.elm.textContent = hours + minutes + seconds + "s";
345
+        }
346
+    };
347
+
348
+    window.Countdown = Countdown;
349
+}() );
350
+

+ 20
- 0
public/live.php View File

@@ -0,0 +1,20 @@
1
+<?php
2
+
3
+require_once('../classes/Octranspo.php');
4
+
5
+$config = json_decode(file_get_contents('../config.json'));
6
+
7
+// OCTranspo creds
8
+$appId = $config->octranspo->appId;
9
+$apiKey = $config->octranspo->apiKey;
10
+$busNo = $config->octranspo->busNo;
11
+$stopNo = $config->octranspo->stopNo;
12
+
13
+$ocTranspo = new OCTranspo($appId, $apiKey);
14
+
15
+$nextTrips = $ocTranspo->getNextTrips($stopNo, $busNo);
16
+
17
+header('Content-Type: application/json');
18
+
19
+echo $nextTrips;
20
+

BIN
public/ping.ogg View File


+ 18
- 0
public/times.php View File

@@ -0,0 +1,18 @@
1
+<?php
2
+
3
+require_once('../classes/Navitia.php');
4
+
5
+$config = json_decode(file_get_contents('../config.json'));
6
+
7
+// Navitia creds
8
+$token = $config->navitia->token;
9
+$stopId = $config->navitia->stopId;
10
+
11
+$navitia= new Navitia($token);
12
+
13
+$nextTrips = $navitia->getStopSchedule($stopId);
14
+
15
+header('Content-Type: application/json');
16
+
17
+echo $nextTrips;
18
+

Loading…
Cancel
Save