preprocessor.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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. if(file.name?.toLowerCase().endsWith(".txt"))
  123. readTxtFile(file)
  124. else if (file.name?.toLowerCase().endsWith(".nmea"))
  125. parseNmeaFile(file)
  126. }
  127. document.getElementById("upload-btn").style.display = "inline";
  128. }
  129. function readTxtFile(file) {
  130. const reader = new FileReader()
  131. reader.addEventListener('load', (event) => {
  132. const lines = event.target.result.split('\n')
  133. let result = []
  134. for(const line of lines) {
  135. const fields = line.split(',')
  136. if(fields.length != 8)
  137. continue
  138. result.push({
  139. timestamp: fields[0],
  140. lat: parseFloat(fields[1]),
  141. lng: parseFloat(fields[2]),
  142. alt: parseFloat(fields[3]),
  143. hdop: parseInt(fields[4]),
  144. speed: parseFloat(fields[5])
  145. })
  146. }
  147. process(file.name, result)
  148. });
  149. reader.readAsText(file)
  150. }
  151. function parseNmeaFile(file) {
  152. const reader = new FileReader()
  153. reader.addEventListener('load', (event) => {
  154. const lines = event.target.result.split('\n')
  155. let result = []
  156. let ggaObj, oldTime;
  157. for(const line of lines) {
  158. const obj = GPS.Parse(line);
  159. if (obj.type == "RMC") {
  160. if(ggaObj.time != oldTime) {
  161. oldTime = ggaObj.time
  162. result.push({
  163. timestamp: obj.time,
  164. lat: obj.lat,
  165. lng: obj.lon,
  166. alt: ggaObj.alt,
  167. hdop: ggaObj.hdop,
  168. speed: obj.speed
  169. })
  170. }
  171. ggaObj = null
  172. } else if(ggaObj) {
  173. if(ggaObj.time != oldTime) {
  174. oldTime = ggaObj.time
  175. result.push({
  176. timestamp: ggaObj.time,
  177. lat: ggaObj.lat,
  178. lng: ggaObj.lon,
  179. alt: ggaObj.alt,
  180. hdop: ggaObj.hdop,
  181. speed: null
  182. })
  183. }
  184. ggaObj = null
  185. }
  186. if(obj.type == "GGA") {
  187. ggaObj = obj;
  188. }
  189. }
  190. process(file.name, result)
  191. });
  192. reader.readAsText(file)
  193. }
  194. let markersToUpload;
  195. function process(name, markers) {
  196. d.push({
  197. label: `${name} ${markers.length}`,
  198. data: markers.map(m => [m.lng, m.lat])
  199. })
  200. let valid = []
  201. for(let i=0; i<markers.length-2; i++) {
  202. const m1 = markers[i];
  203. const m2 = markers[i+1];
  204. const m3 = markers[i+2];
  205. const dx12 = distance(m1, m2)
  206. const dx23 = distance(m2, m3)
  207. const dx13 = distance(m2, m3)
  208. const dt12 = (m2.timestamp - m1.timestamp) / 1000
  209. const dt23 = (m3.timestamp - m2.timestamp) / 1000
  210. const dt13 = dt12 + dt23
  211. if(dt12 > 0 && dx12/dt12 > 50) {// > 50 m/s = 180 km/h
  212. console.log(`n=${i} ${m1.timestamp} too fast: ${dx12/dt12} m/s`)
  213. if(dx12/dt12 > dx13/dt13)
  214. continue
  215. else
  216. markers.pop(i+1)
  217. }
  218. else if(dt23 > 0 && dx23/dt23 > 50) {// > 50 m/s = 180 km/h
  219. console.log(`n=${i} ${m1.timestamp} too fast: ${dx23/dt23} m/s`)
  220. if(dx23/dt23 > dx13/dt13)
  221. markers.pop(i+1)
  222. else
  223. markers.pop(i+2)
  224. }
  225. else if(dx12 > 50 && dx12 > dx13) {
  226. console.log(`n=${i} ${m1.timestamp} too far: ${dx12} m`)
  227. continue
  228. }
  229. valid.push(m1)
  230. }
  231. markers = RamerDouglasPeucker2d(valid, 0.00001) //.0001 = 7m
  232. d.push({
  233. label: `RamerDouglasPeucker2d ${markers.length}`,
  234. data: markers.map(m => [m.lat, m.lng])
  235. })
  236. markers = clean(markers)
  237. d.push({
  238. label: `cleaned ${markers.length}`,
  239. data: markers.map(m => [m.lat, m.lng])
  240. })
  241. plot.setData(d)
  242. plot.setupGrid() //only necessary if your new data will change the axes or grid
  243. plot.draw()
  244. markersToUpload[name] = markers
  245. }
  246. function clean(markers) {
  247. let cleaned = [], tmp = [], oldHeading = 0
  248. const threshold = 5; // meter
  249. for(const marker of markers) {
  250. if(!tmp.length) {
  251. tmp.push(marker)
  252. }
  253. const heading = GPS.Heading(tmp[tmp.length-1].lat, tmp[tmp.length-1].lng, marker.lat, marker.lng)
  254. if(tmp.some(m => distance(m, marker) < threshold) && Math.abs(heading - oldHeading) > 90) {
  255. tmp.push(marker)
  256. oldHeading = heading
  257. continue
  258. }
  259. oldHeading = heading
  260. cleaned.push(marker)
  261. tmp = [marker]
  262. }
  263. return cleaned.concat(tmp.slice(1))
  264. }
  265. function distance(m1, m2) {
  266. const theta = m1.lng - m2.lng
  267. let dist = Math.sin(m1.lat * Math.PI / 180) * Math.sin(m2.lat * Math.PI / 180) +
  268. Math.cos(m1.lat * Math.PI / 180) * Math.cos(m2.lat * Math.PI / 180) * Math.cos(theta * Math.PI / 180);
  269. dist = Math.acos(dist);
  270. dist = dist / Math.PI * 180;
  271. const miles = dist * 60 * 1.1515;
  272. return (miles * 1609.344); // Meter
  273. }
  274. function perpendicularDistance2d(ptX, ptY, l1x, l1y, l2x, l2y) {
  275. if (l2x == l1x)
  276. {
  277. //vertical lines - treat this case specially to avoid dividing
  278. //by zero
  279. return Math.abs(ptX - l2x);
  280. }
  281. else
  282. {
  283. const slope = ((l2y-l1y) / (l2x-l1x));
  284. const passThroughY = (0-l1x)*slope + l1y;
  285. return (Math.abs((slope * ptX) - ptY + passThroughY)) /
  286. (Math.sqrt(slope*slope + 1));
  287. }
  288. }
  289. function RamerDouglasPeucker2d(pointList, epsilon) {
  290. if (pointList.length < 2)
  291. {
  292. return pointList;
  293. }
  294. // Find the point with the maximum distance
  295. let dmax = 0;
  296. let index = 0;
  297. let totalPoints = pointList.length;
  298. for (let i = 1; i < (totalPoints - 1); i++)
  299. {
  300. let d = perpendicularDistance2d(
  301. pointList[i].lat, pointList[i].lng,
  302. pointList[0].lat, pointList[0].lng,
  303. pointList[totalPoints-1].lat,
  304. pointList[totalPoints-1].lng);
  305. if (d > dmax)
  306. {
  307. index = i;
  308. dmax = d;
  309. }
  310. }
  311. // If max distance is greater than epsilon, recursively simplify
  312. if (dmax >= epsilon)
  313. {
  314. // Recursive call on each 'half' of the polyline
  315. const recResults1 = RamerDouglasPeucker2d(pointList.slice(0, index + 1), epsilon);
  316. const recResults2 = RamerDouglasPeucker2d(pointList.slice(index), epsilon);
  317. // Build the result list
  318. return recResults1.slice(0, recResults1.length - 1).concat(recResults2);
  319. }
  320. else
  321. {
  322. return [pointList[0], pointList[totalPoints-1]];
  323. }
  324. }