canvasMapRenderer.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import { getTileSizePx, type CameraState } from '../camera/camera'
  2. import {
  3. TileStore,
  4. type TileStoreCallbacks,
  5. type TileStoreStats,
  6. } from '../tile/tileStore'
  7. import { type LonLatPoint } from '../../utils/projection'
  8. import { type MapLayer } from '../layer/mapLayer'
  9. import { TileLayer } from '../layer/tileLayer'
  10. import { TrackLayer } from '../layer/trackLayer'
  11. import { GpsLayer } from '../layer/gpsLayer'
  12. const RENDER_FRAME_MS = 16
  13. export interface CanvasMapScene {
  14. tileSource: string
  15. zoom: number
  16. centerTileX: number
  17. centerTileY: number
  18. viewportWidth: number
  19. viewportHeight: number
  20. visibleColumns: number
  21. overdraw: number
  22. translateX: number
  23. translateY: number
  24. rotationRad: number
  25. previewScale: number
  26. previewOriginX: number
  27. previewOriginY: number
  28. track: LonLatPoint[]
  29. gpsPoint: LonLatPoint
  30. }
  31. export type CanvasMapRendererStats = TileStoreStats
  32. function buildCamera(scene: CanvasMapScene): CameraState {
  33. return {
  34. centerWorldX: scene.centerTileX,
  35. centerWorldY: scene.centerTileY,
  36. viewportWidth: scene.viewportWidth,
  37. viewportHeight: scene.viewportHeight,
  38. visibleColumns: scene.visibleColumns,
  39. rotationRad: scene.rotationRad,
  40. }
  41. }
  42. export class CanvasMapRenderer {
  43. canvas: any
  44. ctx: any
  45. dpr: number
  46. scene: CanvasMapScene | null
  47. tileStore: TileStore
  48. tileLayer: TileLayer
  49. layers: MapLayer[]
  50. renderTimer: number
  51. animationTimer: number
  52. destroyed: boolean
  53. animationPaused: boolean
  54. pulseFrame: number
  55. lastStats: CanvasMapRendererStats
  56. onStats?: (stats: CanvasMapRendererStats) => void
  57. onTileError?: (message: string) => void
  58. constructor(
  59. onStats?: (stats: CanvasMapRendererStats) => void,
  60. onTileError?: (message: string) => void,
  61. ) {
  62. this.onStats = onStats
  63. this.onTileError = onTileError
  64. this.canvas = null
  65. this.ctx = null
  66. this.dpr = 1
  67. this.scene = null
  68. this.tileStore = new TileStore({
  69. onTileReady: () => {
  70. this.scheduleRender()
  71. },
  72. onTileError: (message) => {
  73. if (this.onTileError) {
  74. this.onTileError(message)
  75. }
  76. this.scheduleRender()
  77. },
  78. } satisfies TileStoreCallbacks)
  79. this.tileLayer = new TileLayer()
  80. this.layers = [
  81. this.tileLayer,
  82. new TrackLayer(),
  83. new GpsLayer(),
  84. ]
  85. this.renderTimer = 0
  86. this.animationTimer = 0
  87. this.destroyed = false
  88. this.animationPaused = false
  89. this.pulseFrame = 0
  90. this.lastStats = {
  91. visibleTileCount: 0,
  92. readyTileCount: 0,
  93. memoryTileCount: 0,
  94. diskTileCount: 0,
  95. memoryHitCount: 0,
  96. diskHitCount: 0,
  97. networkFetchCount: 0,
  98. }
  99. }
  100. attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
  101. this.canvas = canvasNode
  102. this.ctx = canvasNode.getContext('2d')
  103. this.dpr = dpr || 1
  104. canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
  105. canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
  106. if (typeof this.ctx.setTransform === 'function') {
  107. this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0)
  108. } else {
  109. this.ctx.scale(this.dpr, this.dpr)
  110. }
  111. this.tileStore.attachCanvas(canvasNode)
  112. this.startAnimation()
  113. this.scheduleRender()
  114. }
  115. updateScene(scene: CanvasMapScene): void {
  116. this.scene = scene
  117. this.scheduleRender()
  118. }
  119. setAnimationPaused(paused: boolean): void {
  120. this.animationPaused = paused
  121. if (!paused) {
  122. this.scheduleRender()
  123. }
  124. }
  125. destroy(): void {
  126. this.destroyed = true
  127. if (this.renderTimer) {
  128. clearTimeout(this.renderTimer)
  129. this.renderTimer = 0
  130. }
  131. if (this.animationTimer) {
  132. clearTimeout(this.animationTimer)
  133. this.animationTimer = 0
  134. }
  135. this.tileStore.destroy()
  136. this.canvas = null
  137. this.ctx = null
  138. this.scene = null
  139. }
  140. startAnimation(): void {
  141. if (this.animationTimer) {
  142. return
  143. }
  144. const tick = () => {
  145. if (this.destroyed) {
  146. this.animationTimer = 0
  147. return
  148. }
  149. if (!this.animationPaused) {
  150. this.pulseFrame = (this.pulseFrame + 1) % 360
  151. this.scheduleRender()
  152. }
  153. this.animationTimer = setTimeout(tick, 33) as unknown as number
  154. }
  155. tick()
  156. }
  157. scheduleRender(): void {
  158. if (this.renderTimer || !this.ctx || !this.scene || this.destroyed) {
  159. return
  160. }
  161. this.renderTimer = setTimeout(() => {
  162. this.renderTimer = 0
  163. this.renderFrame()
  164. }, RENDER_FRAME_MS) as unknown as number
  165. }
  166. emitStats(stats: CanvasMapRendererStats): void {
  167. if (
  168. stats.visibleTileCount === this.lastStats.visibleTileCount
  169. && stats.readyTileCount === this.lastStats.readyTileCount
  170. && stats.memoryTileCount === this.lastStats.memoryTileCount
  171. && stats.diskTileCount === this.lastStats.diskTileCount
  172. && stats.memoryHitCount === this.lastStats.memoryHitCount
  173. && stats.diskHitCount === this.lastStats.diskHitCount
  174. && stats.networkFetchCount === this.lastStats.networkFetchCount
  175. ) {
  176. return
  177. }
  178. this.lastStats = stats
  179. if (this.onStats) {
  180. this.onStats(stats)
  181. }
  182. }
  183. renderFrame(): void {
  184. if (!this.ctx || !this.scene) {
  185. return
  186. }
  187. const scene = this.scene
  188. const ctx = this.ctx
  189. const camera = buildCamera(scene)
  190. const tileSize = getTileSizePx(camera)
  191. ctx.clearRect(0, 0, scene.viewportWidth, scene.viewportHeight)
  192. ctx.fillStyle = '#dbeed4'
  193. ctx.fillRect(0, 0, scene.viewportWidth, scene.viewportHeight)
  194. if (!tileSize) {
  195. this.emitStats(this.tileStore.getStats(0, 0))
  196. return
  197. }
  198. const previewScale = scene.previewScale || 1
  199. const previewOriginX = scene.previewOriginX || scene.viewportWidth / 2
  200. const previewOriginY = scene.previewOriginY || scene.viewportHeight / 2
  201. ctx.save()
  202. ctx.translate(previewOriginX, previewOriginY)
  203. ctx.scale(previewScale, previewScale)
  204. ctx.translate(-previewOriginX, -previewOriginY)
  205. for (const layer of this.layers) {
  206. layer.draw({
  207. ctx,
  208. camera,
  209. scene,
  210. pulseFrame: this.pulseFrame,
  211. tileStore: this.tileStore,
  212. })
  213. }
  214. ctx.restore()
  215. this.emitStats(this.tileStore.getStats(this.tileLayer.lastVisibleTileCount, this.tileLayer.lastReadyTileCount))
  216. }
  217. }