preprocessor.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. const options = {
  2. legend: {
  3. position: "ne",
  4. show: true,
  5. noColumns: 3,
  6. },
  7. zoom: {
  8. interactive: true
  9. },
  10. pan: {
  11. interactive: true,
  12. cursor: "move",
  13. frameRate: 60
  14. },
  15. series: {
  16. lines: {
  17. show: true,
  18. lineWidth: 2
  19. }
  20. },
  21. xaxis: {
  22. tickDecimals: 2,
  23. //tickSize: 0.01
  24. },
  25. yaxis: {
  26. tickDecimals: 2,
  27. //tickSize: 0.01
  28. }
  29. }
  30. window.onload = function() {
  31. const fileSelector = document.getElementById('file-selector')
  32. fileSelector.addEventListener('change', (event) => {
  33. const fileList = event.target.files
  34. importFiles(fileList)
  35. })
  36. const dropArea = document.getElementsByClassName('content')[0];
  37. dropArea.addEventListener('dragover', (event) => {
  38. event.stopPropagation()
  39. event.preventDefault()
  40. // Style the drag-and-drop as a "copy file" operation.
  41. event.dataTransfer.dropEffect = 'copy'
  42. });
  43. dropArea.addEventListener('drop', (event) => {
  44. event.stopPropagation()
  45. event.preventDefault()
  46. const fileList = event.dataTransfer.files
  47. importFiles(fileList)
  48. })
  49. const uploadBtn = document.getElementById("upload-btn");
  50. const responseP = document.getElementById("response");
  51. uploadBtn.addEventListener('click', (event) => {
  52. uploadBtn.disabled = true;
  53. responseP.textContent = "waiting for response..."
  54. const xmlhttp = new XMLHttpRequest()
  55. xmlhttp.open("POST", "markers/create", true);
  56. xmlhttp.setRequestHeader("Content-type", "application/json")
  57. const token = document.querySelector('[name=csrfmiddlewaretoken]').value
  58. xmlhttp.setRequestHeader('X-CSRFToken', token)
  59. xmlhttp.onreadystatechange = () => {
  60. if(xmlhttp.readyState == 4) {
  61. if(xmlhttp.status < 300) {
  62. responseP.textContent = "Markers created!"
  63. const body = JSON.parse(xmlhttp.responseText)
  64. for(const field in body)
  65. responseP.innerHTML += `<br/>${field}: ${body[field]}`
  66. }
  67. else
  68. responseP.textContent = `Error code ${xmlhttp.status}`
  69. uploadBtn.disabled = false
  70. }
  71. }
  72. xmlhttp.send(JSON.stringify(markersToUpload))
  73. })
  74. const censorBtn = document.getElementById("censor-btn")
  75. censorBtn.addEventListener('click', (event) => {
  76. censorBtn.disabled = true;
  77. responseP.textContent = "waiting for response..."
  78. const xmlhttp = new XMLHttpRequest()
  79. xmlhttp.open("GET", "markers/censor", true)
  80. xmlhttp.onreadystatechange = () => {
  81. if(xmlhttp.readyState == 4) {
  82. if(xmlhttp.status < 300) {
  83. responseP.textContent = "Markers censored!"
  84. const body = JSON.parse(xmlhttp.responseText)
  85. for(const field in body)
  86. responseP.innerHTML += `<br/>${field}: ${body[field]}`
  87. }
  88. else
  89. responseP.textContent = `Error code ${xmlhttp.status}`
  90. censorBtn.disabled = false
  91. }
  92. }
  93. xmlhttp.send()
  94. })
  95. const tripBtn = document.getElementById("trip-btn")
  96. tripBtn.addEventListener('click', (event) => {
  97. tripBtn.disabled = true;
  98. responseP.textContent = "waiting for response..."
  99. const xmlhttp = new XMLHttpRequest()
  100. xmlhttp.open("GET", "markers/create-trips", true)
  101. xmlhttp.onreadystatechange = () => {
  102. if(xmlhttp.readyState == 4) {
  103. if(xmlhttp.status < 300) {
  104. responseP.textContent = "Trips created!"
  105. const body = JSON.parse(xmlhttp.responseText)
  106. for(const field in body)
  107. responseP.innerHTML += `<br/>${field}: ${body[field]}`
  108. }
  109. else
  110. responseP.textContent = `Error code ${xmlhttp.status}`
  111. tripBtn.disabled = false
  112. }
  113. }
  114. xmlhttp.send()
  115. })
  116. }
  117. function importFiles(fileList) {
  118. d = []
  119. plot = $.plot("#placeholder", d, options);
  120. markersToUpload = {}
  121. for (const file of fileList) {
  122. extension = file.name?.toLowerCase().split('.').pop()
  123. if(extension === "txt")
  124. readTxtFile(file)
  125. else if (extension === "nmea")
  126. parseNmeaFile(file)
  127. else if (extension === "gpx")
  128. parseGpxFile(file)
  129. else
  130. console.log(`unsupported file type (${extension})`)
  131. }
  132. document.getElementById("upload-btn").style.display = "inline";
  133. }
  134. function readTxtFile(file) {
  135. const reader = new FileReader()
  136. reader.addEventListener('load', (event) => {
  137. const lines = event.target.result.split('\n')
  138. let result = []
  139. for(const line of lines) {
  140. const fields = line.split(',')
  141. if(fields.length != 8)
  142. continue
  143. result.push({
  144. timestamp: fields[0],
  145. lat: parseFloat(fields[1]),
  146. lng: parseFloat(fields[2]),
  147. alt: parseFloat(fields[3]),
  148. hdop: parseInt(fields[4]),
  149. speed: parseFloat(fields[5])
  150. })
  151. }
  152. process(file.name, result)
  153. });
  154. reader.readAsText(file)
  155. }
  156. function parseNmeaFile(file) {
  157. const reader = new FileReader()
  158. reader.addEventListener('load', (event) => {
  159. const lines = event.target.result.split('\n')
  160. let result = []
  161. let ggaObj, oldTime;
  162. for(const line of lines) {
  163. const obj = GPS.Parse(line);
  164. if (obj.type == "RMC") {
  165. if(ggaObj.time != oldTime) {
  166. oldTime = ggaObj.time
  167. result.push({
  168. timestamp: obj.time,
  169. lat: obj.lat,
  170. lng: obj.lon,
  171. alt: ggaObj.alt,
  172. hdop: ggaObj.hdop,
  173. speed: obj.speed
  174. })
  175. }
  176. ggaObj = null
  177. } else if(ggaObj) {
  178. if(ggaObj.time != oldTime) {
  179. oldTime = ggaObj.time
  180. result.push({
  181. timestamp: ggaObj.time,
  182. lat: ggaObj.lat,
  183. lng: ggaObj.lon,
  184. alt: ggaObj.alt,
  185. hdop: ggaObj.hdop,
  186. speed: null
  187. })
  188. }
  189. ggaObj = null
  190. }
  191. if(obj.type == "GGA") {
  192. ggaObj = obj;
  193. }
  194. }
  195. process(file.name, result)
  196. });
  197. reader.readAsText(file)
  198. }
  199. function parseGpxFile(file) {
  200. const reader = new FileReader()
  201. reader.addEventListener('load', (event) => {
  202. let gpx = new gpxParser()
  203. gpx.parse(event.target.result)
  204. let result = []
  205. for (const track of gpx.tracks) {
  206. for (const point of track.points) {
  207. result.push({
  208. timestamp: point.time,
  209. lat: point.lat,
  210. lng: point.lon,
  211. alt: point.ele,
  212. hdop: null,
  213. speed: null
  214. })
  215. }
  216. }
  217. process(file.name, result)
  218. });
  219. reader.readAsText(file)
  220. }
  221. let markersToUpload;
  222. function process(name, markers) {
  223. d.push({
  224. label: `${name} ${markers.length}`,
  225. data: markers.map(m => [m.lng, m.lat])
  226. })
  227. let valid = []
  228. for(let i=0; i<markers.length-2; i++) {
  229. const m1 = markers[i];
  230. const m2 = markers[i+1];
  231. const m3 = markers[i+2];
  232. const dx12 = distance(m1, m2)
  233. const dx23 = distance(m2, m3)
  234. const dx13 = distance(m2, m3)
  235. const dt12 = (m2.timestamp - m1.timestamp) / 1000
  236. const dt23 = (m3.timestamp - m2.timestamp) / 1000
  237. const dt13 = dt12 + dt23
  238. if(dt12 > 0 && dx12/dt12 > 50) {// > 50 m/s = 180 km/h
  239. console.log(`n=${i} ${m1.timestamp} too fast: ${dx12/dt12} m/s`)
  240. if(dx12/dt12 > dx13/dt13)
  241. continue
  242. else
  243. markers.pop(i+1)
  244. }
  245. else if(dt23 > 0 && dx23/dt23 > 50) {// > 50 m/s = 180 km/h
  246. console.log(`n=${i} ${m1.timestamp} too fast: ${dx23/dt23} m/s`)
  247. if(dx23/dt23 > dx13/dt13)
  248. markers.pop(i+1)
  249. else
  250. markers.pop(i+2)
  251. }
  252. else if(dx12 > 50 && dx12 > dx13) {
  253. console.log(`n=${i} ${m1.timestamp} too far: ${dx12} m`)
  254. continue
  255. }
  256. valid.push(m1)
  257. }
  258. markers = RamerDouglasPeucker2d(valid, 0.00001) //.0001 = 7m
  259. d.push({
  260. label: `RamerDouglasPeucker2d ${markers.length}`,
  261. data: markers.map(m => [m.lng, m.lat])
  262. })
  263. markers = clean(markers)
  264. d.push({
  265. label: `cleaned ${markers.length}`,
  266. data: markers.map(m => [m.lng, m.lat])
  267. })
  268. plot.setData(d)
  269. plot.setupGrid() //only necessary if your new data will change the axes or grid
  270. plot.draw()
  271. markersToUpload[name] = markers
  272. }
  273. function clean(markers) {
  274. let cleaned = [], tmp = [], oldHeading = 0
  275. const threshold = 5; // meter
  276. for(const marker of markers) {
  277. if(!tmp.length) {
  278. tmp.push(marker)
  279. }
  280. const heading = GPS.Heading(tmp[tmp.length-1].lat, tmp[tmp.length-1].lng, marker.lat, marker.lng)
  281. if(tmp.some(m => distance(m, marker) < threshold) && Math.abs(heading - oldHeading) > 90) {
  282. tmp.push(marker)
  283. oldHeading = heading
  284. continue
  285. }
  286. oldHeading = heading
  287. cleaned.push(marker)
  288. tmp = [marker]
  289. }
  290. return cleaned.concat(tmp.slice(1))
  291. }
  292. function distance(m1, m2) {
  293. const theta = m1.lng - m2.lng
  294. let dist = Math.sin(m1.lat * Math.PI / 180) * Math.sin(m2.lat * Math.PI / 180) +
  295. Math.cos(m1.lat * Math.PI / 180) * Math.cos(m2.lat * Math.PI / 180) * Math.cos(theta * Math.PI / 180);
  296. dist = Math.acos(dist);
  297. dist = dist / Math.PI * 180;
  298. const miles = dist * 60 * 1.1515;
  299. return (miles * 1609.344); // Meter
  300. }
  301. function perpendicularDistance2d(ptX, ptY, l1x, l1y, l2x, l2y) {
  302. if (l2x == l1x)
  303. {
  304. //vertical lines - treat this case specially to avoid dividing
  305. //by zero
  306. return Math.abs(ptX - l2x);
  307. }
  308. else
  309. {
  310. const slope = ((l2y-l1y) / (l2x-l1x));
  311. const passThroughY = (0-l1x)*slope + l1y;
  312. return (Math.abs((slope * ptX) - ptY + passThroughY)) /
  313. (Math.sqrt(slope*slope + 1));
  314. }
  315. }
  316. function RamerDouglasPeucker2d(pointList, epsilon) {
  317. if (pointList.length < 2)
  318. {
  319. return pointList;
  320. }
  321. // Find the point with the maximum distance
  322. let dmax = 0;
  323. let index = 0;
  324. let totalPoints = pointList.length;
  325. for (let i = 1; i < (totalPoints - 1); i++)
  326. {
  327. let d = perpendicularDistance2d(
  328. pointList[i].lat, pointList[i].lng,
  329. pointList[0].lat, pointList[0].lng,
  330. pointList[totalPoints-1].lat,
  331. pointList[totalPoints-1].lng);
  332. if (d > dmax)
  333. {
  334. index = i;
  335. dmax = d;
  336. }
  337. }
  338. // If max distance is greater than epsilon, recursively simplify
  339. if (dmax >= epsilon)
  340. {
  341. // Recursive call on each 'half' of the polyline
  342. const recResults1 = RamerDouglasPeucker2d(pointList.slice(0, index + 1), epsilon);
  343. const recResults2 = RamerDouglasPeucker2d(pointList.slice(index), epsilon);
  344. // Build the result list
  345. return recResults1.slice(0, recResults1.length - 1).concat(recResults2);
  346. }
  347. else
  348. {
  349. return [pointList[0], pointList[totalPoints-1]];
  350. }
  351. }