235 lines
6.8 KiB
TypeScript
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)
|
|
}
|
|
}
|
|
}
|