const options = { legend: { position: "nw", show: true, noColumns: 2, }, 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; console.log(fileList); 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; console.log(fileList); importFiles(fileList) }); } function importFiles(fileList) { d = [] plot = $.plot("#placeholder", d, options); for (const file of fileList) { if(file.name?.toLowerCase().endsWith(".txt")) readTxtFile(file) else if (file.name?.toLowerCase().endsWith(".nmea")) parseNmeaFile(file) } } 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: parseInt(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: ggaObj.time, lat: ggaObj.lat, lng: ggaObj.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) } function process(name, markers) { d.push({ label: `${name} ${markers.length}`, data: markers.map(m => [m.lat, m.lng]) }) markers = RamerDouglasPeucker2d(markers, 0.00001) //.0001 = 7m d.push({ label: `RamerDouglasPeucker2d ${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(); } 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]]; } }