webglMapRenderer.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import { CourseLayer } from '../layer/courseLayer'
  2. import { TrackLayer } from '../layer/trackLayer'
  3. import { GpsLayer } from '../layer/gpsLayer'
  4. import { TileLayer } from '../layer/tileLayer'
  5. import { TileStore, type TileStoreCallbacks } from '../tile/tileStore'
  6. import { type MapRenderer, type MapRendererStats, type MapScene } from './mapRenderer'
  7. import { WebGLTileRenderer } from './webglTileRenderer'
  8. import { WebGLVectorRenderer } from './webglVectorRenderer'
  9. import { CourseLabelRenderer } from './courseLabelRenderer'
  10. const RENDER_FRAME_MS = 16
  11. const ANIMATION_FRAME_MS = 33
  12. export class WebGLMapRenderer implements MapRenderer {
  13. tileStore: TileStore
  14. osmTileStore: TileStore
  15. tileLayer: TileLayer
  16. osmTileLayer: TileLayer
  17. courseLayer: CourseLayer
  18. trackLayer: TrackLayer
  19. gpsLayer: GpsLayer
  20. tileRenderer: WebGLTileRenderer
  21. vectorRenderer: WebGLVectorRenderer
  22. labelRenderer: CourseLabelRenderer
  23. scene: MapScene | null
  24. renderTimer: number
  25. animationTimer: number
  26. destroyed: boolean
  27. animationPaused: boolean
  28. pulseFrame: number
  29. lastStats: MapRendererStats
  30. onStats?: (stats: MapRendererStats) => void
  31. onTileError?: (message: string) => void
  32. constructor(onStats?: (stats: MapRendererStats) => void, onTileError?: (message: string) => void) {
  33. this.onStats = onStats
  34. this.onTileError = onTileError
  35. this.tileStore = new TileStore({
  36. onTileReady: () => {
  37. this.scheduleRender()
  38. },
  39. onTileError: (message) => {
  40. if (this.onTileError) {
  41. this.onTileError(message)
  42. }
  43. this.scheduleRender()
  44. },
  45. } satisfies TileStoreCallbacks)
  46. this.osmTileStore = new TileStore({
  47. onTileReady: () => {
  48. this.scheduleRender()
  49. },
  50. onTileError: () => {
  51. this.scheduleRender()
  52. },
  53. } satisfies TileStoreCallbacks)
  54. this.tileLayer = new TileLayer()
  55. this.osmTileLayer = new TileLayer()
  56. this.courseLayer = new CourseLayer()
  57. this.trackLayer = new TrackLayer()
  58. this.gpsLayer = new GpsLayer()
  59. this.tileRenderer = new WebGLTileRenderer(this.tileLayer, this.tileStore, this.osmTileLayer, this.osmTileStore)
  60. this.vectorRenderer = new WebGLVectorRenderer(this.courseLayer, this.trackLayer, this.gpsLayer)
  61. this.labelRenderer = new CourseLabelRenderer(this.courseLayer)
  62. this.scene = null
  63. this.renderTimer = 0
  64. this.animationTimer = 0
  65. this.destroyed = false
  66. this.animationPaused = false
  67. this.pulseFrame = 0
  68. this.lastStats = {
  69. visibleTileCount: 0,
  70. readyTileCount: 0,
  71. memoryTileCount: 0,
  72. diskTileCount: 0,
  73. memoryHitCount: 0,
  74. diskHitCount: 0,
  75. networkFetchCount: 0,
  76. }
  77. }
  78. attachCanvas(canvasNode: any, width: number, height: number, dpr: number, labelCanvasNode?: any): void {
  79. this.tileRenderer.attachCanvas(canvasNode, width, height, dpr)
  80. this.vectorRenderer.attachContext(this.tileRenderer.gl, canvasNode)
  81. if (labelCanvasNode) {
  82. this.labelRenderer.attachCanvas(labelCanvasNode, width, height, dpr)
  83. }
  84. this.startAnimation()
  85. this.scheduleRender()
  86. }
  87. updateScene(scene: MapScene): void {
  88. this.scene = scene
  89. this.scheduleRender()
  90. }
  91. setAnimationPaused(paused: boolean): void {
  92. this.animationPaused = paused
  93. if (!paused) {
  94. this.scheduleRender()
  95. }
  96. }
  97. destroy(): void {
  98. this.destroyed = true
  99. if (this.renderTimer) {
  100. clearTimeout(this.renderTimer)
  101. this.renderTimer = 0
  102. }
  103. if (this.animationTimer) {
  104. clearTimeout(this.animationTimer)
  105. this.animationTimer = 0
  106. }
  107. this.labelRenderer.destroy()
  108. this.vectorRenderer.destroy()
  109. this.tileRenderer.destroy()
  110. this.tileStore.destroy()
  111. this.osmTileStore.destroy()
  112. this.scene = null
  113. }
  114. startAnimation(): void {
  115. if (this.animationTimer) {
  116. return
  117. }
  118. const tick = () => {
  119. if (this.destroyed) {
  120. this.animationTimer = 0
  121. return
  122. }
  123. if (!this.animationPaused) {
  124. this.pulseFrame = (this.pulseFrame + 1) % 360
  125. this.scheduleRender()
  126. }
  127. this.animationTimer = setTimeout(tick, this.getAnimationFrameMs()) as unknown as number
  128. }
  129. tick()
  130. }
  131. getAnimationFrameMs(): number {
  132. return this.scene && this.scene.animationLevel === 'lite' ? 48 : ANIMATION_FRAME_MS
  133. }
  134. scheduleRender(): void {
  135. if (this.renderTimer || !this.scene || this.destroyed) {
  136. return
  137. }
  138. this.renderTimer = setTimeout(() => {
  139. this.renderTimer = 0
  140. this.renderFrame()
  141. }, RENDER_FRAME_MS) as unknown as number
  142. }
  143. renderFrame(): void {
  144. if (!this.scene) {
  145. return
  146. }
  147. this.tileRenderer.render(this.scene)
  148. this.vectorRenderer.render(this.scene, this.pulseFrame)
  149. this.labelRenderer.render(this.scene)
  150. this.emitStats(this.tileStore.getStats(this.tileLayer.lastVisibleTileCount, this.tileLayer.lastReadyTileCount))
  151. }
  152. emitStats(stats: MapRendererStats): void {
  153. if (
  154. stats.visibleTileCount === this.lastStats.visibleTileCount
  155. && stats.readyTileCount === this.lastStats.readyTileCount
  156. && stats.memoryTileCount === this.lastStats.memoryTileCount
  157. && stats.diskTileCount === this.lastStats.diskTileCount
  158. && stats.memoryHitCount === this.lastStats.memoryHitCount
  159. && stats.diskHitCount === this.lastStats.diskHitCount
  160. && stats.networkFetchCount === this.lastStats.networkFetchCount
  161. ) {
  162. return
  163. }
  164. this.lastStats = stats
  165. if (this.onStats) {
  166. this.onStats(stats)
  167. }
  168. }
  169. }