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.lng, m.lat])
})
markers = clean(markers)
d.push({
label: `cleaned ${markers.length}`,
data: markers.map(m => [m.lng, m.lat])
})
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]];
}
}