import { type MapScene } from './mapRenderer' import { CourseLayer } from '../layer/courseLayer' const EARTH_CIRCUMFERENCE_METERS = 40075016.686 const LABEL_FONT_SIZE_RATIO = 1.08 const LABEL_OFFSET_X_RATIO = 1.18 const LABEL_OFFSET_Y_RATIO = -0.68 const DEFAULT_LABEL_COLOR = 'rgba(204, 0, 107, 0.98)' const ACTIVE_LABEL_COLOR = 'rgba(255, 219, 54, 0.98)' const COMPLETED_LABEL_COLOR = 'rgba(126, 131, 138, 0.94)' export class CourseLabelRenderer { courseLayer: CourseLayer canvas: any ctx: any dpr: number width: number height: number constructor(courseLayer: CourseLayer) { this.courseLayer = courseLayer this.canvas = null this.ctx = null this.dpr = 1 this.width = 0 this.height = 0 } attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void { this.canvas = canvasNode this.ctx = canvasNode.getContext('2d') this.dpr = dpr || 1 this.width = width this.height = height canvasNode.width = Math.max(1, Math.floor(width * this.dpr)) canvasNode.height = Math.max(1, Math.floor(height * this.dpr)) } destroy(): void { this.ctx = null this.canvas = null this.width = 0 this.height = 0 } render(scene: MapScene): void { if (!this.ctx || !this.canvas) { return } const course = this.courseLayer.projectCourse(scene) const ctx = this.ctx this.clearCanvas(ctx) if (!course || !course.controls.length || !scene.revealFullCourse) { return } const controlRadiusMeters = scene.cpRadiusMeters > 0 ? scene.cpRadiusMeters : 5 const fontSizePx = this.getMetric(scene, controlRadiusMeters * LABEL_FONT_SIZE_RATIO) const offsetX = this.getMetric(scene, controlRadiusMeters * LABEL_OFFSET_X_RATIO) const offsetY = this.getMetric(scene, controlRadiusMeters * LABEL_OFFSET_Y_RATIO) this.applyPreviewTransform(ctx, scene) ctx.save() ctx.textAlign = 'left' ctx.textBaseline = 'middle' ctx.font = `700 ${fontSizePx}px sans-serif` for (const control of course.controls) { ctx.save() ctx.fillStyle = this.getLabelColor(scene, control.sequence) ctx.translate(control.point.x, control.point.y) ctx.rotate(scene.rotationRad) ctx.fillText(String(control.sequence), offsetX, offsetY) ctx.restore() } ctx.restore() } getLabelColor(scene: MapScene, sequence: number): string { if (scene.activeControlSequences.includes(sequence)) { return ACTIVE_LABEL_COLOR } if (scene.completedControlSequences.includes(sequence)) { return COMPLETED_LABEL_COLOR } return DEFAULT_LABEL_COLOR } clearCanvas(ctx: any): void { ctx.setTransform(1, 0, 0, 1, 0, 0) ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) } applyPreviewTransform(ctx: any, scene: MapScene): void { const previewScale = scene.previewScale || 1 const previewOriginX = scene.previewOriginX || scene.viewportWidth / 2 const previewOriginY = scene.previewOriginY || scene.viewportHeight / 2 const translateX = (previewOriginX - previewOriginX * previewScale) * this.dpr const translateY = (previewOriginY - previewOriginY * previewScale) * this.dpr ctx.setTransform( this.dpr * previewScale, 0, 0, this.dpr * previewScale, translateX, translateY, ) } getMetric(scene: MapScene, meters: number): number { return meters * this.getPixelsPerMeter(scene) } getPixelsPerMeter(scene: MapScene): number { const tileSizePx = scene.viewportWidth / scene.visibleColumns const centerLat = this.worldTileYToLat(scene.exactCenterWorldY, scene.zoom) const metersPerTile = Math.cos(centerLat * Math.PI / 180) * EARTH_CIRCUMFERENCE_METERS / Math.pow(2, scene.zoom) if (!tileSizePx || !metersPerTile) { return 0 } return tileSizePx / metersPerTile } worldTileYToLat(worldY: number, zoom: number): number { const scale = Math.pow(2, zoom) const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * worldY / scale))) return latRad * 180 / Math.PI } }