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' 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 onStats?: (stats: MapRendererStats) => void onTileError?: (message: string) => void constructor(onStats?: (stats: MapRendererStats) => void, onTileError?: (message: string) => void) { this.onStats = onStats this.onTileError = onTileError 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) 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, } } 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.emitStats(this.tileStore.getStats(this.tileLayer.lastVisibleTileCount, this.tileLayer.lastReadyTileCount)) } 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) } } }