preprocessor.js 8.0 KB

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