├── .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 | ![Open GPS-tracker screenshot](http://i.imgur.com/nFvnm0F.png) 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 |

Not tracking

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 |
22 | 23 | 24 |
namemin/km
25 |
26 | 27 | 28 |
29 |
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 | }); --------------------------------------------------------------------------------