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

235 lines
6.8 KiB
TypeScript

import { CourseLayer } from '../layer/courseLayer'
import { TrackLayer } from '../layer/trackLayer'
import { GpsLayer } from '../layer/gpsLayer'
import { TileLayer } from '../layer/tileLayer'
import { TileStore, type TileStoreCallbacks } from '../tile/tileStore'
import { type MapRenderer, type MapRendererStats, type MapScene } from './mapRenderer'
import { WebGLTileRenderer } from './webglTileRenderer'
import { WebGLVectorRenderer } from './webglVectorRenderer'
import { CourseLabelRenderer } from './courseLabelRenderer'
import { type MockSimulatorDebugLogLevel } from '../debug/mockSimulatorDebugLogger'
const RENDER_FRAME_MS = 16
const ANIMATION_FRAME_MS = 33
export class WebGLMapRenderer implements MapRenderer {
tileStore: TileStore
osmTileStore: TileStore
tileLayer: TileLayer
osmTileLayer: TileLayer
courseLayer: CourseLayer
trackLayer: TrackLayer
gpsLayer: GpsLayer
tileRenderer: WebGLTileRenderer
vectorRenderer: WebGLVectorRenderer
labelRenderer: CourseLabelRenderer
scene: MapScene | null
renderTimer: number
animationTimer: number
destroyed: boolean
animationPaused: boolean
pulseFrame: number
lastStats: MapRendererStats
lastGpsLogoDebugInfo: { status: string; url: string; resolvedSrc: string }
onStats?: (stats: MapRendererStats) => void
onTileError?: (message: string) => void
onGpsLogoDebug?: (info: { status: string; url: string; resolvedSrc: string }) => void
onDebugLog?: (
scope: string,
level: MockSimulatorDebugLogLevel,
message: string,
payload?: Record<string, unknown>,
) => void
constructor(
onStats?: (stats: MapRendererStats) => void,
onTileError?: (message: string) => void,
onGpsLogoDebug?: (info: { status: string; url: string; resolvedSrc: string }) => void,
onDebugLog?: (
scope: string,
level: MockSimulatorDebugLogLevel,
message: string,
payload?: Record<string, unknown>,
) => void,
) {
this.onStats = onStats
this.onTileError = onTileError
this.onGpsLogoDebug = onGpsLogoDebug
this.onDebugLog = onDebugLog
this.tileStore = new TileStore({
onTileReady: () => {
this.scheduleRender()
},
onTileError: (message) => {
if (this.onTileError) {
this.onTileError(message)
}
this.scheduleRender()
},
} satisfies TileStoreCallbacks)
this.osmTileStore = new TileStore({
onTileReady: () => {
this.scheduleRender()
},
onTileError: () => {
this.scheduleRender()
},
} satisfies TileStoreCallbacks)
this.tileLayer = new TileLayer()
this.osmTileLayer = new TileLayer()
this.courseLayer = new CourseLayer()
this.trackLayer = new TrackLayer()
this.gpsLayer = new GpsLayer()
this.tileRenderer = new WebGLTileRenderer(this.tileLayer, this.tileStore, this.osmTileLayer, this.osmTileStore)
this.vectorRenderer = new WebGLVectorRenderer(this.courseLayer, this.trackLayer, this.gpsLayer)
this.labelRenderer = new CourseLabelRenderer(this.courseLayer, onDebugLog)
this.scene = null
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,
}
this.lastGpsLogoDebugInfo = {
status: 'idle',
url: '',
resolvedSrc: '',
}
}
attachCanvas(canvasNode: any, width: number, height: number, dpr: number, labelCanvasNode?: any): void {
this.tileRenderer.attachCanvas(canvasNode, width, height, dpr)
this.vectorRenderer.attachContext(this.tileRenderer.gl, canvasNode)
if (labelCanvasNode) {
this.labelRenderer.attachCanvas(labelCanvasNode, width, height, dpr)
}
this.startAnimation()
this.scheduleRender()
}
updateScene(scene: MapScene): 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.labelRenderer.destroy()
this.vectorRenderer.destroy()
this.tileRenderer.destroy()
this.tileStore.destroy()
this.osmTileStore.destroy()
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, this.getAnimationFrameMs()) as unknown as number
}
tick()
}
getAnimationFrameMs(): number {
return this.scene && this.scene.animationLevel === 'lite' ? 48 : ANIMATION_FRAME_MS
}
scheduleRender(): void {
if (this.renderTimer || !this.scene || this.destroyed) {
return
}
this.renderTimer = setTimeout(() => {
this.renderTimer = 0
this.renderFrame()
}, RENDER_FRAME_MS) as unknown as number
}
renderFrame(): void {
if (!this.scene) {
return
}
this.tileRenderer.render(this.scene)
this.vectorRenderer.render(this.scene, this.pulseFrame)
this.labelRenderer.render(this.scene)
this.emitGpsLogoDebug(this.labelRenderer.getGpsLogoDebugInfo())
this.emitStats(this.tileStore.getStats(this.tileLayer.lastVisibleTileCount, this.tileLayer.lastReadyTileCount))
}
getGpsLogoDebugInfo(): { status: string; url: string; resolvedSrc: string } {
return this.labelRenderer.getGpsLogoDebugInfo()
}
emitStats(stats: MapRendererStats): 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)
}
}
emitGpsLogoDebug(info: { status: string; url: string; resolvedSrc: string }): void {
if (
info.status === this.lastGpsLogoDebugInfo.status
&& info.url === this.lastGpsLogoDebugInfo.url
&& info.resolvedSrc === this.lastGpsLogoDebugInfo.resolvedSrc
) {
return
}
this.lastGpsLogoDebugInfo = info
if (this.onGpsLogoDebug) {
this.onGpsLogoDebug(info)
}
}
}