preprocessor.js 7.6 KB

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