courseLabelRenderer.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import { type MapScene } from './mapRenderer'
  2. import { CourseLayer } from '../layer/courseLayer'
  3. const EARTH_CIRCUMFERENCE_METERS = 40075016.686
  4. const LABEL_FONT_SIZE_RATIO = 1.08
  5. const LABEL_OFFSET_X_RATIO = 1.18
  6. const LABEL_OFFSET_Y_RATIO = -0.68
  7. export class CourseLabelRenderer {
  8. courseLayer: CourseLayer
  9. canvas: any
  10. ctx: any
  11. dpr: number
  12. width: number
  13. height: number
  14. constructor(courseLayer: CourseLayer) {
  15. this.courseLayer = courseLayer
  16. this.canvas = null
  17. this.ctx = null
  18. this.dpr = 1
  19. this.width = 0
  20. this.height = 0
  21. }
  22. attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
  23. this.canvas = canvasNode
  24. this.ctx = canvasNode.getContext('2d')
  25. this.dpr = dpr || 1
  26. this.width = width
  27. this.height = height
  28. canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
  29. canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
  30. }
  31. destroy(): void {
  32. this.ctx = null
  33. this.canvas = null
  34. this.width = 0
  35. this.height = 0
  36. }
  37. render(scene: MapScene): void {
  38. if (!this.ctx || !this.canvas) {
  39. return
  40. }
  41. const course = this.courseLayer.projectCourse(scene)
  42. const ctx = this.ctx
  43. this.clearCanvas(ctx)
  44. if (!course || !course.controls.length) {
  45. return
  46. }
  47. const controlRadiusMeters = scene.cpRadiusMeters > 0 ? scene.cpRadiusMeters : 5
  48. const fontSizePx = this.getMetric(scene, controlRadiusMeters * LABEL_FONT_SIZE_RATIO)
  49. const offsetX = this.getMetric(scene, controlRadiusMeters * LABEL_OFFSET_X_RATIO)
  50. const offsetY = this.getMetric(scene, controlRadiusMeters * LABEL_OFFSET_Y_RATIO)
  51. this.applyPreviewTransform(ctx, scene)
  52. ctx.save()
  53. ctx.fillStyle = 'rgba(204, 0, 107, 0.98)'
  54. ctx.textAlign = 'left'
  55. ctx.textBaseline = 'middle'
  56. ctx.font = `700 ${fontSizePx}px sans-serif`
  57. for (const control of course.controls) {
  58. ctx.save()
  59. ctx.translate(control.point.x, control.point.y)
  60. ctx.rotate(scene.rotationRad)
  61. ctx.fillText(String(control.sequence), offsetX, offsetY)
  62. ctx.restore()
  63. }
  64. ctx.restore()
  65. }
  66. clearCanvas(ctx: any): void {
  67. ctx.setTransform(1, 0, 0, 1, 0, 0)
  68. ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
  69. }
  70. applyPreviewTransform(ctx: any, scene: MapScene): void {
  71. const previewScale = scene.previewScale || 1
  72. const previewOriginX = scene.previewOriginX || scene.viewportWidth / 2
  73. const previewOriginY = scene.previewOriginY || scene.viewportHeight / 2
  74. const translateX = (previewOriginX - previewOriginX * previewScale) * this.dpr
  75. const translateY = (previewOriginY - previewOriginY * previewScale) * this.dpr
  76. ctx.setTransform(
  77. this.dpr * previewScale,
  78. 0,
  79. 0,
  80. this.dpr * previewScale,
  81. translateX,
  82. translateY,
  83. )
  84. }
  85. getMetric(scene: MapScene, meters: number): number {
  86. return meters * this.getPixelsPerMeter(scene)
  87. }
  88. getPixelsPerMeter(scene: MapScene): number {
  89. const tileSizePx = scene.viewportWidth / scene.visibleColumns
  90. const centerLat = this.worldTileYToLat(scene.exactCenterWorldY, scene.zoom)
  91. const metersPerTile = Math.cos(centerLat * Math.PI / 180) * EARTH_CIRCUMFERENCE_METERS / Math.pow(2, scene.zoom)
  92. if (!tileSizePx || !metersPerTile) {
  93. return 0
  94. }
  95. return tileSizePx / metersPerTile
  96. }
  97. worldTileYToLat(worldY: number, zoom: number): number {
  98. const scale = Math.pow(2, zoom)
  99. const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * worldY / scale)))
  100. return latRad * 180 / Math.PI
  101. }
  102. }