const options = { legend: { position: "ne", show: true, noColumns: 3, }, zoom: { interactive: true }, pan: { interactive: true, cursor: "move", frameRate: 60 }, series: { lines: { show: true, lineWidth: 2 } }, xaxis: { tickDecimals: 2, //tickSize: 0.01 }, yaxis: { tickDecimals: 2, //tickSize: 0.01 } } window.onload = function() { const fileSelector = document.getElementById('file-selector') fileSelector.addEventListener('change', (event) => { const fileList = event.target.files importFiles(fileList) }) const dropArea = document.getElementsByClassName('content')[0]; dropArea.addEventListener('dragover', (event) => { event.stopPropagation() event.preventDefault() // Style the drag-and-drop as a "copy file" operation. event.dataTransfer.dropEffect = 'copy' }); dropArea.addEventListener('drop', (event) => { event.stopPropagation() event.preventDefault() const fileList = event.dataTransfer.files importFiles(fileList) }) const uploadBtn = document.getElementById("upload-btn"); const responseP = document.getElementById("response"); uploadBtn.addEventListener('click', (event) => { uploadBtn.disabled = true; responseP.textContent = "waiting for response..." const xmlhttp = new XMLHttpRequest() xmlhttp.open("POST", "markers/create", true); xmlhttp.setRequestHeader("Content-type", "application/json") const token = document.querySelector('[name=csrfmiddlewaretoken]').value xmlhttp.setRequestHeader('X-CSRFToken', token) xmlhttp.onreadystatechange = () => { if(xmlhttp.readyState == 4) { if(xmlhttp.status < 300) { responseP.textContent = "Markers created!" const body = JSON.parse(xmlhttp.responseText) for(const field in body) responseP.innerHTML += `
${field}: ${body[field]}` } else responseP.textContent = `Error code ${xmlhttp.status}` uploadBtn.disabled = false } } xmlhttp.send(JSON.stringify(markersToUpload)) }) const censorBtn = document.getElementById("censor-btn") censorBtn.addEventListener('click', (event) => { censorBtn.disabled = true; responseP.textContent = "waiting for response..." const xmlhttp = new XMLHttpRequest() xmlhttp.open("GET", "markers/censor", true) xmlhttp.onreadystatechange = () => { if(xmlhttp.readyState == 4) { if(xmlhttp.status < 300) { responseP.textContent = "Markers censored!" const body = JSON.parse(xmlhttp.responseText) for(const field in body) responseP.innerHTML += `
${field}: ${body[field]}` } else responseP.textContent = `Error code ${xmlhttp.status}` censorBtn.disabled = false } } xmlhttp.send() }) const tripBtn = document.getElementById("trip-btn") tripBtn.addEventListener('click', (event) => { tripBtn.disabled = true; responseP.textContent = "waiting for response..." const xmlhttp = new XMLHttpRequest() xmlhttp.open("GET", "markers/create-trips", true) xmlhttp.onreadystatechange = () => { if(xmlhttp.readyState == 4) { if(xmlhttp.status < 300) { responseP.textContent = "Trips created!" const body = JSON.parse(xmlhttp.responseText) for(const field in body) responseP.innerHTML += `
${field}: ${body[field]}` } else responseP.textContent = `Error code ${xmlhttp.status}` tripBtn.disabled = false } } xmlhttp.send() }) } function importFiles(fileList) { d = [] plot = $.plot("#placeholder", d, options); markersToUpload = {} for (const file of fileList) { if(file.name?.toLowerCase().endsWith(".txt")) readTxtFile(file) else if (file.name?.toLowerCase().endsWith(".nmea")) parseNmeaFile(file) } document.getElementById("upload-btn").style.display = "inline"; } function readTxtFile(file) { const reader = new FileReader() reader.addEventListener('load', (event) => { const lines = event.target.result.split('\n') let result = [] for(const line of lines) { const fields = line.split(',') if(fields.length != 8) continue result.push({ timestamp: fields[0], lat: parseFloat(fields[1]), lng: parseFloat(fields[2]), alt: parseFloat(fields[3]), hdop: parseInt(fields[4]), speed: parseFloat(fields[5]) }) } process(file.name, result) }); reader.readAsText(file) } function parseNmeaFile(file) { const reader = new FileReader() reader.addEventListener('load', (event) => { const lines = event.target.result.split('\n') let result = [] let ggaObj, oldTime; for(const line of lines) { const obj = GPS.Parse(line); if (obj.type == "RMC") { if(ggaObj.time != oldTime) { oldTime = ggaObj.time result.push({ timestamp: obj.time, lat: obj.lat, lng: obj.lon, alt: ggaObj.alt, hdop: ggaObj.hdop, speed: obj.speed }) } ggaObj = null } else if(ggaObj) { if(ggaObj.time != oldTime) { oldTime = ggaObj.time result.push({ timestamp: ggaObj.time, lat: ggaObj.lat, lng: ggaObj.lon, alt: ggaObj.alt, hdop: ggaObj.hdop, speed: null }) } ggaObj = null } if(obj.type == "GGA") { ggaObj = obj; } } process(file.name, result) }); reader.readAsText(file) } let markersToUpload; function process(name, markers) { d.push({ label: `${name} ${markers.length}`, data: markers.map(m => [m.lng, m.lat]) }) let valid = [] for(let i=0; i 0 && dx12/dt12 > 50) {// > 50 m/s = 180 km/h console.log(`n=${i} ${m1.timestamp} too fast: ${dx12/dt12} m/s`) if(dx12/dt12 > dx13/dt13) continue else markers.pop(i+1) } else if(dt23 > 0 && dx23/dt23 > 50) {// > 50 m/s = 180 km/h console.log(`n=${i} ${m1.timestamp} too fast: ${dx23/dt23} m/s`) if(dx23/dt23 > dx13/dt13) markers.pop(i+1) else markers.pop(i+2) } else if(dx12 > 50 && dx12 > dx13) { console.log(`n=${i} ${m1.timestamp} too far: ${dx12} m`) continue } valid.push(m1) } markers = RamerDouglasPeucker2d(valid, 0.00001) //.0001 = 7m d.push({ label: `RamerDouglasPeucker2d ${markers.length}`, data: markers.map(m => [m.lat, m.lng]) }) markers = clean(markers) d.push({ label: `cleaned ${markers.length}`, data: markers.map(m => [m.lat, m.lng]) }) plot.setData(d) plot.setupGrid() //only necessary if your new data will change the axes or grid plot.draw() markersToUpload[name] = markers } function clean(markers) { let cleaned = [], tmp = [], oldHeading = 0 const threshold = 5; // meter for(const marker of markers) { if(!tmp.length) { tmp.push(marker) } const heading = GPS.Heading(tmp[tmp.length-1].lat, tmp[tmp.length-1].lng, marker.lat, marker.lng) if(tmp.some(m => distance(m, marker) < threshold) && Math.abs(heading - oldHeading) > 90) { tmp.push(marker) oldHeading = heading continue } oldHeading = heading cleaned.push(marker) tmp = [marker] } return cleaned.concat(tmp.slice(1)) } function distance(m1, m2) { const theta = m1.lng - m2.lng let dist = Math.sin(m1.lat * Math.PI / 180) * Math.sin(m2.lat * Math.PI / 180) + Math.cos(m1.lat * Math.PI / 180) * Math.cos(m2.lat * Math.PI / 180) * Math.cos(theta * Math.PI / 180); dist = Math.acos(dist); dist = dist / Math.PI * 180; const miles = dist * 60 * 1.1515; return (miles * 1609.344); // Meter } function perpendicularDistance2d(ptX, ptY, l1x, l1y, l2x, l2y) { if (l2x == l1x) { //vertical lines - treat this case specially to avoid dividing //by zero return Math.abs(ptX - l2x); } else { const slope = ((l2y-l1y) / (l2x-l1x)); const passThroughY = (0-l1x)*slope + l1y; return (Math.abs((slope * ptX) - ptY + passThroughY)) / (Math.sqrt(slope*slope + 1)); } } function RamerDouglasPeucker2d(pointList, epsilon) { if (pointList.length < 2) { return pointList; } // Find the point with the maximum distance let dmax = 0; let index = 0; let totalPoints = pointList.length; for (let i = 1; i < (totalPoints - 1); i++) { let d = perpendicularDistance2d( pointList[i].lat, pointList[i].lng, pointList[0].lat, pointList[0].lng, pointList[totalPoints-1].lat, pointList[totalPoints-1].lng); if (d > dmax) { index = i; dmax = d; } } // If max distance is greater than epsilon, recursively simplify if (dmax >= epsilon) { // Recursive call on each 'half' of the polyline const recResults1 = RamerDouglasPeucker2d(pointList.slice(0, index + 1), epsilon); const recResults2 = RamerDouglasPeucker2d(pointList.slice(index), epsilon); // Build the result list return recResults1.slice(0, recResults1.length - 1).concat(recResults2); } else { return [pointList[0], pointList[totalPoints-1]]; } }