preprocessor.js 8.8 KB

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