├── .gitignore
├── README.md
├── license-wtfpl.txt
├── mobile_app
├── app.js
├── config.xml
├── icon.png
├── index.html
└── style.css
├── server
├── gpstracks.sql
└── server.js
└── viewer
├── slider.js
├── style.css
├── viewer.html
└── viewer.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Open GPS-tracker
2 | ========
3 |
4 | 
5 |
6 | Open GPS-tracker is a GPS-tracking-thing written in JavaScript. It is primarily built for tracking running events, but may be modified to track anything.
7 |
8 | It utilizes Node.js and WebSockets to communicate between the 'runners', server and viewers. MySQL is used for storage of track data.
9 |
10 | ##Structure
11 |
12 | ###Tracking app
13 |
14 | mobile_app - the tracking app, to be run on a GPS-enabled device. Sends location data on a set interval to the Node.js-server.
15 |
16 | ###Server
17 |
18 | The server recieves the tracking data, sends it to all connected viewers, then stores the tracking data in the database.
19 |
20 | ###Viewer
21 |
22 | The viewer gets data from the server via WebSockets and plots it on a map (Google Maps API).
23 |
24 | ##Instructions
25 |
26 | What you need: Node.js & Socket.io, MySQL, web server.
27 |
28 | ###Installation:
29 | - Edit `server/server.js` with your MySQL-details.
30 | - Create database as per `gpstracks.sql`.
31 | - Edit `mobile_app/app.js` with your socket.io-server.
32 | - Edit `mobile_app/index.html` with your socket.io-server.
33 | - Edit `viewer/viewer.html` with your socket.io-server.
34 | - Edit `viewer/viewer.js` with your socket.io-server. Take a look at line 275 for editing custom tile server.
35 |
36 | ###Tracking:
37 | 1. Start server with `node server.js`.
38 | 2. Send someone for a walk with the mobile_app running.
39 | 1. Browse to `viewer.html` and hopefully you'll see the tracking goodness.
--------------------------------------------------------------------------------
/license-wtfpl.txt:
--------------------------------------------------------------------------------
1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2 | Version 2, December 2004
3 |
4 | Everyone is permitted to copy and distribute verbatim or modified
5 | copies of this license document, and changing it is allowed as long
6 | as the name is changed.
7 |
8 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
9 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
10 |
11 | 0. You just DO WHAT THE FUCK YOU WANT TO.
--------------------------------------------------------------------------------
/mobile_app/app.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 | var socket = io.connect('http://YOUR SERVER:8080');
3 |
4 | var latlon = {};
5 | while (!latlon.id) {
6 | latlon.id = prompt("Please enter your ID:", "");
7 | }
8 | var first = true;
9 | var intervalId;
10 | var watchId;
11 |
12 | socket.emit('runnerConnect', latlon.id+" connected as runner");
13 |
14 | function startTrack() {
15 | if(navigator.geolocation) {
16 | console.log('trying to find a fix');
17 | watchId = navigator.geolocation.watchPosition(geo_success, errorHandler,
18 | {enableHighAccuracy:true, maximumAge:30000, timeout:27000});
19 | }
20 | else{
21 | alert("Sorry, device does not support geolocation! Update your browser.");
22 | }
23 | }
24 |
25 | function geo_success(position) {
26 | $("#status p").text("Tracking active");
27 | $('#status').removeClass("stopped").addClass("active");
28 | $('button').text("Stop tracking");
29 |
30 | latlon.lat = position.coords.latitude;
31 | latlon.lon = position.coords.longitude;
32 | if(!position.coords.speed) { latlon.speed = 0; }
33 | else{ latlon.speed = position.coords.speed }
34 |
35 | if(first) {
36 | intervalId = setInterval(send, 5000);
37 | }
38 | first = false;
39 | }
40 |
41 | function addTime() {
42 | // insert time in formData-object
43 | var d = new Date();
44 | var d_utc = ISODateString(d);
45 | latlon.time = d_utc;
46 |
47 | // date to ISO 8601,
48 | // developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date#Example.3a_ISO_8601_formatted_dates
49 | function ISODateString(d) {
50 | function pad(n) {return n<10 ? '0'+n : n}
51 | return d.getUTCFullYear()+'-'
52 | + pad(d.getUTCMonth()+1)+'-'
53 | + pad(d.getUTCDate())+'T'
54 | + pad(d.getUTCHours())+':'
55 | + pad(d.getUTCMinutes())+':'
56 | + pad(d.getUTCSeconds())+'Z'
57 | }
58 | }
59 |
60 | function send() {
61 | addTime();
62 | socket.emit('sendevent', latlon);
63 | }
64 |
65 | function toggleTimer() {
66 | if(intervalId) {
67 | console.log('stopping');
68 | clearInterval(intervalId);
69 | intervalId = null;
70 | navigator.geolocation.clearWatch(watchId);
71 | $("#status p").text("Not tracking");
72 | $('#status').removeClass("active").addClass("stopped");
73 | $('button').text("Start tracking");
74 | first = true;
75 | }
76 | else{
77 | console.log('starting');
78 | startTrack();
79 | }
80 | }
81 |
82 | function errorHandler(err) {
83 | if(err.code == 1) {
84 | alert("Error: Access was denied");
85 | }
86 | else if(err.code == 2) {
87 | alert("Error: Position is unavailable");
88 | }
89 | }
90 |
91 | $('#startstop').on("click", toggleTimer);
92 | });
--------------------------------------------------------------------------------
/mobile_app/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | GPS tracker
8 |
9 |
10 | A GPS-tracker for your enjoyment.
11 |
12 |
13 |
15 | Henrik Johansson
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/mobile_app/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nipuna-g/Train-Tracker-GPS/5d4e850d7fc01e7a13c5086f90c2b85c094d5ac3/mobile_app/icon.png
--------------------------------------------------------------------------------
/mobile_app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Open GPS-tracker
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/mobile_app/style.css:
--------------------------------------------------------------------------------
1 | *{
2 | box-sizing: border-box;
3 | }
4 |
5 | html, body{
6 | height: 100%;
7 | width: 100%;
8 | margin: 0;
9 | padding: 0;
10 | }
11 |
12 | body{
13 | display: table;
14 | }
15 |
16 | #wrap{
17 | height: 100%;
18 | width: 100%;
19 | font-family: sans-serif;
20 | padding: 0;
21 | margin: 0;
22 | background: #020202; /* Old browsers */
23 | background: -moz-linear-gradient(top, #020202 0%, #272D33 100%); /* FF3.6+ */
24 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#020202), color-stop(100%,#272D33)); /* Chrome,Safari4+ */
25 | background: -webkit-linear-gradient(top, #020202 0%,#272D33 100%); /* Chrome10+,Safari5.1+ */
26 | background: -o-linear-gradient(top, #020202 0%,#272D33 100%); /* Opera 11.10+ */
27 | background: -ms-linear-gradient(top, #020202 0%,#272D33 100%); /* IE10+ */
28 | background: linear-gradient(top, #020202 0%,#272D33 100%); /* W3C */
29 | display: table-cell;
30 | vertical-align: middle;
31 | text-align: center;
32 | }
33 |
34 | p{
35 | margin:0;
36 | }
37 |
38 | #status{
39 | font-size: 1.5em;
40 | padding: 0;
41 | width: 100%;
42 | position: fixed;
43 | top: 0;
44 | text-align: center;
45 | -webkit-transition: .7s ease;
46 | -moz-transition: .7s ease;
47 | -o-transition: .7s ease;
48 | -ms-transition: .7s ease;
49 | transition: .7s ease;
50 | }
51 |
52 | #status p{
53 | padding: 1em 0;
54 | color: #F3F3F3;
55 | background: #7d7e7d; /* Old browsers */
56 | background: -moz-linear-gradient(top, transparent, #020202 100%); /* FF3.6+ */
57 | background: -webkit-linear-gradient(top, transparent,#020202 100%); /* Chrome10+,Safari5.1+ */
58 | background: -o-linear-gradient(top, transparent,#020202 100%); /* Opera 11.10+ */
59 | background: -ms-linear-gradient(top, transparent,#020202 100%); /* IE10+ */
60 | background: linear-gradient(top, transparent,#020202 100%); /* W3C */
61 |
62 | }
63 |
64 | .active{
65 | background: green;
66 | }
67 |
68 | button{
69 | font-family: sans-serif;
70 | font-size: 1.25em;
71 | background: transparent;
72 | cursor: pointer;
73 | color: #F3F3F3;
74 | border-right: none;
75 | border-left: none;
76 | -webkit-border-radius: 0px;
77 | -moz-border-radius: 0px;
78 | border-radius: 0px;
79 | width: 100%;
80 | height: 3em;
81 | border: none;
82 | border-top: 1px solid darkgray;
83 | border-bottom: 1px solid darkgray;
84 | }
85 |
86 | button:last-child{
87 | margin: 0 auto;
88 | }
--------------------------------------------------------------------------------
/server/gpstracks.sql:
--------------------------------------------------------------------------------
1 | SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
2 | SET time_zone = "+00:00";
3 |
4 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
5 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
6 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
7 | /*!40101 SET NAMES utf8 */;
8 |
9 |
10 | CREATE TABLE IF NOT EXISTS `tracks` (
11 | `pointid` int(11) NOT NULL AUTO_INCREMENT,
12 | `runnerid` varchar(255) NOT NULL,
13 | `lat` decimal(7,5) NOT NULL,
14 | `lon` decimal(8,5) NOT NULL,
15 | `time` varchar(255) NOT NULL,
16 | `speed` decimal(4,2) NOT NULL,
17 | PRIMARY KEY (`pointid`)
18 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=802 ;
19 |
20 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
21 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
22 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
23 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | var io = require('socket.io').listen(8080);
2 | var mysql = require('mysql');
3 | var connection = mysql.createConnection({
4 | host : 'localhost',
5 | user : 'user',
6 | password : 'pass',
7 | database : 'gpstracks'
8 | });
9 |
10 | connection.connect();
11 |
12 | io.sockets.on('connection', function (socket) {
13 |
14 | var socketRef = socket;
15 |
16 | socket.on('client', function (){
17 | // Client connected - send all tracking data
18 | socketRef.join("clients");
19 | var out = "";
20 | connection.query("SELECT runnerid,GROUP_CONCAT(lat ORDER BY pointid DESC SEPARATOR ',') AS lat,GROUP_CONCAT(lon ORDER BY pointid DESC SEPARATOR ',') AS lon,GROUP_CONCAT(speed ORDER BY pointid DESC SEPARATOR ',') AS speed FROM tracks GROUP BY runnerid ORDER BY runnerid",
21 | function(err, rows, fields) {
22 | if (err) throw err;
23 | var outObj = {};
24 | outObj.runners = [];
25 | for (var i = 0; i < rows.length; i++) {
26 | outObj.runners.push(rows[i]);
27 | }
28 | out = JSON.stringify(outObj);
29 | socketRef.emit('allData', out);
30 | console.log('sent init data');
31 | });
32 | });
33 |
34 | socket.on('sendevent', function (data){
35 | // Data recieved from runner
36 |
37 | // emit data to clients
38 | io.sockets.in('clients').emit('sendfromserver', data);
39 |
40 | // save to database
41 | connection.query('INSERT INTO tracks SET runnerid = '+connection.escape(data.id)+', lat = '+connection.escape(data.lat)+', lon = '+connection.escape(data.lon)+', speed = '+connection.escape(data.speed)+', time = '+connection.escape(data.time)+'',
42 | function(err, rows, fields) {
43 | if (err) throw err;
44 | });
45 | });
46 |
47 | socket.on('runnerConnect', function (data){
48 | console.log(data);
49 | });
50 | });
51 |
52 | process.on('SIGINT', function() {
53 | console.log( "\nGracefully shutting down from SIGINT (Ctrl-C)");
54 | connection.end();
55 | process.exit();
56 | })
57 |
--------------------------------------------------------------------------------
/viewer/slider.js:
--------------------------------------------------------------------------------
1 | /*
2 | html5slider - a JS implementation of for Firefox 16 and up
3 | https://github.com/fryn/html5slider
4 |
5 | Copyright (c) 2010-2012 Frank Yan,
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in
15 | all copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | THE SOFTWARE.
24 | */
25 |
26 | (function() {
27 |
28 | // test for native support
29 | var test = document.createElement('input');
30 | try {
31 | test.type = 'range';
32 | if (test.type == 'range')
33 | return;
34 | } catch (e) {
35 | return;
36 | }
37 |
38 | // test for required property support
39 | test.style.background = 'linear-gradient(red, red)';
40 | if (!test.style.backgroundImage || !('MozAppearance' in test.style) ||
41 | !document.mozSetImageElement || !this.MutationObserver)
42 | return;
43 |
44 | var scale;
45 | var isMac = navigator.platform == 'MacIntel';
46 | var thumb = {
47 | radius: isMac ? 9 : 6,
48 | width: isMac ? 22 : 12,
49 | height: isMac ? 16 : 20
50 | };
51 | var track = 'linear-gradient(transparent ' + (isMac ?
52 | '6px, #999 6px, #999 7px, #ccc 8px, #bbb 9px, #bbb 10px, transparent 10px' :
53 | '9px, #999 9px, #bbb 10px, #fff 11px, transparent 11px') +
54 | ', transparent)';
55 | var styles = {
56 | 'min-width': thumb.width + 'px',
57 | 'min-height': thumb.height + 'px',
58 | 'max-height': thumb.height + 'px',
59 | padding: '0 0 ' + (isMac ? '2px' : '1px'),
60 | border: 0,
61 | 'border-radius': 0,
62 | cursor: 'default',
63 | 'text-indent': '-999999px' // -moz-user-select: none; breaks mouse capture
64 | };
65 | var options = {
66 | attributes: true,
67 | attributeFilter: ['min', 'max', 'step', 'value']
68 | };
69 | var forEach = Array.prototype.forEach;
70 | var onChange = document.createEvent('HTMLEvents');
71 | onChange.initEvent('change', true, false);
72 |
73 | if (document.readyState == 'loading')
74 | document.addEventListener('DOMContentLoaded', initialize, true);
75 | else
76 | initialize();
77 |
78 | function initialize() {
79 | // create initial sliders
80 | forEach.call(document.querySelectorAll('input[type=range]'), transform);
81 | // create sliders on-the-fly
82 | new MutationObserver(function(mutations) {
83 | mutations.forEach(function(mutation) {
84 | if (mutation.addedNodes)
85 | forEach.call(mutation.addedNodes, function(node) {
86 | check(node);
87 | if (node.childElementCount)
88 | forEach.call(node.querySelectorAll('input'), check);
89 | });
90 | });
91 | }).observe(document, { childList: true, subtree: true });
92 | }
93 |
94 | function check(input) {
95 | if (input.localName == 'input' && input.type != 'range' &&
96 | input.getAttribute('type') == 'range')
97 | transform(input);
98 | }
99 |
100 | function transform(slider) {
101 |
102 | var isValueSet, areAttrsSet, isChanged, isClick, prevValue, rawValue, prevX;
103 | var min, max, step, range, value = slider.value;
104 |
105 | // lazily create shared slider affordance
106 | if (!scale) {
107 | scale = document.body.appendChild(document.createElement('hr'));
108 | style(scale, {
109 | '-moz-appearance': isMac ? 'scale-horizontal' : 'scalethumb-horizontal',
110 | display: 'block',
111 | visibility: 'visible',
112 | opacity: 1,
113 | position: 'fixed',
114 | top: '-999999px'
115 | });
116 | document.mozSetImageElement('__sliderthumb__', scale);
117 | }
118 |
119 | // reimplement value and type properties
120 | var getValue = function() { return '' + value; };
121 | var setValue = function setValue(val) {
122 | value = '' + val;
123 | isValueSet = true;
124 | draw();
125 | delete slider.value;
126 | slider.value = value;
127 | slider.__defineGetter__('value', getValue);
128 | slider.__defineSetter__('value', setValue);
129 | };
130 | slider.__defineGetter__('value', getValue);
131 | slider.__defineSetter__('value', setValue);
132 | slider.__defineGetter__('type', function() { return 'range'; });
133 |
134 | // sync properties with attributes
135 | ['min', 'max', 'step'].forEach(function(prop) {
136 | if (slider.hasAttribute(prop))
137 | areAttrsSet = true;
138 | slider.__defineGetter__(prop, function() {
139 | return this.hasAttribute(prop) ? this.getAttribute(prop) : '';
140 | });
141 | slider.__defineSetter__(prop, function(val) {
142 | val === null ? this.removeAttribute(prop) : this.setAttribute(prop, val);
143 | });
144 | });
145 |
146 | // initialize slider
147 | slider.readOnly = true;
148 | style(slider, styles);
149 | update();
150 |
151 | new MutationObserver(function(mutations) {
152 | mutations.forEach(function(mutation) {
153 | if (mutation.attributeName != 'value') {
154 | update();
155 | areAttrsSet = true;
156 | }
157 | // note that value attribute only sets initial value
158 | else if (!isValueSet) {
159 | value = slider.getAttribute('value');
160 | draw();
161 | }
162 | });
163 | }).observe(slider, options);
164 |
165 | slider.addEventListener('mousedown', onDragStart, true);
166 | slider.addEventListener('keydown', onKeyDown, true);
167 | slider.addEventListener('focus', onFocus, true);
168 | slider.addEventListener('blur', onBlur, true);
169 |
170 | function onDragStart(e) {
171 | isClick = true;
172 | setTimeout(function() { isClick = false; }, 0);
173 | if (e.button || !range)
174 | return;
175 | var width = parseFloat(getComputedStyle(this, 0).width);
176 | var multiplier = (width - thumb.width) / range;
177 | if (!multiplier)
178 | return;
179 | // distance between click and center of thumb
180 | var dev = e.clientX - this.getBoundingClientRect().left - thumb.width / 2 -
181 | (value - min) * multiplier;
182 | // if click was not on thumb, move thumb to click location
183 | if (Math.abs(dev) > thumb.radius) {
184 | isChanged = true;
185 | this.value -= -dev / multiplier;
186 | }
187 | rawValue = value;
188 | prevX = e.clientX;
189 | this.addEventListener('mousemove', onDrag, true);
190 | this.addEventListener('mouseup', onDragEnd, true);
191 | }
192 |
193 | function onDrag(e) {
194 | var width = parseFloat(getComputedStyle(this, 0).width);
195 | var multiplier = (width - thumb.width) / range;
196 | if (!multiplier)
197 | return;
198 | rawValue += (e.clientX - prevX) / multiplier;
199 | prevX = e.clientX;
200 | isChanged = true;
201 | this.value = rawValue;
202 | }
203 |
204 | function onDragEnd() {
205 | this.removeEventListener('mousemove', onDrag, true);
206 | this.removeEventListener('mouseup', onDragEnd, true);
207 | }
208 |
209 | function onKeyDown(e) {
210 | if (e.keyCode > 36 && e.keyCode < 41) { // 37-40: left, up, right, down
211 | onFocus.call(this);
212 | isChanged = true;
213 | this.value = value + (e.keyCode == 38 || e.keyCode == 39 ? step : -step);
214 | }
215 | }
216 |
217 | function onFocus() {
218 | if (!isClick)
219 | this.style.boxShadow = !isMac ? '0 0 0 2px #fb0' :
220 | 'inset 0 0 20px rgba(0,127,255,.1), 0 0 1px rgba(0,127,255,.4)';
221 | }
222 |
223 | function onBlur() {
224 | this.style.boxShadow = '';
225 | }
226 |
227 | // determines whether value is valid number in attribute form
228 | function isAttrNum(value) {
229 | return !isNaN(value) && +value == parseFloat(value);
230 | }
231 |
232 | // validates min, max, and step attributes and redraws
233 | function update() {
234 | min = isAttrNum(slider.min) ? +slider.min : 0;
235 | max = isAttrNum(slider.max) ? +slider.max : 100;
236 | if (max < min)
237 | max = min > 100 ? min : 100;
238 | step = isAttrNum(slider.step) && slider.step > 0 ? +slider.step : 1;
239 | range = max - min;
240 | draw(true);
241 | }
242 |
243 | // recalculates value property
244 | function calc() {
245 | if (!isValueSet && !areAttrsSet)
246 | value = slider.getAttribute('value');
247 | if (!isAttrNum(value))
248 | value = (min + max) / 2;;
249 | // snap to step intervals (WebKit sometimes does not - bug?)
250 | value = Math.round((value - min) / step) * step + min;
251 | if (value < min)
252 | value = min;
253 | else if (value > max)
254 | value = min + ~~(range / step) * step;
255 | }
256 |
257 | // renders slider using CSS background ;)
258 | function draw(attrsModified) {
259 | calc();
260 | if (isChanged && value != prevValue)
261 | slider.dispatchEvent(onChange);
262 | isChanged = false;
263 | if (!attrsModified && value == prevValue)
264 | return;
265 | prevValue = value;
266 | var position = range ? (value - min) / range * 100 : 0;
267 | var bg = '-moz-element(#__sliderthumb__) ' + position + '% no-repeat, ';
268 | style(slider, { background: bg + track });
269 | }
270 |
271 | }
272 |
273 | function style(element, styles) {
274 | for (var prop in styles)
275 | element.style.setProperty(prop, styles[prop], 'important');
276 | }
277 |
278 | })();
279 |
280 |
--------------------------------------------------------------------------------
/viewer/style.css:
--------------------------------------------------------------------------------
1 | *{
2 | -moz-box-sizing: border-box;
3 | box-sizing: border-box;
4 | }
5 |
6 | html { height: 100% }
7 |
8 | body { height: 100%; margin: 0; padding: 0 }
9 |
10 | #map_canvas { width:100%; height:100%; }
11 |
12 | body{
13 | font-family: sans-serif;
14 | font-size: 14px;
15 | }
16 |
17 | #settings{
18 | position: fixed;
19 | width: 144px;
20 | background: white;
21 | top: 29px;
22 | right: 5px;
23 | padding: 6px;
24 | color: #444;
25 | border: 1px solid #717B87;
26 | -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px;
27 | box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px;
28 | }
29 |
30 | #runnerlist{
31 | border-bottom: 1px solid lightgray;
32 | padding-bottom: .5em;
33 | margin-bottom: .5em;
34 | }
35 |
36 | #runnerlist{
37 | width: 100%;
38 | border-spacing: 0;
39 | }
40 |
41 | #runnerlist th, [for=replayspeed]{
42 | font-size: .8em;
43 | font-weight: bold;
44 | }
45 |
46 | #runnerlist th{
47 | text-align: left;
48 | }
49 |
50 | #runnerlist th:first-child{
51 | padding-left: 37px;
52 | }
53 |
54 | #runnerlist th:last-child{
55 | text-align: right;
56 | }
57 |
58 | .replay{
59 | font-weight: bold;
60 | cursor: pointer;
61 | display: inline-block;
62 | width: 16px;
63 | height: 16px;
64 | text-align: center;
65 | border: 1px solid #777777;
66 | margin-right: 4px;
67 | line-height: 15px;
68 | }
69 |
70 | .runnercolor{
71 | padding: 0;
72 | margin: 0;
73 | border: none;
74 | width: 20px;
75 | float: left;
76 | height: 24px;
77 | margin-top: -4px;
78 | margin-right: -3px;
79 | margin-left: -1px;
80 | cursor: pointer;
81 | }
82 |
83 | .pace{
84 | text-align: right;
85 | font-family: 'source code pro', monospace;
86 | }
87 |
88 | #settings p{
89 | padding-bottom: 1em;
90 | border-bottom: 1px solid #E9E9E9;
91 | }
92 |
93 | input[type="range"]{
94 | width: 100%;
95 | }
96 |
97 | .stopped{
98 | background: rgba(0, 255, 0, 0.5);
99 | }
100 |
101 | .started{
102 | background: rgba(255, 0, 0, 0.5);
103 | }
--------------------------------------------------------------------------------
/viewer/viewer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Open GPS-tracker map view
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/viewer/viewer.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function () {
2 |
3 | var updateInterval = 5*1000,
4 | replaySpeed = 100,
5 | tailLength = (60/(updateInterval/1000)),
6 | map,
7 | runnerCount = 0,
8 | runners = [],
9 | socketServer = 'http://YOR SOCKET SERVER HERE:8080';
10 |
11 | function runner(json,id) {
12 | var data = [],
13 | path = new google.maps.MVCArray(),
14 | poly,
15 | polySymbol,
16 | colors = ["#FF0000", "#FF69B4", "#00FF7F", "#FF00FF", "#FFA500", "#00FF00",
17 | "#FA8072", "#00FFFF", "#ADFF2F", "#00FF7F"],
18 | runnerColor = colors[Math.floor(Math.random()*colors.length)],
19 | popInterval,
20 | isPlaying = false,
21 | lastUpdate = Date.now(),
22 | lastPoint,
23 | r = {};
24 |
25 | r.id = json.runners[id].runnerid;
26 |
27 | $('[data-id="'+id+'"] .runnercolor').val(runnerColor);
28 |
29 | if(!data.length){ populate(); }
30 |
31 | function populate(){
32 | var lats = json.runners[id].lat.split(",");
33 | var lons = json.runners[id].lon.split(",");
34 | var speeds = json.runners[id].speed.split(",");
35 |
36 | for (var i = lons.length - 1; i >= 0; i--) {
37 | var p = new google.maps.LatLng(lats[i], lons[i]);
38 | p.speed = speeds[i];
39 | data.push(p);
40 | }
41 | map.setCenter(data[data.length-1]);
42 | makePath(tailLength*2,true);
43 | }
44 |
45 | function makePath(length,poly){
46 | path.clear();
47 | console.log('makin path, length: '+length);
48 |
49 | // prevents trying to access out of range points
50 | if(data.length < length){
51 | length = data.length-1;
52 | }
53 |
54 | for (var i=0; i 2){
92 | path.removeAt(0);
93 | }
94 | }
95 |
96 | function conditionalPop() {
97 | // this is exectued every 2.5 sek. It looks at the time elapsed between now and the last data reciept.
98 | // If that time is higher than the updateInterval, the runner has stopped sending or hasn't moved,
99 | // see r.update. OR, if the tail length is longer than allowed, it's popped.
100 |
101 | var now = Date.now();
102 | var diff = now-lastUpdate;
103 | if (diff > updateInterval || path.getLength() > tailLength*2){
104 | if(!isPlaying){
105 | pop();
106 | }
107 | }
108 | else{
109 | console.log('nopop');
110 | }
111 | }
112 |
113 | popInterval = setInterval(conditionalPop, (updateInterval/2));
114 |
115 | r.pop = function() {
116 | pop();
117 | };
118 |
119 | r.update = function(json) {
120 | var newLat = json.lat;
121 | var newLon = json.lon;
122 | var newSpeed = json.speed;
123 | var p = new google.maps.LatLng(newLat, newLon);
124 |
125 | if(lastPoint !== p.toString()) {
126 | // movement found, add new point to data[] and MVCArray
127 | displayPace(newSpeed);
128 | p.speed = newSpeed;
129 | data.push(p);
130 | if(!isPlaying) {
131 | path.push(interpolate(p, data[data.length-2 ]));
132 | setTimeout(function() {
133 | if(!isPlaying){
134 | path.push(p);
135 | }
136 | }, (updateInterval)/2);
137 | }
138 | lastUpdate = Date.now();
139 | // console.log("movement detected");
140 | }else{
141 | data.push(p);
142 | // console.log('no movement');
143 | }
144 | lastPoint = p.toString();
145 | };
146 |
147 | r.replay = function() {
148 | if(!isPlaying) {
149 | var currLen = parseInt((path.getLength()/2)+1);
150 | path.clear();
151 | path.push(data[0]);
152 | path.push(interpolate(data[1], data[0]));
153 | var i = 1;
154 | isPlaying = true;
155 | replayPoint();
156 | }
157 |
158 | function replayPoint() {
159 | if(i === data.length-1) {
160 | path.push(data[i]);
161 | displayPace(data[i].speed);
162 | console.log('breaking');
163 | makePath(currLen,false);
164 | isPlaying = false;
165 | }
166 | else {
167 | path.push(data[i]);
168 | setTimeout(function() { path.push(interpolate(data[i], data[i-1])); }, replaySpeed/2);
169 | displayPace(data[i].speed);
170 | i++;
171 | setTimeout(function() { requestAnimationFrame(replayPoint); }, replaySpeed);
172 | }
173 | }
174 | };
175 |
176 | r.setColor = function(color){
177 | poly.setOptions({strokeColor:color});
178 | polySymbol.fillColor = color;
179 | runnerColor = color;
180 | };
181 |
182 | return r;
183 | }
184 |
185 | function makeRunners(json){
186 | console.log('makerunners');
187 | if(json.runners.length === 0){
188 | alert('No runners recieved');
189 | toggleTimer();
190 | }
191 |
192 | for (var i=0; i↻'+json.runners[i].runnerid+' | '+getPace(lastspeed[0])+' | ');
195 | runners.push(runner(json,i));
196 | runnerCount = i;
197 | }
198 | }
199 |
200 | function updateRunners(data){
201 | function currentRunner(data){
202 | for (var i = runners.length - 1; i >= 0; i--) {
203 | if(runners[i].id === data.id) {
204 | return i;
205 | }
206 | }
207 | }
208 |
209 | runners[currentRunner(data)].update(data);
210 |
211 | }
212 |
213 | function setSpeed(speed){
214 | replaySpeed = speed;
215 | $("[for=replayspeed]").html('Replay time: '+replaySpeed);
216 | }
217 |
218 | function interpolate(fresh,old) {
219 | var intLat = (old.lat()+fresh.lat())/2;
220 | var intLon = (old.lng()+fresh.lng())/2;
221 | return new google.maps.LatLng(intLat, intLon);
222 | }
223 |
224 | function getPace(pace) {
225 | if (pace === 0 || isNaN(pace)) {
226 | return "N/A";
227 | }
228 | else {
229 | var onemin = pace * 60;
230 | var x = 1000 / onemin;
231 | var time = 60 * x;
232 | var minutes = Math.floor(time / 60);
233 | var seconds = ((Math.round(time - minutes * 60)).toString());
234 | if (seconds.length === 1) { seconds = "0" + seconds; }
235 | return minutes + ":" + seconds;
236 | }
237 | }
238 |
239 | // Socket.io stuff
240 |
241 | var socket = io.connect(socketServer);
242 |
243 | socket.emit('client');
244 |
245 | socket.on('sendfromserver', function (data) {
246 | updateRunners(data);
247 | });
248 |
249 | socket.on('allData', function (json) {
250 | makeRunners(JSON.parse(json));
251 | });
252 |
253 | // Map stuff
254 | var mapOptions = {
255 | center: new google.maps.LatLng(63.845224,20.073608),
256 | zoom: 14,
257 | mapTypeId: google.maps.MapTypeId.SATELLITE,
258 | streetViewControl: false,
259 | panControl: false
260 | };
261 |
262 | var osm = new google.maps.ImageMapType({
263 | getTileUrl: function(coord, zoom) {
264 | return "http://tile.openstreetmap.org/" + zoom + "/" + coord.x + "/" + coord.y + ".png";
265 | },
266 | tileSize: new google.maps.Size(256, 256),
267 | isPng: true,
268 | maxZoom: 18,
269 | name: "OSM",
270 | alt: "OpenStreetMap"
271 | });
272 |
273 | var OLmaps = new google.maps.ImageMapType({
274 | getTileUrl: function(coord, zoom) {
275 | return "http://YOUR CUSTOM TILE SERVER URL" + zoom + "/" + coord.x + "/" + coord.y + ".png";
276 | },
277 | tileSize: new google.maps.Size(256, 256),
278 | isPng: true,
279 | maxZoom: 16,
280 | name: "OL maps",
281 | alt: "OL maps"
282 | });
283 |
284 | function toggleMap() {
285 | if(map.overlayMapTypes.getAt(0)){
286 | map.overlayMapTypes.removeAt(0);
287 | }
288 | else{
289 | map.overlayMapTypes.insertAt(0, OLmaps);
290 | }
291 | }
292 |
293 | map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions);
294 |
295 | map.mapTypes.set('osm', osm);
296 | map.setOptions({
297 | mapTypeControlOptions: {
298 | mapTypeIds:
299 | ['osm',
300 | google.maps.MapTypeId.ROADMAP,
301 | // google.maps.MapTypeId.TERRAIN,
302 | google.maps.MapTypeId.SATELLITE],
303 | // google.maps.MapTypeId.HYBRID],
304 | style: google.maps.MapTypeControlStyle.HORIZONTAL_BAR
305 | }
306 | });
307 |
308 | // Event handlers
309 | $("[for=replayspeed]").text('Replay time: '+replaySpeed);
310 | $('#replayspeed').on('change', function(){ setSpeed($(this).val()); });
311 | $('#togglemap').on('change', toggleMap);
312 | $('#runnerlist').on('change', '.runnercolor', function(event){
313 | var id = $(this).closest('tr').attr('data-id');
314 | var newColor = $('[data-id="'+id+'"] .runnercolor').val();
315 | runners[id].setColor(newColor);
316 | });
317 | $('#runnerlist').on('click', '.replay', function(event){
318 | var id = $(this).closest('tr').attr('data-id');
319 | runners[id].replay();
320 | });
321 |
322 | // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
323 | // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
324 | // requestAnimationFrame polyfill by Erik Möller
325 | // fixes from Paul Irish and Tino Zijdel
326 | (function() {
327 | var lastTime = 0;
328 | var vendors = ['ms', 'moz', 'webkit', 'o'];
329 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
330 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
331 | window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
332 | || window[vendors[x]+'CancelRequestAnimationFrame'];
333 | }
334 |
335 | if (!window.requestAnimationFrame)
336 | window.requestAnimationFrame = function(callback, element) {
337 | var currTime = new Date().getTime();
338 | var timeToCall = Math.max(0, 16 - (currTime - lastTime));
339 | var id = window.setTimeout(function() { callback(currTime + timeToCall); },
340 | timeToCall);
341 | lastTime = currTime + timeToCall;
342 | return id;
343 | };
344 |
345 | if (!window.cancelAnimationFrame)
346 | window.cancelAnimationFrame = function(id) {
347 | clearTimeout(id);
348 | };
349 | }());
350 | });
--------------------------------------------------------------------------------