index.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. window.addEventListener('error', ((event, source, lineno, colno, error) => {
  2. alert(source + ' ' + lineno + ',' + colno + ': ' + error.message)
  3. }))
  4. const MIN_SPEED = 3 // km/h
  5. function round(value, decimals = 0) {
  6. return Math.round(value * (10 ** decimals)) / (10 ** decimals)
  7. }
  8. function fmtHhMm(h, m) {
  9. return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m)
  10. }
  11. function fmtDuration(seconds, resolution = null) {
  12. const divmod = (x, y) => [Math.floor(x / y), x % y];
  13. let days = 0, hours = 0, mins = 0, secs = seconds;
  14. [mins, secs] = divmod(secs, 60);
  15. [hours, mins] = divmod(mins, 60);
  16. [days, hours] = divmod(hours, 24);
  17. switch (resolution) {
  18. case "d":
  19. hours = mins = secs = 0
  20. break
  21. case "h":
  22. mins = secs = 0
  23. break
  24. case "m":
  25. secs = 0
  26. break
  27. case "s":
  28. secs = Math.floor(secs)
  29. break
  30. }
  31. const fmtToken = (value, unit) => ((value > 0 ? value + unit + ' ' : ''))
  32. const result = fmtToken(days, 'd') + fmtToken(hours, 'h') + fmtToken(mins, 'm') + fmtToken(secs, 's')
  33. if (result.length > 0) {
  34. return result.trim()
  35. } else {
  36. return '0' + (resolution || 's')
  37. }
  38. }
  39. document.addEventListener('alpine:init', () => {
  40. Alpine.data('main', () => ({
  41. page: 'dashboard',
  42. error: '',
  43. speed: {
  44. minRide: MIN_SPEED, // km/h
  45. current: 0, // km/h
  46. max: 0, // km/h
  47. avg: 0, // km/h
  48. nowaitAvg: 0, // km/h
  49. timeseries: {
  50. cache: [],
  51. values: []
  52. }
  53. },
  54. time: {
  55. start: new Date(), // date
  56. update: null, // epoch number
  57. waitDuration: 0, // secs
  58. tripDuration: 0, // secs
  59. updateDelay: null, //secs
  60. },
  61. altitude: {
  62. current: null, // meter
  63. accuracy: null // meter
  64. },
  65. heading: null, // 0.0-360.0 degrees
  66. positionAccuracy: null, // meter
  67. noSleep: null,
  68. init() {
  69. if (navigator.geolocation) {
  70. setInterval(() => {
  71. const now = new Date()
  72. this.time.tripDuration = (now.getTime() - this.time.start.getTime()) / 1000
  73. if (this.time.update)
  74. this.time.updateDelay = Math.floor((now.getTime() - this.time.update) / 1000)
  75. if (!isNaN(this.speed.current)) {
  76. this.speed.timeseries.cache.push(this.speed.current)
  77. if (Math.floor(now.getTime() / 1000) % 5 === 0) {
  78. if (this.speed.timeseries.cache.length > 0) {
  79. const currentAvg = this.speed.timeseries.cache.reduce((sum, val) => sum + val, 0) / this.speed.timeseries.cache.length
  80. this.speed.timeseries.values.push(currentAvg)
  81. this.speed.timeseries.cache = []
  82. }
  83. if (this.speed.timeseries.values.length > 0) {
  84. let sumTotal = 0, sumNoWait = 0, countNoWait = 0
  85. for (const sp of this.speed.timeseries.values) {
  86. sumTotal += sp
  87. if (sp >= MIN_SPEED) {
  88. sumNoWait += sp
  89. countNoWait++
  90. }
  91. }
  92. this.speed.avg = sumTotal / this.speed.timeseries.values.length
  93. this.speed.nowaitAvg = (sumNoWait / countNoWait) || 0
  94. }
  95. }
  96. if (this.speed.current < MIN_SPEED) {
  97. this.time.waitDuration++
  98. }
  99. }
  100. }, 1000)
  101. navigator.geolocation.watchPosition(({timestamp, coords}) => {
  102. this.time.update = timestamp
  103. this.speed.current = coords.speed * 3.6 // m/s to km/h
  104. if (!this.speed.max || this.speed.current > this.speed.max) this.speed.max = this.speed.current
  105. this.altitude.current = coords.altitude
  106. this.altitude.accuracy = coords.altitudeAccuracy
  107. this.heading = coords.heading
  108. this.positionAccuracy = coords.accuracy
  109. }, err => {
  110. this.error = err.message
  111. }, {
  112. enableHighAccuracy: true,
  113. timeout: 15_000
  114. })
  115. } else {
  116. alert("Geolocation is not supported by this browser.")
  117. }
  118. },
  119. fmtStartTime() {
  120. return fmtHhMm(this.time.start.getHours(), this.time.start.getMinutes())
  121. },
  122. fmtTripTime() {
  123. return fmtDuration(this.time.tripDuration, 'm')
  124. },
  125. fmtWaitTime() {
  126. return fmtDuration(this.time.waitDuration)
  127. },
  128. fmtUpdateDelay() {
  129. return fmtDuration(this.time.updateDelay) + ' ago'
  130. },
  131. fmtHeading() {
  132. const directions = ['N', 'NE', 'NE', 'NE', 'E', 'SE', 'SE', 'SE', 'S', 'SW', 'SW', 'SW', 'W', 'NW', 'NW', 'NW', 'N']
  133. return directions[round(this.heading / 22.5)]
  134. },
  135. fmtCurrentSpeed() {
  136. return round(this.speed.current)
  137. },
  138. fmtMaxSpeed() {
  139. return round(this.speed.max, 1).toLocaleString(undefined, {minimumFractionDigits: 1})
  140. },
  141. fmtAvgSpeed() {
  142. return round(this.speed.avg || 0, 1).toLocaleString(undefined, {minimumFractionDigits: 1})
  143. },
  144. fmtAvgNoWaitSpeed() {
  145. return round(this.speed.nowaitAvg || 0, 1).toLocaleString(undefined, {minimumFractionDigits: 1})
  146. },
  147. clamp(value, min, max) {
  148. return Math.min(Math.max(value, min), max)
  149. },
  150. toggleNoSleep() {
  151. if (this.noSleep) {
  152. this.noSleep.disable()
  153. this.noSleep = null
  154. } else {
  155. this.noSleep = new NoSleep()
  156. this.noSleep.enable()
  157. }
  158. }
  159. }))
  160. })
  161. window.addEventListener('load', async () => {
  162. if ('serviceWorker' in navigator) {
  163. try {
  164. await navigator.serviceWorker.register('./sw.js');
  165. } catch (e) {
  166. console.error(e)
  167. }
  168. }
  169. })