| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- import { getTileSizePx, type CameraState } from '../camera/camera'
- import {
- TileStore,
- type TileStoreCallbacks,
- type TileStoreStats,
- } from '../tile/tileStore'
- import { type LonLatPoint } from '../../utils/projection'
- import { type MapLayer } from '../layer/mapLayer'
- import { TileLayer } from '../layer/tileLayer'
- import { TrackLayer } from '../layer/trackLayer'
- import { GpsLayer } from '../layer/gpsLayer'
- const RENDER_FRAME_MS = 16
- export interface CanvasMapScene {
- tileSource: string
- zoom: number
- centerTileX: number
- centerTileY: number
- viewportWidth: number
- viewportHeight: number
- visibleColumns: number
- overdraw: number
- translateX: number
- translateY: number
- rotationRad: number
- previewScale: number
- previewOriginX: number
- previewOriginY: number
- track: LonLatPoint[]
- gpsPoint: LonLatPoint
- }
- export type CanvasMapRendererStats = TileStoreStats
- function buildCamera(scene: CanvasMapScene): CameraState {
- return {
- centerWorldX: scene.centerTileX,
- centerWorldY: scene.centerTileY,
- viewportWidth: scene.viewportWidth,
- viewportHeight: scene.viewportHeight,
- visibleColumns: scene.visibleColumns,
- rotationRad: scene.rotationRad,
- }
- }
- export class CanvasMapRenderer {
- canvas: any
- ctx: any
- dpr: number
- scene: CanvasMapScene | null
- tileStore: TileStore
- tileLayer: TileLayer
- layers: MapLayer[]
- renderTimer: number
- animationTimer: number
- destroyed: boolean
- animationPaused: boolean
- pulseFrame: number
- lastStats: CanvasMapRendererStats
- onStats?: (stats: CanvasMapRendererStats) => void
- onTileError?: (message: string) => void
- constructor(
- onStats?: (stats: CanvasMapRendererStats) => void,
- onTileError?: (message: string) => void,
- ) {
- this.onStats = onStats
- this.onTileError = onTileError
- this.canvas = null
- this.ctx = null
- this.dpr = 1
- this.scene = null
- this.tileStore = new TileStore({
- onTileReady: () => {
- this.scheduleRender()
- },
- onTileError: (message) => {
- if (this.onTileError) {
- this.onTileError(message)
- }
- this.scheduleRender()
- },
- } satisfies TileStoreCallbacks)
- this.tileLayer = new TileLayer()
- this.layers = [
- this.tileLayer,
- new TrackLayer(),
- new GpsLayer(),
- ]
- this.renderTimer = 0
- this.animationTimer = 0
- this.destroyed = false
- this.animationPaused = false
- this.pulseFrame = 0
- this.lastStats = {
- visibleTileCount: 0,
- readyTileCount: 0,
- memoryTileCount: 0,
- diskTileCount: 0,
- memoryHitCount: 0,
- diskHitCount: 0,
- networkFetchCount: 0,
- }
- }
- attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
- this.canvas = canvasNode
- this.ctx = canvasNode.getContext('2d')
- this.dpr = dpr || 1
- canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
- canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
- if (typeof this.ctx.setTransform === 'function') {
- this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0)
- } else {
- this.ctx.scale(this.dpr, this.dpr)
- }
- this.tileStore.attachCanvas(canvasNode)
- this.startAnimation()
- this.scheduleRender()
- }
- updateScene(scene: CanvasMapScene): void {
- this.scene = scene
- this.scheduleRender()
- }
- setAnimationPaused(paused: boolean): void {
- this.animationPaused = paused
- if (!paused) {
- this.scheduleRender()
- }
- }
- destroy(): void {
- this.destroyed = true
- if (this.renderTimer) {
- clearTimeout(this.renderTimer)
- this.renderTimer = 0
- }
- if (this.animationTimer) {
- clearTimeout(this.animationTimer)
- this.animationTimer = 0
- }
- this.tileStore.destroy()
- this.canvas = null
- this.ctx = null
- this.scene = null
- }
- startAnimation(): void {
- if (this.animationTimer) {
- return
- }
- const tick = () => {
- if (this.destroyed) {
- this.animationTimer = 0
- return
- }
- if (!this.animationPaused) {
- this.pulseFrame = (this.pulseFrame + 1) % 360
- this.scheduleRender()
- }
- this.animationTimer = setTimeout(tick, 33) as unknown as number
- }
- tick()
- }
- scheduleRender(): void {
- if (this.renderTimer || !this.ctx || !this.scene || this.destroyed) {
- return
- }
- this.renderTimer = setTimeout(() => {
- this.renderTimer = 0
- this.renderFrame()
- }, RENDER_FRAME_MS) as unknown as number
- }
- emitStats(stats: CanvasMapRendererStats): void {
- if (
- stats.visibleTileCount === this.lastStats.visibleTileCount
- && stats.readyTileCount === this.lastStats.readyTileCount
- && stats.memoryTileCount === this.lastStats.memoryTileCount
- && stats.diskTileCount === this.lastStats.diskTileCount
- && stats.memoryHitCount === this.lastStats.memoryHitCount
- && stats.diskHitCount === this.lastStats.diskHitCount
- && stats.networkFetchCount === this.lastStats.networkFetchCount
- ) {
- return
- }
- this.lastStats = stats
- if (this.onStats) {
- this.onStats(stats)
- }
- }
- renderFrame(): void {
- if (!this.ctx || !this.scene) {
- return
- }
- const scene = this.scene
- const ctx = this.ctx
- const camera = buildCamera(scene)
- const tileSize = getTileSizePx(camera)
- ctx.clearRect(0, 0, scene.viewportWidth, scene.viewportHeight)
- ctx.fillStyle = '#dbeed4'
- ctx.fillRect(0, 0, scene.viewportWidth, scene.viewportHeight)
- if (!tileSize) {
- this.emitStats(this.tileStore.getStats(0, 0))
- return
- }
- const previewScale = scene.previewScale || 1
- const previewOriginX = scene.previewOriginX || scene.viewportWidth / 2
- const previewOriginY = scene.previewOriginY || scene.viewportHeight / 2
- ctx.save()
- ctx.translate(previewOriginX, previewOriginY)
- ctx.scale(previewScale, previewScale)
- ctx.translate(-previewOriginX, -previewOriginY)
- for (const layer of this.layers) {
- layer.draw({
- ctx,
- camera,
- scene,
- pulseFrame: this.pulseFrame,
- tileStore: this.tileStore,
- })
- }
- ctx.restore()
- this.emitStats(this.tileStore.getStats(this.tileLayer.lastVisibleTileCount, this.tileLayer.lastReadyTileCount))
- }
- }
|