class GPS { parse(data) { this.data = data; this.index = 0; this.maxTimeDiff = 3600 * 6; this.maxDist = 3000; this.tracks = []; let t; while(t = gps.next()) { if (t.length > 1) this.tracks.push(new Track(t)); } this.regions = this.getRegions(); } next() { if(this.index >= this.data.length) return; let points = []; while(this.index < this.data.length - 1) { const cur = this.data[this.index]; const nxt = this.data[this.index+1]; cur.timeDiff = Date.parse(nxt.timestamp) / 1000 - Date.parse(cur.timestamp) / 1000; cur.distance = distance(cur, nxt); if(isNaN(cur.distance)) cur.distance = 0; if(cur.speed <= 0) { cur.speed = cur.distance / cur.timeDiff * 3.6; // km/h } if(cur.timeDiff > this.maxTimeDiff || cur.distance > this.maxDist) { cur.distance = null; cur.timeDiff = null; cur.speed = null; break; } points.push(cur); this.index++; } if(this.index == this.data.length - 1) { this.data[this.index].distance = 0; this.data[this.index].timeDiff = 0; this.data[this.index].speed = 0; } points.push(this.data[this.index]); this.index++; return points; } getRegions() { var trips = []; var s = 0; for(let i=0; i<=this.tracks.length; i++) { while(i < this.tracks.length - 2) { const cur = this.tracks[i].info; const nxt = this.tracks[i+1].info; if((nxt.startTime - cur.endTime) > 3600 * 24 * 7 || distance(cur.center, nxt.center) > 100000) { break; } i++; } let trip = this.tracks.slice(s, i); let dist = trip.reduce((sum, p) => p.info.distance + sum, 0); if (dist > 30000) { trips.push({ lat: median(trip.map(v => v.info.center.lat)), lng: median(trip.map(v => v.info.center.lng)), timestamp: median(trip.map(v => v.info.startTime)), distance: dist }); } s = i; } return trips; } getInfo() { let i = { points: this.data.length, distance: this.tracks.reduce((sum, p) => p.info.distance + sum, 0), topSpeed: this.tracks.reduce((max, p) => p.info.topSpeed > max ? p.info.topSpeed : max, 0), ascend: this.tracks.reduce((sum, p) => p.info.ascend + sum, 0), descend: this.tracks.reduce((sum, p) => p.info.descend + sum, 0), movTime: this.tracks.reduce((sum, p) => p.info.movTime + sum, 0), totalTime: this.tracks.reduce((sum, p) => p.info.totalTime + sum, 0) }; i.avgSpeed = i.distance / i.movTime * 3.6; return i; } } let trackId = 0; class Track { constructor(data) { this.data = data; this.id = trackId++; this.deleted = this.clean(); this.info = this.getInfo(); } clean() { let last = this.data[this.data.length-2]; let speed = last.speed, alt = last.alt; let res = []; for(let i=this.data.length-2; i>=0; i--) { let cur = this.data[i]; let lst = this.data[i+1]; if(Math.abs(cur.speed - lst.speed) / cur.timeDiff > 10 || Math.abs(cur.speed - speed) > 50) { res.push(this.data.splice(i, 1)[0]); } else speed = speed * 0.9 + cur.speed * 0.1; alt = alt * 0.9 + cur.alt * 0.1; } return res; } getInfo() { let i = { id: this.id, points: this.data.length, color: getRandomColor(this.data[0].timestamp), distance: this.data.reduce((sum, p) => p.distance + sum, 0), topSpeed: this.data.reduce((max, p) => p.speed > max ? p.speed : max, this.data[0].speed), ascend: 0, descend: 0, startTime: Date.parse(this.data[0].timestamp) / 1000, endTime: Date.parse(this.data[this.data.length-1].timestamp) / 1000, movTime: this.data.reduce((sum, p) => p.speed > 2 ? p.timeDiff + sum : sum, 0), minLat: this.data.reduce((min, p) => p.lat < min ? p.lat : min, this.data[0].lat), maxLat: this.data.reduce((max, p) => p.lat > max ? p.lat : max, this.data[0].lat), minLng: this.data.reduce((min, p) => p.lng < min ? p.lng : min, this.data[0].lng), maxLng: this.data.reduce((max, p) => p.lng > max ? p.lng : max, this.data[0].lng), }; for(let n=1; n lst) i.ascend += cur - lst; else i.descend += lst - cur; } i.totalAscend = i.ascend - i.descend; i.avgSpeed = i.distance / i.movTime * 3.6; i.center = this.data[parseInt(this.data.length/2)]; i.totalTime = i.endTime - i.startTime; return i; } } function getRandomColor(str) { let arr = str.split(''); let i = Math.abs(arr.reduce( (hashCode, currentVal) => (hashCode = currentVal.charCodeAt(0) + (hashCode << 6) + (hashCode << 16) - hashCode), 0 )); return "hsl(" + (i * 2 % 360) + ", " + (i * 3 % 60 + 40) + "%, " + (i * 5 % 40 + 30) + "%)"; } function distance (a, b) { // Convert degrees to radians var lat1 = a.lat * Math.PI / 180.0; var lon1 = a.lng * Math.PI / 180.0; var lat2 = b.lat * Math.PI / 180.0; var lon2 = b.lng * Math.PI / 180.0; // radius of earth in metres var r = 6378100; // P var rho1 = r * Math.cos(lat1); var z1 = r * Math.sin(lat1); var x1 = rho1 * Math.cos(lon1); var y1 = rho1 * Math.sin(lon1); // Q var rho2 = r * Math.cos(lat2); var z2 = r * Math.sin(lat2); var x2 = rho2 * Math.cos(lon2); var y2 = rho2 * Math.sin(lon2); // Dot product var dot = (x1 * x2 + y1 * y2 + z1 * z2); var cos_theta = dot / (r * r); var theta = Math.acos(cos_theta); // Distance in Metres return r * theta; } function timeFormat (sec, x = "time") { let d = new Date(sec*1000); let t = new Date(null,null,null,null,null,sec).toTimeString().match(/\d{2}:\d{2}:\d{2}/)[0]; switch(x) { case "time": if(sec < 3600*24) return t; else return Math.floor(sec/3600/24) + "d " + t; case "datetime": return d.toLocaleDateString('de-DE', { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }); case "date": return d.toLocaleDateString('de-DE', { year: "numeric", month: "2-digit", day: "2-digit" }); case "short": return d.toLocaleDateString('de-DE', { year: "numeric", month: "2-digit" }); } } function median(values) { values.sort( function(a,b) {return a - b;} ); var half = Math.floor(values.length/2); return values[half]; } function avg (v) { return v.reduce((a,b) => a+b, 0)/v.length; } function smoothOut (vector, variance) { var t_avg = avg(vector)*variance; var ret = Array(vector.length); for (var i = 0; i < vector.length; i++) { (function () { var prev = i>0 ? ret[i-1] : vector[i]; var next = i