kurviger.com Speed Stats

Show speed stats (Desktop only)

  1. // ==UserScript==
  2. // @name kurviger.com Speed Stats
  3. // @namespace shiftgeist
  4. // @match https://kurviger.com/*
  5. // @match https://kurviger.de/*
  6. // @grant GM_addStyle
  7. // @version 20250417
  8. // @author shiftgeist
  9. // @description Show speed stats (Desktop only)
  10. // @license GNU GPLv3
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=kurviger.com
  12. // ==/UserScript==
  13.  
  14. GM_addStyle(`
  15. .leaflet-marker-icon.rounded-full {
  16. display: none !important;
  17. }
  18.  
  19. .leaflet-zoom-animated [stroke="#ff7575"] /* 30 */ {
  20. stroke-width: 10px;
  21. }
  22.  
  23. /* .leaflet-zoom-animated [stroke="#ffcf6e"], 50 */
  24. .leaflet-zoom-animated [stroke="#fff06e"], /* 70 */
  25. .leaflet-zoom-animated [stroke="#6eff8d"] /* 100 */ {
  26. stroke: #477BD6;
  27. }
  28. `)
  29.  
  30. const debug = window.localStorage.getItem('debug-log') === 'true'
  31. const doc = document
  32. const elDropdown = '.selection-dropdown'
  33. const elSource = '.heightgraph-container-inner'
  34. let sourceObserver = null
  35. let stats = null
  36. let timeoutCounter = 0
  37.  
  38. function log(...params) {
  39. if (debug) {
  40. console.debug('[Speed]', ...params)
  41. }
  42. }
  43.  
  44. function getSpeed(color) {
  45. switch (color) {
  46. case 'rgb(255, 117, 117)':
  47. return '30'
  48. case 'rgb(255, 207, 110)':
  49. return '50'
  50. case 'rgb(255, 240, 110)':
  51. return '70'
  52. case 'rgb(110, 255, 141)':
  53. return '100'
  54. default:
  55. case 'rgb(179, 179, 179)':
  56. return 'unknown'
  57. }
  58. }
  59.  
  60. function getData() {
  61. const source = doc.querySelector(elSource)
  62.  
  63. const childrenEl = Array.from(source.querySelectorAll('.area'))
  64. const containerWidth = childrenEl.reduce((acc, v) => acc + v.getBoundingClientRect().width - 4, 0)
  65.  
  66. const children = childrenEl.map(v => ({
  67. width: v.getBoundingClientRect().width / containerWidth,
  68. speed: getSpeed(v.style.fill),
  69. }))
  70.  
  71. const speeds = {
  72. 30: 0,
  73. 50: 0,
  74. 70: 0,
  75. 100: 0,
  76. unknown: 0,
  77. }
  78.  
  79. for (const s of children) {
  80. speeds[s.speed] += s.width
  81. }
  82.  
  83. speeds.sum = speeds['30'] + speeds['50'] + speeds['70'] + speeds['100']
  84. speeds.average = speeds['30'] * 30 + speeds['50'] * 50 + speeds['70'] * 70 + speeds['100'] * 100
  85.  
  86. return speeds
  87. }
  88.  
  89. function isSpeedGraph() {
  90. return doc.querySelector(elDropdown).value === '2'
  91. }
  92.  
  93. function prepareStats() {
  94. if (stats) {
  95. log('remove first', stats)
  96. stats.remove()
  97. stats = null
  98. }
  99.  
  100. createListener(createStats)
  101.  
  102. if (!isSpeedGraph()) {
  103. log('wrong graph type')
  104. return
  105. }
  106.  
  107. return true
  108. }
  109.  
  110. function createListener(callback) {
  111. if (!sourceObserver) {
  112. log('registering observer')
  113.  
  114. sourceObserver = new MutationObserver(() => {
  115. if (prepareStats()) {
  116. callback()
  117. }
  118. })
  119.  
  120. sourceObserver.observe(document.querySelector(elSource), { childList: true })
  121. }
  122. }
  123.  
  124. function createStats() {
  125. if (!prepareStats()) return
  126.  
  127. log('create stats')
  128.  
  129. stats = doc.createElement('div')
  130. stats.id = 'SpeedStats'
  131. stats.style.zIndex = '9999'
  132. stats.style.position = 'absolute'
  133. stats.style.top = '155px'
  134. stats.style.left = '50%'
  135. stats.style.transform = 'translate(-50%)'
  136. doc.querySelector('.heightgraph').appendChild(stats)
  137.  
  138. const d = getData()
  139.  
  140. stats.textContent += `${(d['30'] * 100).toFixed()}% 30 km/h`
  141. stats.textContent += `, `
  142. stats.textContent += `${(d['50'] * 100).toFixed()}% 50 km/h`
  143. stats.textContent += `, `
  144. stats.textContent += `${(d['70'] * 100).toFixed()}% 70 km/h`
  145. stats.textContent += `, `
  146. stats.textContent += `${(d['100'] * 100).toFixed()}% 100 km/h`
  147. stats.textContent += `, `
  148. stats.textContent += `Average ${d.average.toFixed()} km/h`
  149.  
  150. log('Stats created', d)
  151. }
  152.  
  153. function waitForLoad(callback) {
  154. log('wait for load')
  155.  
  156. if (!window.location.href.includes('/plan') || timeoutCounter > 20) {
  157. log('wrong url or too many retries')
  158. return
  159. }
  160.  
  161. if (doc.querySelectorAll(`${elSource}, ${elDropdown}`).length) {
  162. log('has source')
  163. callback()
  164. } else {
  165. timeoutCounter += 1
  166. setTimeout(() => waitForLoad(callback), 20 * (timeoutCounter / 2 + 1))
  167. }
  168. }
  169.  
  170. waitForLoad(createStats)

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址