|
|
@@ -1,7 +1,28 @@
|
|
|
+import { getTileSizePx, type CameraState } from '../camera/camera'
|
|
|
+import { worldTileToLonLat } from '../../utils/projection'
|
|
|
import { type MapScene } from './mapRenderer'
|
|
|
+import { CourseLayer, type ProjectedCourseLayers, type ProjectedCourseLeg } from '../layer/courseLayer'
|
|
|
import { TrackLayer } from '../layer/trackLayer'
|
|
|
import { GpsLayer } from '../layer/gpsLayer'
|
|
|
|
|
|
+const COURSE_COLOR: [number, number, number, number] = [0.8, 0.0, 0.42, 0.96]
|
|
|
+const EARTH_CIRCUMFERENCE_METERS = 40075016.686
|
|
|
+const CONTROL_RING_WIDTH_RATIO = 0.2
|
|
|
+const FINISH_INNER_RADIUS_RATIO = 0.6
|
|
|
+const FINISH_RING_WIDTH_RATIO = 0.2
|
|
|
+const START_RING_WIDTH_RATIO = 0.2
|
|
|
+const LEG_WIDTH_RATIO = 0.2
|
|
|
+const LEG_TRIM_TO_RING_CENTER_RATIO = 1 - CONTROL_RING_WIDTH_RATIO / 2
|
|
|
+
|
|
|
+type RgbaColor = [number, number, number, number]
|
|
|
+
|
|
|
+const GUIDE_FLOW_COUNT = 6
|
|
|
+const GUIDE_FLOW_SPEED = 0.022
|
|
|
+const GUIDE_FLOW_MIN_RADIUS_RATIO = 0.14
|
|
|
+const GUIDE_FLOW_MAX_RADIUS_RATIO = 0.34
|
|
|
+const GUIDE_FLOW_OUTER_SCALE = 1.45
|
|
|
+const GUIDE_FLOW_INNER_SCALE = 0.56
|
|
|
+
|
|
|
function createShader(gl: any, type: number, source: string): any {
|
|
|
const shader = gl.createShader(type)
|
|
|
if (!shader) {
|
|
|
@@ -46,6 +67,7 @@ export class WebGLVectorRenderer {
|
|
|
canvas: any
|
|
|
gl: any
|
|
|
dpr: number
|
|
|
+ courseLayer: CourseLayer
|
|
|
trackLayer: TrackLayer
|
|
|
gpsLayer: GpsLayer
|
|
|
program: any
|
|
|
@@ -54,10 +76,11 @@ export class WebGLVectorRenderer {
|
|
|
positionLocation: number
|
|
|
colorLocation: number
|
|
|
|
|
|
- constructor(trackLayer: TrackLayer, gpsLayer: GpsLayer) {
|
|
|
+ constructor(courseLayer: CourseLayer, trackLayer: TrackLayer, gpsLayer: GpsLayer) {
|
|
|
this.canvas = null
|
|
|
this.gl = null
|
|
|
this.dpr = 1
|
|
|
+ this.courseLayer = courseLayer
|
|
|
this.trackLayer = trackLayer
|
|
|
this.gpsLayer = gpsLayer
|
|
|
this.program = null
|
|
|
@@ -123,11 +146,16 @@ export class WebGLVectorRenderer {
|
|
|
}
|
|
|
|
|
|
const gl = this.gl
|
|
|
+ const course = this.courseLayer.projectCourse(scene)
|
|
|
const trackPoints = this.trackLayer.projectPoints(scene)
|
|
|
const gpsPoint = this.gpsLayer.projectPoint(scene)
|
|
|
const positions: number[] = []
|
|
|
const colors: number[] = []
|
|
|
|
|
|
+ if (course) {
|
|
|
+ this.pushCourse(positions, colors, course, scene, pulseFrame)
|
|
|
+ }
|
|
|
+
|
|
|
for (let index = 1; index < trackPoints.length; index += 1) {
|
|
|
this.pushSegment(positions, colors, trackPoints[index - 1], trackPoints[index], 6, [0.09, 0.43, 0.36, 0.96], scene)
|
|
|
}
|
|
|
@@ -163,13 +191,261 @@ export class WebGLVectorRenderer {
|
|
|
gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2)
|
|
|
}
|
|
|
|
|
|
+ getPixelsPerMeter(scene: MapScene): number {
|
|
|
+ const camera: CameraState = {
|
|
|
+ centerWorldX: scene.exactCenterWorldX,
|
|
|
+ centerWorldY: scene.exactCenterWorldY,
|
|
|
+ viewportWidth: scene.viewportWidth,
|
|
|
+ viewportHeight: scene.viewportHeight,
|
|
|
+ visibleColumns: scene.visibleColumns,
|
|
|
+ }
|
|
|
+ const tileSizePx = getTileSizePx(camera)
|
|
|
+ const centerLonLat = worldTileToLonLat({ x: scene.exactCenterWorldX, y: scene.exactCenterWorldY }, scene.zoom)
|
|
|
+ const metersPerTile = Math.cos(centerLonLat.lat * Math.PI / 180) * EARTH_CIRCUMFERENCE_METERS / Math.pow(2, scene.zoom)
|
|
|
+ if (!tileSizePx || !metersPerTile) {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+ return tileSizePx / metersPerTile
|
|
|
+ }
|
|
|
+
|
|
|
+ getMetric(scene: MapScene, meters: number): number {
|
|
|
+ return meters * this.getPixelsPerMeter(scene)
|
|
|
+ }
|
|
|
+
|
|
|
+ getControlRadiusMeters(scene: MapScene): number {
|
|
|
+ return scene.cpRadiusMeters > 0 ? scene.cpRadiusMeters : 5
|
|
|
+ }
|
|
|
+
|
|
|
+ pushCourse(
|
|
|
+ positions: number[],
|
|
|
+ colors: number[],
|
|
|
+ course: ProjectedCourseLayers,
|
|
|
+ scene: MapScene,
|
|
|
+ pulseFrame: number,
|
|
|
+ ): void {
|
|
|
+ const controlRadiusMeters = this.getControlRadiusMeters(scene)
|
|
|
+
|
|
|
+ for (const leg of course.legs) {
|
|
|
+ this.pushCourseLeg(positions, colors, leg, controlRadiusMeters, scene)
|
|
|
+ }
|
|
|
+
|
|
|
+ const guideLeg = this.getGuideLeg(course)
|
|
|
+ if (guideLeg) {
|
|
|
+ this.pushGuidanceFlow(positions, colors, guideLeg, controlRadiusMeters, scene, pulseFrame)
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const start of course.starts) {
|
|
|
+ this.pushStartTriangle(positions, colors, start.point.x, start.point.y, start.headingDeg, controlRadiusMeters, scene)
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const control of course.controls) {
|
|
|
+ this.pushRing(
|
|
|
+ positions,
|
|
|
+ colors,
|
|
|
+ control.point.x,
|
|
|
+ control.point.y,
|
|
|
+ this.getMetric(scene, controlRadiusMeters),
|
|
|
+ this.getMetric(scene, controlRadiusMeters * (1 - CONTROL_RING_WIDTH_RATIO)),
|
|
|
+ COURSE_COLOR,
|
|
|
+ scene,
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const finish of course.finishes) {
|
|
|
+ this.pushRing(
|
|
|
+ positions,
|
|
|
+ colors,
|
|
|
+ finish.point.x,
|
|
|
+ finish.point.y,
|
|
|
+ this.getMetric(scene, controlRadiusMeters),
|
|
|
+ this.getMetric(scene, controlRadiusMeters * (1 - FINISH_RING_WIDTH_RATIO)),
|
|
|
+ COURSE_COLOR,
|
|
|
+ scene,
|
|
|
+ )
|
|
|
+ this.pushRing(
|
|
|
+ positions,
|
|
|
+ colors,
|
|
|
+ finish.point.x,
|
|
|
+ finish.point.y,
|
|
|
+ this.getMetric(scene, controlRadiusMeters * FINISH_INNER_RADIUS_RATIO),
|
|
|
+ this.getMetric(scene, controlRadiusMeters * FINISH_INNER_RADIUS_RATIO * (1 - FINISH_RING_WIDTH_RATIO / FINISH_INNER_RADIUS_RATIO)),
|
|
|
+ COURSE_COLOR,
|
|
|
+ scene,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getGuideLeg(course: ProjectedCourseLayers): ProjectedCourseLeg | null {
|
|
|
+ return course.legs.length ? course.legs[0] : null
|
|
|
+ }
|
|
|
+
|
|
|
+ pushCourseLeg(
|
|
|
+ positions: number[],
|
|
|
+ colors: number[],
|
|
|
+ leg: ProjectedCourseLeg,
|
|
|
+ controlRadiusMeters: number,
|
|
|
+ scene: MapScene,
|
|
|
+ ): void {
|
|
|
+ const trimmed = this.getTrimmedCourseLeg(leg, controlRadiusMeters, scene)
|
|
|
+ if (!trimmed) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.pushSegment(positions, colors, trimmed.from, trimmed.to, this.getMetric(scene, controlRadiusMeters * LEG_WIDTH_RATIO), COURSE_COLOR, scene)
|
|
|
+ }
|
|
|
+
|
|
|
+ pushGuidanceFlow(
|
|
|
+ positions: number[],
|
|
|
+ colors: number[],
|
|
|
+ leg: ProjectedCourseLeg,
|
|
|
+ controlRadiusMeters: number,
|
|
|
+ scene: MapScene,
|
|
|
+ pulseFrame: number,
|
|
|
+ ): void {
|
|
|
+ const trimmed = this.getTrimmedCourseLeg(leg, controlRadiusMeters, scene)
|
|
|
+ if (!trimmed) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const dx = trimmed.to.x - trimmed.from.x
|
|
|
+ const dy = trimmed.to.y - trimmed.from.y
|
|
|
+ const length = Math.sqrt(dx * dx + dy * dy)
|
|
|
+ if (!length) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ for (let index = 0; index < GUIDE_FLOW_COUNT; index += 1) {
|
|
|
+ const progress = (pulseFrame * GUIDE_FLOW_SPEED + index / GUIDE_FLOW_COUNT) % 1
|
|
|
+ const eased = progress * progress
|
|
|
+ const x = trimmed.from.x + dx * progress
|
|
|
+ const y = trimmed.from.y + dy * progress
|
|
|
+ const radius = this.getMetric(
|
|
|
+ scene,
|
|
|
+ controlRadiusMeters * (GUIDE_FLOW_MIN_RADIUS_RATIO + (GUIDE_FLOW_MAX_RADIUS_RATIO - GUIDE_FLOW_MIN_RADIUS_RATIO) * eased),
|
|
|
+ )
|
|
|
+ const outerColor = this.getGuideFlowOuterColor(eased)
|
|
|
+ const innerColor = this.getGuideFlowInnerColor(eased)
|
|
|
+
|
|
|
+ this.pushCircle(positions, colors, x, y, radius * GUIDE_FLOW_OUTER_SCALE, outerColor, scene)
|
|
|
+ this.pushCircle(positions, colors, x, y, radius * GUIDE_FLOW_INNER_SCALE, innerColor, scene)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getTrimmedCourseLeg(
|
|
|
+ leg: ProjectedCourseLeg,
|
|
|
+ controlRadiusMeters: number,
|
|
|
+ scene: MapScene,
|
|
|
+ ): { from: { x: number; y: number }; to: { x: number; y: number } } | null {
|
|
|
+ return this.trimSegment(
|
|
|
+ leg.from,
|
|
|
+ leg.to,
|
|
|
+ this.getLegTrim(leg.fromKind, controlRadiusMeters, scene),
|
|
|
+ this.getLegTrim(leg.toKind, controlRadiusMeters, scene),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ getGuideFlowOuterColor(progress: number): RgbaColor {
|
|
|
+ return [1, 0.18, 0.6, 0.16 + progress * 0.34]
|
|
|
+ }
|
|
|
+
|
|
|
+ getGuideFlowInnerColor(progress: number): RgbaColor {
|
|
|
+ return [1, 0.95, 0.98, 0.3 + progress * 0.54]
|
|
|
+ }
|
|
|
+
|
|
|
+ getLegTrim(kind: ProjectedCourseLeg['fromKind'], controlRadiusMeters: number, scene: MapScene): number {
|
|
|
+ if (kind === 'start') {
|
|
|
+ return this.getMetric(scene, controlRadiusMeters * (1 - START_RING_WIDTH_RATIO / 2))
|
|
|
+ }
|
|
|
+
|
|
|
+ if (kind === 'finish') {
|
|
|
+ return this.getMetric(scene, controlRadiusMeters * (1 - FINISH_RING_WIDTH_RATIO / 2))
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.getMetric(scene, controlRadiusMeters * LEG_TRIM_TO_RING_CENTER_RATIO)
|
|
|
+ }
|
|
|
+
|
|
|
+ trimSegment(
|
|
|
+ from: { x: number; y: number },
|
|
|
+ to: { x: number; y: number },
|
|
|
+ fromTrim: number,
|
|
|
+ toTrim: number,
|
|
|
+ ): { from: { x: number; y: number }; to: { x: number; y: number } } | null {
|
|
|
+ const dx = to.x - from.x
|
|
|
+ const dy = to.y - from.y
|
|
|
+ const length = Math.sqrt(dx * dx + dy * dy)
|
|
|
+ if (!length || length <= fromTrim + toTrim) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ const ux = dx / length
|
|
|
+ const uy = dy / length
|
|
|
+ return {
|
|
|
+ from: {
|
|
|
+ x: from.x + ux * fromTrim,
|
|
|
+ y: from.y + uy * fromTrim,
|
|
|
+ },
|
|
|
+ to: {
|
|
|
+ x: to.x - ux * toTrim,
|
|
|
+ y: to.y - uy * toTrim,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ pushStartTriangle(
|
|
|
+ positions: number[],
|
|
|
+ colors: number[],
|
|
|
+ centerX: number,
|
|
|
+ centerY: number,
|
|
|
+ headingDeg: number | null,
|
|
|
+ controlRadiusMeters: number,
|
|
|
+ scene: MapScene,
|
|
|
+ ): void {
|
|
|
+ const startRadius = this.getMetric(scene, controlRadiusMeters)
|
|
|
+ const startRingWidth = this.getMetric(scene, controlRadiusMeters * START_RING_WIDTH_RATIO)
|
|
|
+ const headingRad = ((headingDeg === null ? 0 : headingDeg) - 90) * Math.PI / 180
|
|
|
+ const vertices = [0, 1, 2].map((index) => {
|
|
|
+ const angle = headingRad + index * (Math.PI * 2 / 3)
|
|
|
+ return {
|
|
|
+ x: centerX + Math.cos(angle) * startRadius,
|
|
|
+ y: centerY + Math.sin(angle) * startRadius,
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ this.pushSegment(positions, colors, vertices[0], vertices[1], startRingWidth, COURSE_COLOR, scene)
|
|
|
+ this.pushSegment(positions, colors, vertices[1], vertices[2], startRingWidth, COURSE_COLOR, scene)
|
|
|
+ this.pushSegment(positions, colors, vertices[2], vertices[0], startRingWidth, COURSE_COLOR, scene)
|
|
|
+ }
|
|
|
+
|
|
|
+ pushRing(
|
|
|
+ positions: number[],
|
|
|
+ colors: number[],
|
|
|
+ centerX: number,
|
|
|
+ centerY: number,
|
|
|
+ outerRadius: number,
|
|
|
+ innerRadius: number,
|
|
|
+ color: RgbaColor,
|
|
|
+ scene: MapScene,
|
|
|
+ ): void {
|
|
|
+ const segments = 36
|
|
|
+ for (let index = 0; index < segments; index += 1) {
|
|
|
+ const startAngle = index / segments * Math.PI * 2
|
|
|
+ const endAngle = (index + 1) / segments * Math.PI * 2
|
|
|
+ const outerStart = this.toClip(centerX + Math.cos(startAngle) * outerRadius, centerY + Math.sin(startAngle) * outerRadius, scene)
|
|
|
+ const outerEnd = this.toClip(centerX + Math.cos(endAngle) * outerRadius, centerY + Math.sin(endAngle) * outerRadius, scene)
|
|
|
+ const innerStart = this.toClip(centerX + Math.cos(startAngle) * innerRadius, centerY + Math.sin(startAngle) * innerRadius, scene)
|
|
|
+ const innerEnd = this.toClip(centerX + Math.cos(endAngle) * innerRadius, centerY + Math.sin(endAngle) * innerRadius, scene)
|
|
|
+ this.pushTriangle(positions, colors, outerStart, outerEnd, innerStart, color)
|
|
|
+ this.pushTriangle(positions, colors, innerStart, outerEnd, innerEnd, color)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
pushSegment(
|
|
|
positions: number[],
|
|
|
colors: number[],
|
|
|
start: { x: number; y: number },
|
|
|
end: { x: number; y: number },
|
|
|
width: number,
|
|
|
- color: [number, number, number, number],
|
|
|
+ color: RgbaColor,
|
|
|
scene: MapScene,
|
|
|
): void {
|
|
|
const deltaX = end.x - start.x
|
|
|
@@ -196,7 +472,7 @@ export class WebGLVectorRenderer {
|
|
|
centerX: number,
|
|
|
centerY: number,
|
|
|
radius: number,
|
|
|
- color: [number, number, number, number],
|
|
|
+ color: RgbaColor,
|
|
|
scene: MapScene,
|
|
|
): void {
|
|
|
const segments = 20
|
|
|
@@ -216,7 +492,7 @@ export class WebGLVectorRenderer {
|
|
|
first: { x: number; y: number },
|
|
|
second: { x: number; y: number },
|
|
|
third: { x: number; y: number },
|
|
|
- color: [number, number, number, number],
|
|
|
+ color: RgbaColor,
|
|
|
): void {
|
|
|
positions.push(first.x, first.y, second.x, second.y, third.x, third.y)
|
|
|
for (let index = 0; index < 3; index += 1) {
|
|
|
@@ -237,3 +513,5 @@ export class WebGLVectorRenderer {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+
|