function initWebGLContext( canvas: HTMLCanvasElement ): WebGLRenderingContext | null { const gl = (canvas.getContext("webgl") as WebGLRenderingContext | null) || (canvas.getContext("experimental-webgl") as WebGLRenderingContext | null); return gl; } function resizeCanvasToDisplaySize(canvas: HTMLCanvasElement): boolean { const dpr = Math.max(1, window.devicePixelRatio || 1); const displayWidth = Math.floor(canvas.clientWidth * dpr); const displayHeight = Math.floor(canvas.clientHeight * dpr); if (canvas.width !== displayWidth || canvas.height !== displayHeight) { canvas.width = displayWidth; canvas.height = displayHeight; return true; } return false; } export const WebGLMap = observer(() => { const canvasRef = useRef(null); const glRef = useRef(null); const programRef = useRef(null); const bufferRef = useRef(null); const pointProgramRef = useRef(null); const pointBufferRef = useRef(null); const screenLineProgramRef = useRef(null); const screenLineBufferRef = useRef(null); const attribsRef = useRef<{ a_pos: number } | null>(null); const uniformsRef = useRef<{ u_cameraPos: WebGLUniformLocation | null; u_scale: WebGLUniformLocation | null; u_resolution: WebGLUniformLocation | null; u_color: WebGLUniformLocation | null; } | null>(null); const { routeData, stationData, stationDataEn, stationDataZh, sightData } = useMapData() as any; const { position, scale, setPosition, setScale, isAutoMode, setIsAutoMode, screenCenter, setScreenCenter, userActivityTimestamp, updateUserActivity, } = useTransform(); const cameraAnimationStore = useCameraAnimationStore(); const scaleLimitsRef = useRef({ min: null as number | null, max: null as number | null, }); useEffect(() => { if ( routeData?.scale_min !== undefined && routeData?.scale_max !== undefined ) { scaleLimitsRef.current = { min: routeData.scale_min / 10, max: routeData.scale_max / 10, }; } }, [routeData?.scale_min, routeData?.scale_max]); const clampScale = useCallback((value: number) => { const { min, max } = scaleLimitsRef.current; if (min === null || max === null) { return value; } const clampedValue = Math.max(min, Math.min(max, value)); return clampedValue; }, []); const { selectedLanguage } = useGeolocationStore(); const positionRef = useRef(position); const scaleRef = useRef(scale); const setPositionRef = useRef(setPosition); const setScaleRef = useRef(setScale); useEffect(() => { setPositionRef.current = setPosition; }, [setPosition]); useEffect(() => { setScaleRef.current = setScale; }, [setScale]); useEffect(() => { if (routeData) { } }, [routeData]); useEffect(() => { positionRef.current = position; }, [position]); useEffect(() => { scaleRef.current = scale; }, [scale]); const rotationAngle = useMemo(() => { const deg = (routeData as any)?.rotate ?? 0; return (deg * Math.PI) / 180; }, [routeData]); const { position: animatedYellowDotPosition, animateTo: animateYellowDotTo, setPositionImmediate: setYellowDotPositionImmediate, } = useAnimatedPolarPosition(0, 0, 800); const routePath = useMemo(() => { if (!routeData?.path || routeData?.path.length === 0) return new Float32Array(); const centerLat = routeData.center_latitude; const centerLon = routeData.center_longitude; if (centerLat === undefined || centerLon === undefined) return new Float32Array(); const cos = Math.cos(rotationAngle); const sin = Math.sin(rotationAngle); const verts: number[] = []; for (const [lat, lon] of routeData.path) { const local = coordinatesToLocal(lat - centerLat, lon - centerLon); const x = local.x * UP_SCALE; const y = local.y * UP_SCALE; const rx = x * cos - y * sin; const ry = x * sin + y * cos; verts.push(rx, ry); } return new Float32Array(verts); }, [ routeData?.path, routeData?.center_latitude, routeData?.center_longitude, rotationAngle, ]); const transformedTramCoords = useMemo(() => { const centerLat = routeData?.center_latitude; const centerLon = routeData?.center_longitude; if (centerLat === undefined || centerLon === undefined) return null; const coords: any = apiStore?.context?.currentCoordinates; if (!coords) return null; const local = coordinatesToLocal( coords.latitude - centerLat, coords.longitude - centerLon ); const x = local.x * UP_SCALE; const y = local.y * UP_SCALE; const cos = Math.cos(rotationAngle); const sin = Math.sin(rotationAngle); const rx = x * cos - y * sin; const ry = x * sin + y * cos; return { x: rx, y: ry }; }, [ routeData?.center_latitude, routeData?.center_longitude, apiStore?.context?.currentCoordinates, rotationAngle, ]); useEffect(() => { const callback = (newPos: { x: number; y: number }, newZoom: number) => { setPosition(newPos); setScale(newZoom); }; cameraAnimationStore.setUpdateCallback(callback); cameraAnimationStore.syncState(position, scale); return () => { cameraAnimationStore.setUpdateCallback(null); }; }, []); useEffect(() => { if ( routeData?.scale_min !== undefined && routeData?.scale_max !== undefined ) { cameraAnimationStore.setMaxZoom(routeData.scale_max / SCALE_FACTOR); cameraAnimationStore.setMinZoom(routeData.scale_min / SCALE_FACTOR); } }, [routeData?.scale_min, routeData?.scale_max, cameraAnimationStore]); useEffect(() => { const interval = setInterval(() => { const timeSinceActivity = Date.now() - userActivityTimestamp; if (timeSinceActivity >= 5000 && !isAutoMode) { setIsAutoMode(true); } }, 1000); return () => clearInterval(interval); }, [userActivityTimestamp, isAutoMode, setIsAutoMode]); useEffect(() => { if (cameraAnimationStore.isActivelyAnimating) { return; } if (isAutoMode && transformedTramCoords && screenCenter) { const transformedStations = stationData ? stationData .map((station: any) => { const centerLat = routeData?.center_latitude; const centerLon = routeData?.center_longitude; if (centerLat === undefined || centerLon === undefined) return null; const local = coordinatesToLocal( station.latitude - centerLat, station.longitude - centerLon ); const x = local.x * UP_SCALE; const y = local.y * UP_SCALE; const cos = Math.cos(rotationAngle); const sin = Math.sin(rotationAngle); const rx = x * cos - y * sin; const ry = x * sin + y * cos; return { longitude: rx, latitude: ry, id: station.id, }; }) .filter(Boolean) : []; if ( transformedTramCoords && screenCenter && transformedStations && scaleLimitsRef.current !== null && scaleLimitsRef.current.max !== null && scaleLimitsRef.current.min && scaleLimitsRef.current.min !== null ) { cameraAnimationStore.setMaxZoom(scaleLimitsRef.current!.max); cameraAnimationStore.setMinZoom(scaleLimitsRef.current!.min); cameraAnimationStore.syncState(positionRef.current, scaleRef.current); cameraAnimationStore.followTram( transformedTramCoords, screenCenter, transformedStations ); } } else if (!isAutoMode) { cameraAnimationStore.stopAnimation(); } }, [ isAutoMode, transformedTramCoords, screenCenter, cameraAnimationStore, stationData, routeData, rotationAngle, ]); const stationLabels = useMemo(() => { if (!stationData || !routeData) return [] as Array<{ x: number; y: number; name: string; sub?: string }>; const centerLat = routeData.center_latitude; const centerLon = routeData.center_longitude; if (centerLat === undefined || centerLon === undefined) return []; const cos = Math.cos(rotationAngle); const sin = Math.sin(rotationAngle); const result: Array<{ x: number; y: number; name: string; sub?: string }> = []; for (let i = 0; i < stationData.length; i++) { const st = stationData[i]; const local = coordinatesToLocal( st.latitude - centerLat, st.longitude - centerLon ); const x = local.x * UP_SCALE; const y = local.y * UP_SCALE; const rx = x * cos - y * sin; const ry = x * sin + y * cos; const DEFAULT_LABEL_OFFSET_X = 25; const DEFAULT_LABEL_OFFSET_Y = 0; const labelOffsetX = st.offset_x === 0 && st.offset_y === 0 ? DEFAULT_LABEL_OFFSET_X : st.offset_x; const labelOffsetY = st.offset_x === 0 && st.offset_y === 0 ? DEFAULT_LABEL_OFFSET_Y : st.offset_y; const textBlockPositionX = rx + labelOffsetX; const textBlockPositionY = ry + labelOffsetY; const dpr = Math.max( 1, (typeof window !== "undefined" && window.devicePixelRatio) || 1 ); const sx = (textBlockPositionX * scale + position.x) / dpr; const sy = (textBlockPositionY * scale + position.y) / dpr; let sub: string | undefined; if ((selectedLanguage as any) === "zh") sub = (stationDataZh as any)?.[i]?.name; else if ( (selectedLanguage as any) === "en" || (selectedLanguage as any) === "ru" ) sub = (stationDataEn as any)?.[i]?.name; result.push({ x: sx, y: sy, name: st.name, sub }); } return result; }, [ stationData, stationDataEn as any, stationDataZh as any, position.x, position.y, scale, routeData?.center_latitude, routeData?.center_longitude, rotationAngle, selectedLanguage as any, ]); const stationPoints = useMemo(() => { if (!stationData || !routeData) return new Float32Array(); const centerLat = routeData.center_latitude; const centerLon = routeData.center_longitude; if (centerLat === undefined || centerLon === undefined) return new Float32Array(); const cos = Math.cos(rotationAngle); const sin = Math.sin(rotationAngle); const verts: number[] = []; for (const s of stationData) { const local = coordinatesToLocal( s.latitude - centerLat, s.longitude - centerLon ); const x = local.x * UP_SCALE; const y = local.y * UP_SCALE; const rx = x * cos - y * sin; const ry = x * sin + y * cos; verts.push(rx, ry); } return new Float32Array(verts); }, [ stationData, routeData?.center_latitude, routeData?.center_longitude, rotationAngle, ]); const sightPoints = useMemo(() => { if (!sightData || !routeData) return new Float32Array(); const centerLat = routeData.center_latitude; const centerLon = routeData.center_longitude; if (centerLat === undefined || centerLon === undefined) return new Float32Array(); const cos = Math.cos(rotationAngle); const sin = Math.sin(rotationAngle); const verts: number[] = []; for (const s of sightData) { const local = coordinatesToLocal( s.latitude - centerLat, s.longitude - centerLon ); const x = local.x * UP_SCALE; const y = local.y * UP_SCALE; const rx = x * cos - y * sin; const ry = x * sin + y * cos; verts.push(rx, ry); } return new Float32Array(verts); }, [ sightData, routeData?.center_latitude, routeData?.center_longitude, rotationAngle, ]); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const gl = initWebGLContext(canvas); glRef.current = gl; if (!gl) return; const vertSrc = ` attribute vec2 a_pos; uniform vec2 u_cameraPos; uniform float u_scale; uniform vec2 u_resolution; void main() { vec2 screen = a_pos * u_scale + u_cameraPos; vec2 zeroToOne = screen / u_resolution; vec2 zeroToTwo = zeroToOne * 2.0; vec2 clip = zeroToTwo - 1.0; gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); } `; const fragSrc = ` precision mediump float; uniform vec4 u_color; void main() { gl_FragColor = u_color; } `; const compile = (type: number, src: string) => { const s = gl.createShader(type)!; gl.shaderSource(s, src); gl.compileShader(s); return s; }; const vs = compile(gl.VERTEX_SHADER, vertSrc); const fs = compile(gl.FRAGMENT_SHADER, fragSrc); const prog = gl.createProgram()!; gl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog); programRef.current = prog; gl.useProgram(prog); const a_pos = gl.getAttribLocation(prog, "a_pos"); const u_cameraPos = gl.getUniformLocation(prog, "u_cameraPos"); const u_scale = gl.getUniformLocation(prog, "u_scale"); const u_resolution = gl.getUniformLocation(prog, "u_resolution"); const u_color = gl.getUniformLocation(prog, "u_color"); attribsRef.current = { a_pos }; uniformsRef.current = { u_cameraPos, u_scale, u_resolution, u_color }; const buffer = gl.createBuffer(); bufferRef.current = buffer; const pointVert = ` attribute vec2 a_pos; uniform vec2 u_cameraPos; uniform float u_scale; uniform vec2 u_resolution; uniform float u_pointSize; void main() { vec2 screen = a_pos * u_scale + u_cameraPos; vec2 zeroToOne = screen / u_resolution; vec2 zeroToTwo = zeroToOne * 2.0; vec2 clip = zeroToTwo - 1.0; gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); gl_PointSize = u_pointSize; } `; const pointFrag = ` precision mediump float; uniform vec4 u_color; void main() { vec2 c = gl_PointCoord * 2.0 - 1.0; float d = dot(c, c); if (d > 1.0) discard; gl_FragColor = u_color; } `; const vs2 = compile(gl.VERTEX_SHADER, pointVert); const fs2 = compile(gl.FRAGMENT_SHADER, pointFrag); const pprog = gl.createProgram()!; gl.attachShader(pprog, vs2); gl.attachShader(pprog, fs2); gl.linkProgram(pprog); pointProgramRef.current = pprog; pointBufferRef.current = gl.createBuffer(); const lineVert = ` attribute vec2 a_screen; uniform vec2 u_resolution; void main(){ vec2 zeroToOne = a_screen / u_resolution; vec2 clip = zeroToOne * 2.0 - 1.0; gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); } `; const lineFrag = ` precision mediump float; uniform vec4 u_color; void main(){ gl_FragColor = u_color; } `; const lv = compile(gl.VERTEX_SHADER, lineVert); const lf = compile(gl.FRAGMENT_SHADER, lineFrag); const lprog = gl.createProgram()!; gl.attachShader(lprog, lv); gl.attachShader(lprog, lf); gl.linkProgram(lprog); screenLineProgramRef.current = lprog; screenLineBufferRef.current = gl.createBuffer(); const handleResize = () => { const changed = resizeCanvasToDisplaySize(canvas); if (!gl) return; setScreenCenter({ x: canvas.width / 2, y: canvas.height / 2, }); if (changed) { gl.viewport(0, 0, canvas.width, canvas.height); } gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); }; handleResize(); window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); }; }, []); useEffect(() => { const centerLat = routeData?.center_latitude; const centerLon = routeData?.center_longitude; if (centerLat !== undefined && centerLon !== undefined) { const coords: any = apiStore?.context?.currentCoordinates; if (coords) { const local = coordinatesToLocal( coords.latitude - centerLat, coords.longitude - centerLon ); const x = local.x * UP_SCALE; const y = local.y * UP_SCALE; const cos = Math.cos(rotationAngle), sin = Math.sin(rotationAngle); const rx = x * cos - y * sin; const ry = x * sin + y * cos; if (isAutoMode) { animateYellowDotTo(rx, ry); } else { setYellowDotPositionImmediate(rx, ry); } } } }, [ routeData?.center_latitude, routeData?.center_longitude, rotationAngle, apiStore?.context?.currentCoordinates?.latitude, apiStore?.context?.currentCoordinates?.longitude, isAutoMode, animateYellowDotTo, setYellowDotPositionImmediate, ]); useEffect(() => { const gl = glRef.current; const canvas = canvasRef.current; const prog = programRef.current; const buffer = bufferRef.current; const attribs = attribsRef.current; const uniforms = uniformsRef.current; const pprog = pointProgramRef.current; const pbuffer = pointBufferRef.current; if ( !gl || !canvas || !prog || !buffer || !attribs || !uniforms || !pprog || !pbuffer ) return; gl.viewport(0, 0, canvas.width, canvas.height); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(prog); gl.uniform2f(uniforms.u_cameraPos, position.x, position.y); gl.uniform1f(uniforms.u_scale, scale); gl.uniform2f(uniforms.u_resolution, canvas.width, canvas.height); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, routePath, gl.STATIC_DRAW); gl.enableVertexAttribArray(attribs.a_pos); gl.vertexAttribPointer(attribs.a_pos, 2, gl.FLOAT, false, 0, 0); const vcount = routePath.length / 2; let tramSegIndex = -1; { const centerLatTmp = routeData?.center_latitude; const centerLonTmp = routeData?.center_longitude; if (centerLatTmp !== undefined && centerLonTmp !== undefined) { const coordsAny: any = apiStore?.context?.currentCoordinates; if (coordsAny) { const loc = coordinatesToLocal( coordsAny.latitude - centerLatTmp, coordsAny.longitude - centerLonTmp ); const wx = loc.x * UP_SCALE; const wy = loc.y * UP_SCALE; const cosR = Math.cos(rotationAngle), sinR = Math.sin(rotationAngle); const tX = wx * cosR - wy * sinR; const tY = wx * sinR + wy * cosR; let best = -1, bestD = Infinity; for (let i = 0; i < vcount - 1; i++) { const p1x = routePath[i * 2], p1y = routePath[i * 2 + 1]; const p2x = routePath[(i + 1) * 2], p2y = routePath[(i + 1) * 2 + 1]; const dx = p2x - p1x, dy = p2y - p1y; const len2 = dx * dx + dy * dy; if (!len2) continue; const t = ((tX - p1x) * dx + (tY - p1y) * dy) / len2; const cl = Math.max(0, Math.min(1, t)); const px = p1x + cl * dx, py = p1y + cl * dy; const d = Math.hypot(tX - px, tY - py); if (d < bestD) { bestD = d; best = i; } } tramSegIndex = best; } } } const vertexCount = routePath.length / 2; if (vertexCount > 1) { const generateThickLine = (points: Float32Array, width: number) => { const vertices: number[] = []; const halfWidth = width / 2; if (points.length < 4) return new Float32Array(); for (let i = 0; i < points.length - 2; i += 2) { const x1 = points[i]; const y1 = points[i + 1]; const x2 = points[i + 2]; const y2 = points[i + 3]; const dx = x2 - x1; const dy = y2 - y1; const length = Math.sqrt(dx * dx + dy * dy); if (length === 0) continue; const perpX = (-dy / length) * halfWidth; const perpY = (dx / length) * halfWidth; vertices.push(x1 + perpX, y1 + perpY); vertices.push(x1 - perpX, y1 - perpY); vertices.push(x2 + perpX, y2 + perpY); vertices.push(x1 - perpX, y1 - perpY); vertices.push(x2 - perpX, y2 - perpY); vertices.push(x2 + perpX, y2 + perpY); if (i < points.length - 4) { const x3 = points[i + 4]; const y3 = points[i + 5]; const dx2 = x3 - x2; const dy2 = y3 - y2; const length2 = Math.sqrt(dx2 * dx2 + dy2 * dy2); if (length2 > 0) { const perpX2 = (-dy2 / length2) * halfWidth; const perpY2 = (dx2 / length2) * halfWidth; vertices.push(x2 + perpX, y2 + perpY); vertices.push(x2 - perpX, y2 - perpY); vertices.push(x2 + perpX2, y2 + perpY2); vertices.push(x2 - perpX, y2 - perpY); vertices.push(x2 - perpX2, y2 - perpY2); vertices.push(x2 + perpX2, y2 + perpY2); } } } return new Float32Array(vertices); }; const lineWidth = Math.min(6); const r1 = ((PATH_COLOR >> 16) & 0xff) / 255; const g1 = ((PATH_COLOR >> 8) & 0xff) / 255; const b1 = (PATH_COLOR & 0xff) / 255; gl.uniform4f(uniforms.u_color, r1, g1, b1, 1); if (tramSegIndex >= 0) { const animatedPos = animatedYellowDotPosition; if ( animatedPos && animatedPos.x !== undefined && animatedPos.y !== undefined ) { const passedPoints: number[] = []; for (let i = 0; i <= tramSegIndex; i++) { passedPoints.push(routePath[i * 2], routePath[i * 2 + 1]); } passedPoints.push(animatedPos.x, animatedPos.y); if (passedPoints.length >= 4) { const thickLineVertices = generateThickLine( new Float32Array(passedPoints), lineWidth ); gl.bufferData(gl.ARRAY_BUFFER, thickLineVertices, gl.STATIC_DRAW); gl.drawArrays(gl.TRIANGLES, 0, thickLineVertices.length / 2); } } } const r2 = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255; const g2 = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255; const b2 = (UNPASSED_STATION_COLOR & 0xff) / 255; gl.uniform4f(uniforms.u_color, r2, g2, b2, 1); const animatedPos = animatedYellowDotPosition; if ( animatedPos && animatedPos.x !== undefined && animatedPos.y !== undefined ) { const unpassedPoints: number[] = []; unpassedPoints.push(animatedPos.x, animatedPos.y); for (let i = tramSegIndex + 1; i < vertexCount; i++) { unpassedPoints.push(routePath[i * 2], routePath[i * 2 + 1]); } if (unpassedPoints.length >= 4) { const thickLineVertices = generateThickLine( new Float32Array(unpassedPoints), lineWidth ); gl.bufferData(gl.ARRAY_BUFFER, thickLineVertices, gl.STATIC_DRAW); gl.drawArrays(gl.TRIANGLES, 0, thickLineVertices.length / 2); } } } if (stationPoints.length > 0) { gl.useProgram(pprog); const a_pos_pts = gl.getAttribLocation(pprog, "a_pos"); const u_cameraPos_pts = gl.getUniformLocation(pprog, "u_cameraPos"); const u_scale_pts = gl.getUniformLocation(pprog, "u_scale"); const u_resolution_pts = gl.getUniformLocation(pprog, "u_resolution"); const u_pointSize = gl.getUniformLocation(pprog, "u_pointSize"); const u_color_pts = gl.getUniformLocation(pprog, "u_color"); gl.uniform2f(u_cameraPos_pts, position.x, position.y); gl.uniform1f(u_scale_pts, scale); gl.uniform2f(u_resolution_pts, canvas.width, canvas.height); gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer); gl.bufferData(gl.ARRAY_BUFFER, stationPoints, gl.STATIC_DRAW); gl.enableVertexAttribArray(a_pos_pts); gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0); gl.uniform1f(u_pointSize, 10 * scale * 1.5); const r_outline = ((BACKGROUND_COLOR >> 16) & 0xff) / 255; const g_outline = ((BACKGROUND_COLOR >> 8) & 0xff) / 255; const b_outline = (BACKGROUND_COLOR & 0xff) / 255; gl.uniform4f(u_color_pts, r_outline, g_outline, b_outline, 1); gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2); gl.uniform1f(u_pointSize, 8.0 * scale * 1.5); if (tramSegIndex >= 0) { const passedStations = []; for (let i = 0; i < stationData.length; i++) { if (i <= tramSegIndex) { passedStations.push(stationPoints[i * 2], stationPoints[i * 2 + 1]); } } if (passedStations.length > 0) { const r_passed = ((PATH_COLOR >> 16) & 0xff) / 255; const g_passed = ((PATH_COLOR >> 8) & 0xff) / 255; const b_passed = (PATH_COLOR & 0xff) / 255; gl.uniform4f(u_color_pts, r_passed, g_passed, b_passed, 1); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array(passedStations), gl.STATIC_DRAW ); gl.drawArrays(gl.POINTS, 0, passedStations.length / 2); } } if (tramSegIndex >= 0) { const unpassedStations = []; for (let i = 0; i < stationData.length; i++) { if (i > tramSegIndex) { unpassedStations.push( stationPoints[i * 2], stationPoints[i * 2 + 1] ); } } if (unpassedStations.length > 0) { const r_unpassed = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255; const g_unpassed = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255; const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255; gl.uniform4f(u_color_pts, r_unpassed, g_unpassed, b_unpassed, 1); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array(unpassedStations), gl.STATIC_DRAW ); gl.drawArrays(gl.POINTS, 0, unpassedStations.length / 2); } } else { const r_unpassed = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255; const g_unpassed = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255; const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255; gl.uniform4f(u_color_pts, r_unpassed, g_unpassed, b_unpassed, 1); gl.bufferData(gl.ARRAY_BUFFER, stationPoints, gl.STATIC_DRAW); gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2); } } const a_pos_pts = gl.getAttribLocation(pprog, "a_pos"); const u_cameraPos_pts = gl.getUniformLocation(pprog, "u_cameraPos"); const u_scale_pts = gl.getUniformLocation(pprog, "u_scale"); const u_resolution_pts = gl.getUniformLocation(pprog, "u_resolution"); const u_pointSize = gl.getUniformLocation(pprog, "u_pointSize"); const u_color_pts = gl.getUniformLocation(pprog, "u_color"); gl.uniform2f(u_cameraPos_pts, position.x, position.y); gl.uniform1f(u_scale_pts, scale); gl.uniform2f(u_resolution_pts, canvas.width, canvas.height); const toPointsArray = (arr: number[]) => new Float32Array(arr); const pathPts: { x: number; y: number }[] = []; for (let i = 0; i < routePath.length; i += 2) pathPts.push({ x: routePath[i], y: routePath[i + 1] }); const getSeg = (px: number, py: number) => { if (pathPts.length < 2) return -1; let best = -1, bestD = Infinity; for (let i = 0; i < pathPts.length - 1; i++) { const p1 = pathPts[i], p2 = pathPts[i + 1]; const dx = p2.x - p1.x, dy = p2.y - p1.y; const len2 = dx * dx + dy * dy; if (!len2) continue; const t = ((px - p1.x) * dx + (py - p1.y) * dy) / len2; const tt = Math.max(0, Math.min(1, t)); const cx = p1.x + tt * dx, cy = p1.y + tt * dy; const d = Math.hypot(px - cx, py - cy); if (d < bestD) { bestD = d; best = i; } } return best; }; let tramSegForStations = -1; { const cLat = routeData?.center_latitude, cLon = routeData?.center_longitude; const tram = apiStore?.context?.currentCoordinates as any; if (tram && cLat !== undefined && cLon !== undefined) { const loc = coordinatesToLocal( tram.latitude - cLat, tram.longitude - cLon ); const wx = loc.x * UP_SCALE, wy = loc.y * UP_SCALE; const cosR = Math.cos(rotationAngle), sinR = Math.sin(rotationAngle); const tx = wx * cosR - wy * sinR, ty = wx * sinR + wy * cosR; tramSegForStations = getSeg(tx, ty); } } const passedStations: number[] = []; const unpassedStations: number[] = []; for (let i = 0; i < stationPoints.length; i += 2) { const sx = stationPoints[i], sy = stationPoints[i + 1]; const seg = getSeg(sx, sy); if (tramSegForStations !== -1 && seg !== -1 && seg < tramSegForStations) passedStations.push(sx, sy); else unpassedStations.push(sx, sy); } const outlineSize = 10.0 * scale * 2, coreSize = 8.0 * scale * 2; gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer); gl.bufferData( gl.ARRAY_BUFFER, toPointsArray(unpassedStations), gl.STREAM_DRAW ); gl.enableVertexAttribArray(a_pos_pts); gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0); gl.uniform1f(u_pointSize, outlineSize); gl.uniform4f( u_color_pts, ((BACKGROUND_COLOR >> 16) & 255) / 255, ((BACKGROUND_COLOR >> 8) & 255) / 255, (BACKGROUND_COLOR & 255) / 255, 1 ); if (unpassedStations.length) gl.drawArrays(gl.POINTS, 0, unpassedStations.length / 2); gl.uniform1f(u_pointSize, coreSize); gl.uniform4f( u_color_pts, ((UNPASSED_STATION_COLOR >> 16) & 255) / 255, ((UNPASSED_STATION_COLOR >> 8) & 255) / 255, (UNPASSED_STATION_COLOR & 255) / 255, 1 ); if (unpassedStations.length) gl.drawArrays(gl.POINTS, 0, unpassedStations.length / 2); gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer); gl.bufferData( gl.ARRAY_BUFFER, toPointsArray(passedStations), gl.STREAM_DRAW ); gl.enableVertexAttribArray(a_pos_pts); gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0); gl.uniform1f(u_pointSize, outlineSize); gl.uniform4f( u_color_pts, ((BACKGROUND_COLOR >> 16) & 255) / 255, ((BACKGROUND_COLOR >> 8) & 255) / 255, (BACKGROUND_COLOR & 255) / 255, 1 ); if (passedStations.length) gl.drawArrays(gl.POINTS, 0, passedStations.length / 2); gl.uniform1f(u_pointSize, coreSize); gl.uniform4f( u_color_pts, ((PATH_COLOR >> 16) & 255) / 255, ((PATH_COLOR >> 8) & 255) / 255, (PATH_COLOR & 255) / 255, 1 ); if (passedStations.length) gl.drawArrays(gl.POINTS, 0, passedStations.length / 2); if ( stationData && stationData.length > 0 && routeData && apiStore?.context ) { const centerLat = routeData.center_latitude; const centerLon = routeData.center_longitude; if (centerLat !== undefined && centerLon !== undefined) { const cos = Math.cos(rotationAngle); const sin = Math.sin(rotationAngle); const startStationData = stationData.find( (station) => station.id.toString() === apiStore.context?.startStopId ); const endStationData = stationData.find( (station) => station.id.toString() === apiStore.context?.endStopId ); const terminalStations: number[] = []; if (startStationData) { const startLocal = coordinatesToLocal( startStationData.latitude - centerLat, startStationData.longitude - centerLon ); const startX = startLocal.x * UP_SCALE; const startY = startLocal.y * UP_SCALE; const startRx = startX * cos - startY * sin; const startRy = startX * sin + startY * cos; terminalStations.push(startRx, startRy); } if (endStationData) { const endLocal = coordinatesToLocal( endStationData.latitude - centerLat, endStationData.longitude - centerLon ); const endX = endLocal.x * UP_SCALE; const endY = endLocal.y * UP_SCALE; const endRx = endX * cos - endY * sin; const endRy = endX * sin + endY * cos; terminalStations.push(endRx, endRy); } if (terminalStations.length > 0) { const terminalStationData: any[] = []; if (startStationData) terminalStationData.push(startStationData); if (endStationData) terminalStationData.push(endStationData); let tramSegIndex = -1; const coords: any = apiStore?.context?.currentCoordinates; if (coords && centerLat !== undefined && centerLon !== undefined) { const local = coordinatesToLocal( coords.latitude - centerLat, coords.longitude - centerLon ); const wx = local.x * UP_SCALE; const wy = local.y * UP_SCALE; const cosR = Math.cos(rotationAngle); const sinR = Math.sin(rotationAngle); const tx = wx * cosR - wy * sinR; const ty = wx * sinR + wy * cosR; let best = -1; let bestD = Infinity; for (let i = 0; i < routePath.length - 2; i += 2) { const p1x = routePath[i]; const p1y = routePath[i + 1]; const p2x = routePath[i + 2]; const p2y = routePath[i + 3]; const dx = p2x - p1x; const dy = p2y - p1y; const len2 = dx * dx + dy * dy; if (!len2) continue; const t = ((tx - p1x) * dx + (ty - p1y) * dy) / len2; const cl = Math.max(0, Math.min(1, t)); const px = p1x + cl * dx; const py = p1y + cl * dy; const d = Math.hypot(tx - px, ty - py); if (d < bestD) { bestD = d; best = i / 2; } } tramSegIndex = best; } const isStartPassed = startStationData ? (() => { const sx = terminalStations[0]; const sy = terminalStations[1]; const seg = (() => { if (routePath.length < 4) return -1; let best = -1; let bestD = Infinity; for (let i = 0; i < routePath.length - 2; i += 2) { const p1x = routePath[i]; const p1y = routePath[i + 1]; const p2x = routePath[i + 2]; const p2y = routePath[i + 3]; const dx = p2x - p1x; const dy = p2y - p1y; const len2 = dx * dx + dy * dy; if (!len2) continue; const t = ((sx - p1x) * dx + (sy - p1y) * dy) / len2; const cl = Math.max(0, Math.min(1, t)); const px = p1x + cl * dx; const py = p1y + cl * dy; const d = Math.hypot(sx - px, sy - py); if (d < bestD) { bestD = d; best = i / 2; } } return best; })(); return tramSegIndex !== -1 && seg !== -1 && seg < tramSegIndex; })() : false; const isEndPassed = endStationData ? (() => { const ex = terminalStations[terminalStations.length - 2]; const ey = terminalStations[terminalStations.length - 1]; const seg = (() => { if (routePath.length < 4) return -1; let best = -1; let bestD = Infinity; for (let i = 0; i < routePath.length - 2; i += 2) { const p1x = routePath[i]; const p1y = routePath[i + 1]; const p2x = routePath[i + 2]; const p2y = routePath[i + 3]; const dx = p2x - p1x; const dy = p2y - p1y; const len2 = dx * dx + dy * dy; if (!len2) continue; const t = ((ex - p1x) * dx + (ey - p1y) * dy) / len2; const cl = Math.max(0, Math.min(1, t)); const px = p1x + cl * dx; const py = p1y + cl * dy; const d = Math.hypot(ex - px, ey - py); if (d < bestD) { bestD = d; best = i / 2; } } return best; })(); return tramSegIndex !== -1 && seg !== -1 && seg < tramSegIndex; })() : false; gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array(terminalStations), gl.STREAM_DRAW ); gl.enableVertexAttribArray(a_pos_pts); gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0); gl.uniform1f(u_pointSize, 18.0 * scale); if (startStationData && endStationData) { if (isStartPassed) { gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); } else { gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); } gl.drawArrays(gl.POINTS, 0, 1); if (isEndPassed) { gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); } else { gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); } gl.drawArrays(gl.POINTS, 1, 1); } else { const isPassed = startStationData ? isStartPassed : isEndPassed; if (isPassed) { gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); } else { gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); } gl.drawArrays(gl.POINTS, 0, terminalStations.length / 2); } gl.uniform1f(u_pointSize, 11.0 * scale); const r_center = ((BACKGROUND_COLOR >> 16) & 0xff) / 255; const g_center = ((BACKGROUND_COLOR >> 8) & 0xff) / 255; const b_center = (BACKGROUND_COLOR & 0xff) / 255; gl.uniform4f(u_color_pts, r_center, g_center, b_center, 1.0); gl.drawArrays(gl.POINTS, 0, terminalStations.length / 2); } } } if (animatedYellowDotPosition) { const rx = animatedYellowDotPosition.x; const ry = animatedYellowDotPosition.y; gl.uniform1f(u_pointSize, 13.3333 * scale); gl.uniform4f(u_color_pts, 1.0, 1.0, 0.0, 1.0); const tmp = new Float32Array([rx, ry]); gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer); gl.bufferData(gl.ARRAY_BUFFER, tmp, gl.STREAM_DRAW); gl.enableVertexAttribArray(a_pos_pts); gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.POINTS, 0, 1); } }, [ routePath, stationPoints, sightPoints, position.x, position.y, scale, animatedYellowDotPosition?.x, animatedYellowDotPosition?.y, ]); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; if (!routePath || routePath.length < 4) return; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (let i = 0; i < routePath.length; i += 2) { const x = routePath[i]; const y = routePath[i + 1]; if (x < minX) minX = x; if (y < minY) minY = y; if (x > maxX) maxX = x; if (y > maxY) maxY = y; } if ( !isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY) ) return; const worldWidth = Math.max(1, maxX - minX); const worldHeight = Math.max(1, maxY - minY); const margin = 0.1; const targetScale = Math.min( (canvas.width * (1 - margin)) / worldWidth, (canvas.height * (1 - margin)) / worldHeight ); const centerX = (minX + maxX) / 2; const centerY = (minY + maxY) / 2; const clampedScale = clampScale(targetScale); setScaleRef.current(clampedScale); setPositionRef.current({ x: canvas.width / 2 - centerX * clampedScale, y: canvas.height / 2 - centerY * clampedScale, }); }, [routePath]); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; let isDragging = false; let startMouse = { x: 0, y: 0 }; let startPos = { x: 0, y: 0 }; const activePointers = new Map(); let isPinching = false; let pinchStart: { distance: number; midpoint: { x: number; y: number }; scale: number; position: { x: number; y: number }; } | null = null; const getDistance = ( p1: { x: number; y: number }, p2: { x: number; y: number } ) => Math.hypot(p2.x - p1.x, p2.y - p1.y); const getMidpoint = ( p1: { x: number; y: number }, p2: { x: number; y: number } ) => ({ x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2, }); const onPointerDown = (e: PointerEvent) => { updateUserActivity(); if (isAutoMode) { setIsAutoMode(false); } cameraAnimationStore.stopAnimation(); canvas.setPointerCapture(e.pointerId); const rect = canvas.getBoundingClientRect(); activePointers.set(e.pointerId, { x: e.clientX - rect.left, y: e.clientY - rect.top, }); if (activePointers.size === 1) { isDragging = true; startMouse = { x: e.clientX - rect.left, y: e.clientY - rect.top }; startPos = { x: positionRef.current.x, y: positionRef.current.y }; } else if (activePointers.size === 2) { isDragging = false; const [p1, p2] = Array.from(activePointers.values()); pinchStart = { distance: getDistance(p1, p2), midpoint: getMidpoint(p1, p2), scale: scaleRef.current, position: { x: positionRef.current.x, y: positionRef.current.y }, }; isPinching = true; } }; const onPointerMove = (e: PointerEvent) => { if (!activePointers.has(e.pointerId)) return; updateUserActivity(); const rect = canvas.getBoundingClientRect(); activePointers.set(e.pointerId, { x: e.clientX - rect.left, y: e.clientY - rect.top, }); if (activePointers.size === 2) { isDragging = false; const pointersArray = Array.from(activePointers.values()); if (pointersArray.length === 2) { const [p1, p2] = pointersArray; if (!isPinching || pinchStart === null) { isPinching = true; pinchStart = { distance: getDistance(p1, p2), midpoint: getMidpoint(p1, p2), scale: scaleRef.current, position: { x: positionRef.current.x, y: positionRef.current.y }, }; } if (pinchStart) { const currentDistance = getDistance(p1, p2); const zoomFactor = currentDistance / pinchStart.distance; const unclampedScale = pinchStart.scale * zoomFactor; const newScale = clampScale(Math.max(0.1, unclampedScale)); const k = newScale / pinchStart.scale; const newPosition = { x: pinchStart.position.x * k + pinchStart.midpoint.x * (1 - k), y: pinchStart.position.y * k + pinchStart.midpoint.y * (1 - k), }; setPositionRef.current(newPosition); setScaleRef.current(newScale); } } } else if (isDragging && activePointers.size === 1) { const p = Array.from(activePointers.values())[0]; if ( !startMouse || !startPos || typeof startMouse.x !== "number" || typeof startMouse.y !== "number" || typeof startPos.x !== "number" || typeof startPos.y !== "number" ) { console.warn( "WebGLMap: Некорректные значения startMouse или startPos:", { startMouse, startPos, p, } ); return; } const dx = p.x - startMouse.x; const dy = p.y - startMouse.y; setPositionRef.current({ x: startPos.x + dx, y: startPos.y + dy }); } }; const onPointerUp = (e: PointerEvent) => { updateUserActivity(); canvas.releasePointerCapture(e.pointerId); activePointers.delete(e.pointerId); if (activePointers.size < 2) { isPinching = false; pinchStart = null; } if (activePointers.size === 0) { isDragging = false; } else if (activePointers.size === 1) { const p = Array.from(activePointers.values())[0]; startPos = { x: positionRef.current.x, y: positionRef.current.y }; startMouse = { x: p.x, y: p.y }; isDragging = true; } }; const onPointerCancel = (e: PointerEvent) => { updateUserActivity(); canvas.releasePointerCapture(e.pointerId); activePointers.delete(e.pointerId); isPinching = false; pinchStart = null; if (activePointers.size === 0) { isDragging = false; } }; const onWheel = (e: WheelEvent) => { e.preventDefault(); updateUserActivity(); if (isAutoMode) { setIsAutoMode(false); } cameraAnimationStore.stopAnimation(); const rect = canvas.getBoundingClientRect(); const mouseX = (e.clientX - rect.left) * (canvas.width / canvas.clientWidth); const mouseY = (e.clientY - rect.top) * (canvas.height / canvas.clientHeight); const delta = e.deltaY > 0 ? 0.9 : 1.1; const unclampedScale = scaleRef.current * delta; const newScale = clampScale(Math.max(0.1, unclampedScale)); const k = newScale / scaleRef.current; const newPosition = { x: positionRef.current.x * k + mouseX * (1 - k), y: positionRef.current.y * k + mouseY * (1 - k), }; setScaleRef.current(newScale); setPositionRef.current(newPosition); }; canvas.addEventListener("pointerdown", onPointerDown); canvas.addEventListener("pointermove", onPointerMove); canvas.addEventListener("pointerup", onPointerUp); canvas.addEventListener("pointercancel", onPointerCancel); canvas.addEventListener("pointerleave", onPointerUp); canvas.addEventListener("wheel", onWheel, { passive: false }); return () => { canvas.removeEventListener("pointerdown", onPointerDown); canvas.removeEventListener("pointermove", onPointerMove); canvas.removeEventListener("pointerup", onPointerUp); canvas.removeEventListener("pointercancel", onPointerCancel); canvas.removeEventListener("pointerleave", onPointerUp); canvas.removeEventListener("wheel", onWheel as any); }; }, [ updateUserActivity, setIsAutoMode, cameraAnimationStore, isAutoMode, clampScale, ]); return (
{stationLabels.map((l, idx) => (
{l.name}
{l.sub ? (
{l.sub}
) : null}
))} {sightData?.map((s: any, i: number) => { const centerLat = routeData?.center_latitude; const centerLon = routeData?.center_longitude; if (centerLat === undefined || centerLon === undefined) return null; const cos = Math.cos(rotationAngle); const sin = Math.sin(rotationAngle); const local = coordinatesToLocal( s.latitude - centerLat, s.longitude - centerLon ); const x = local.x * UP_SCALE; const y = local.y * UP_SCALE; const rx = x * cos - y * sin; const ry = x * sin + y * cos; const dpr = Math.max( 1, (typeof window !== "undefined" && window.devicePixelRatio) || 1 ); const sx = (rx * scale + position.x) / dpr; const sy = (ry * scale + position.y) / dpr; const size = 30; const handleSightClick = () => { const { setSelectedSightId, setIsManualSelection, setIsRightWidgetSelectorOpen, closeGovernorModal, } = useGeolocationStore(); setSelectedSightId(String(s.id)); setIsManualSelection(true); setIsRightWidgetSelectorOpen(false); closeGovernorModal(); }; return ( ); })} {(() => { if (!routeData) return null; const centerLat = routeData.center_latitude; const centerLon = routeData.center_longitude; if (centerLat === undefined || centerLon === undefined) return null; const coords: any = apiStore?.context?.currentCoordinates; if (!coords) return null; const local = coordinatesToLocal( coords.latitude - centerLat, coords.longitude - centerLon ); const wx = local.x * UP_SCALE; const wy = local.y * UP_SCALE; const cosR = Math.cos(rotationAngle); const sinR = Math.sin(rotationAngle); const rx = wx * cosR - wy * sinR; const ry = wx * sinR + wy * cosR; const dpr2 = Math.max( 1, (typeof window !== "undefined" && window.devicePixelRatio) || 1 ); const screenX = (rx * scale + position.x) / dpr2; const screenY = (ry * scale + position.y) / dpr2; const pathPts: { x: number; y: number }[] = []; for (let i = 0; i < routePath.length; i += 2) pathPts.push({ x: routePath[i], y: routePath[i + 1] }); const stationsForAngle = (stationData || []).map((st: any) => { const loc = coordinatesToLocal( st.latitude - centerLat, st.longitude - centerLon ); const x = loc.x * UP_SCALE, y = loc.y * UP_SCALE; const rx2 = x * cosR - y * sinR, ry2 = x * sinR + y * cosR; return { longitude: rx2, latitude: ry2, offset_x: st.offset_x, offset_y: st.offset_y, }; }); let tramSegIndex = -1; if (routePath.length >= 4) { let best = -1, bestD = Infinity; for (let i = 0; i < routePath.length - 2; i += 2) { const p1x = routePath[i], p1y = routePath[i + 1]; const p2x = routePath[i + 2], p2y = routePath[i + 3]; const dx = p2x - p1x, dy = p2y - p1y; const len2 = dx * dx + dy * dy; if (!len2) continue; const t = ((rx - p1x) * dx + (ry - p1y) * dy) / len2; const cl = Math.max(0, Math.min(1, t)); const px = p1x + cl * dx, py = p1y + cl * dy; const d = Math.hypot(rx - px, ry - py); if (d < bestD) { bestD = d; best = i / 2; } } tramSegIndex = best; } const optimalAngle = (() => { const testRadiusInMap = 100 / scale; const minPath = 60, minPassed = 60, minStation = 50; let bestAng = 0, bestScore = Infinity; for (let i = 0; i < 12; i++) { const ang = (i * Math.PI * 2) / 12; const tx = rx + Math.cos(ang) * testRadiusInMap; const ty = ry + Math.sin(ang) * testRadiusInMap; const distPath = (function () { if (pathPts.length < 2) return Infinity; let md = Infinity; for (let k = 0; k < pathPts.length - 1; k++) { const p1 = pathPts[k], p2 = pathPts[k + 1]; const L2 = (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2; if (!L2) continue; const tt = ((tx - p1.x) * (p2.x - p1.x) + (ty - p1.y) * (p2.y - p1.y)) / L2; const cl = Math.max(0, Math.min(1, tt)); const px = p1.x + cl * (p2.x - p1.x), py = p1.y + cl * (p2.y - p1.y); const d = Math.hypot(tx - px, ty - py); if (d < md) md = d; } return md * scale; })(); const distPassed = (function () { if (pathPts.length < 2 || tramSegIndex < 0) return Infinity; let md = Infinity; for ( let k = 0; k <= Math.min(tramSegIndex, pathPts.length - 2); k++ ) { const p1 = pathPts[k], p2 = pathPts[k + 1]; const L2 = (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2; if (!L2) continue; const tt = ((tx - p1.x) * (p2.x - p1.x) + (ty - p1.y) * (p2.y - p1.y)) / L2; const cl = Math.max(0, Math.min(1, tt)); const px = p1.x + cl * (p2.x - p1.x), py = p1.y + cl * (p2.y - p1.y); const d = Math.hypot(tx - px, ty - py); if (d < md) md = d; } return md * scale; })(); const distStation = (function () { if (!stationsForAngle.length) return Infinity; const DEFAULT_LABEL_OFFSET_X = 25, DEFAULT_LABEL_OFFSET_Y = 0; let md = Infinity; for (const st of stationsForAngle) { const offsetX = st.offset_x === 0 && st.offset_y === 0 ? DEFAULT_LABEL_OFFSET_X : st.offset_x || 0 * 3; const offsetY = st.offset_x === 0 && st.offset_y === 0 ? DEFAULT_LABEL_OFFSET_Y : st.offset_y || 0 * 3; const lx = st.longitude + offsetX, ly = st.latitude + offsetY; const d = Math.hypot(tx - lx, ty - ly); if (d < md) md = d; } return md * scale; })(); let weight = 0; if (distPath < minPath) weight += 100 * (1 - distPath / minPath); if (distPassed < minPassed) weight += 10 * (1 - distPassed / minPassed); if (distStation < minStation) weight += 1000 * (1 - distStation / minStation); if (weight < bestScore) { bestScore = weight; bestAng = ang; } } return bestAng; })(); return ( ); })()}
); }); export default WebGLMap;