Files
cmr-mini/miniprogram/engine/renderer/canvasMapRenderer.ts

248 lines
6.1 KiB
TypeScript

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))
}
}