123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- 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 += `<br/>${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 += `<br/>${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 += `<br/>${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<markers.length-2; i++) {
- const m1 = markers[i];
- const m2 = markers[i+1];
- const m3 = markers[i+2];
- const dx12 = distance(m1, m2)
- const dx23 = distance(m2, m3)
- const dx13 = distance(m2, m3)
- const dt12 = (m2.timestamp - m1.timestamp) / 1000
- const dt23 = (m3.timestamp - m2.timestamp) / 1000
- const dt13 = dt12 + dt23
-
- if(dt12 > 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]];
- }
- }
|