248 lines
6.1 KiB
TypeScript
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))
|
|
}
|
|
}
|