import React, {
  useEffect,
  useMemo,
  useRef,
  useCallback,
  useState,
} from "react";
import { observer } from "mobx-react-lite";
import { useMapData } from "./MapDataContext";
import { SightData } from "./types";
import { useTransform } from "./transformContext";
import { coordinatesToLocal } from "./utils";
import {
  UP_SCALE,
  PATH_COLOR,
  BACKGROUND_COLOR,
  UNPASSED_STATION_COLOR,
  BUS_COLOR,
  BASE_ICON_SIZE,
} from "./Constants";
import { SCALE_FACTOR } from "../../assets/Constants";
import { apiStore } from "../../api/ApiStore/store";
import { useGeolocationStore } from "../../stores/hooks/useGeolocationStore";
import { useRouteFollowingPosition } from "../../hooks/useRouteFollowingPosition";
import { useCameraAnimationStore } from "../../stores";
import { TramIconWebGL } from "./TramIconWebGL";
import { OverlayScrollbarsWrapper } from "../OverlayScrollbarsWrapper";
import { apiBaseURL } from "../../api/apiConfig";
import "../../styles/OverlayScrollbars.css";
const SIGHT_ICON_URL = new URL("../../assets/images/sight.svg", import.meta.url)
  .href;
const SIGHT_ICON_BASE_SIZE = 30;
const CUSTOM_SIGHT_ICON_BASE_SCALE = 0.1;
const DEBUG_WEBGL_ROUTE_MAP = true;
const YELLOW_ICON_FILTER =
  "brightness(0) saturate(100%) invert(84%) sepia(99%) saturate(3457%) hue-rotate(358deg) brightness(104%) contrast(99%)";
const clamp = (value: number, min: number, max: number) =>
  Math.min(max, Math.max(min, value));

const debugWebglLog = (..._args: unknown[]) => {
  if (!DEBUG_WEBGL_ROUTE_MAP) return;
};

const isMediaIdEmpty = (id: string | null) => {
  if (id == null || id === "") return true;
  const digits = id.replace(/-/g, "");
  return digits === "" || /^0+$/.test(digits);
};

const appendTokenToUrl = (url: string, token: string): string => {
  if (!token) return url;
  if (/([?&])token=/.test(url)) return url;
  const separator = url.includes("?") ? "&" : "?";
  return `${url}${separator}token=${encodeURIComponent(token)}`;
};

const buildMediaDownloadUrl = (
  baseUrl: string,
  mediaId: string,
  token: string,
): string => {
  if (/^https?:\/\//i.test(mediaId)) {
    return appendTokenToUrl(mediaId, token);
  }

  const normalizedBase = baseUrl.replace(/\/+$/, "");
  const normalizedMediaId = mediaId.replace(/^\/+/, "");

  if (/^media\/.+\/download$/i.test(normalizedMediaId)) {
    return appendTokenToUrl(`${normalizedBase}/${normalizedMediaId}`, token);
  }

  if (/^media$/i.test(normalizedBase.split("/").pop() ?? "")) {
    return appendTokenToUrl(
      `${normalizedBase}/${normalizedMediaId}/download`,
      token,
    );
  }

  return appendTokenToUrl(
    `${normalizedBase}/media/${normalizedMediaId}/download`,
    token,
  );
};

type Cluster = {
  id: string;
  longitude: number;
  latitude: number;
  count: number;
  sights: SightData[];
};

type PointItem = { type: "point"; id: string; data: SightData };
type ClusterItem = { type: "cluster"; id: string; data: Cluster };
type ClusteredItem = PointItem | ClusterItem;

const getDistance = (
  p1: { longitude: number; latitude: number },
  p2: { longitude: number; latitude: number },
) => {
  return Math.sqrt(
    Math.pow(p1.longitude - p2.longitude, 2) +
      Math.pow(p1.latitude - p2.latitude, 2),
  );
};

const getDistanceFromPointToPath = (
  point: { longitude: number; latitude: number },
  pathPoints: { x: number; y: number }[],
): number => {
  if (!pathPoints || pathPoints.length < 2) {
    return Infinity;
  }

  let minDistance = Infinity;

  for (let i = 0; i < pathPoints.length - 1; i++) {
    const p1 = pathPoints[i];
    const p2 = pathPoints[i + 1];

    const lineLengthSq = (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2;
    if (lineLengthSq === 0) continue;

    const t =
      ((point.longitude - p1.x) * (p2.x - p1.x) +
        (point.latitude - p1.y) * (p2.y - p1.y)) /
      lineLengthSq;
    const projectionT = Math.max(0, Math.min(1, t));

    const projectedX = p1.x + projectionT * (p2.x - p1.x);
    const projectedY = p1.y + projectionT * (p2.y - p1.y);

    const dist = Math.sqrt(
      (point.longitude - projectedX) ** 2 + (point.latitude - projectedY) ** 2,
    );
    minDistance = Math.min(minDistance, dist);
  }
  return minDistance;
};

const useSightClustering = (
  sights: SightData[],
  distanceThreshold: number,
  pathPoints: { x: number; y: number }[],
): ClusteredItem[] => {
  return useMemo(() => {
    const unclusteredSights: (SightData & { visited?: boolean })[] = sights.map(
      (s) => ({ ...s }),
    );
    const clusteredResult: ClusteredItem[] = [];

    for (const sight of unclusteredSights) {
      if (sight.visited) {
        continue;
      }

      const clusterSights: SightData[] = [];
      const queue = [sight];
      sight.visited = true;

      while (queue.length > 0 && clusterSights.length < 4) {
        const current = queue.shift()!;
        clusterSights.push(current);

        for (const potentialNeighbor of unclusteredSights) {
          if (
            !potentialNeighbor.visited &&
            clusterSights.length < 4 &&
            getDistance(current, potentialNeighbor) < distanceThreshold
          ) {
            potentialNeighbor.visited = true;
            queue.push(potentialNeighbor);
          }
        }
      }

      if (clusterSights.length > 1) {
        let furthestSight: SightData | null = null;
        let maxDistanceToPath = -1;

        for (const s of clusterSights) {
          const dist = getDistanceFromPointToPath(s, pathPoints);
          if (dist > maxDistanceToPath) {
            maxDistanceToPath = dist;
            furthestSight = s;
          }
        }

        const count = clusterSights.length;
        const longitude = furthestSight
          ? furthestSight.longitude
          : clusterSights.reduce((sum, s) => sum + s.longitude, 0) / count;
        const latitude = furthestSight
          ? furthestSight.latitude
          : clusterSights.reduce((sum, s) => sum + s.latitude, 0) / count;

        const id = `cluster-${clusterSights[0].id}`;
        clusteredResult.push({
          type: "cluster",
          id,
          data: { id, count, longitude, latitude, sights: clusterSights },
        });
      } else {
        const singleSight = clusterSights[0];
        clusteredResult.push({
          type: "point",
          id: String(singleSight.id),
          data: singleSight,
        });
      }
    }
    return clusteredResult;
  }, [sights, distanceThreshold, pathPoints]);
};

const getAnchorFromOffset = (align: number): { x: number; y: number } => {
  let anchorX: number;
  if (align === 1) {
    anchorX = 0;
  } else if (align === 3) {
    anchorX = 1;
  } else {
    anchorX = 0.5;
  }

  const anchorY = 0.5;

  return { x: anchorX, y: anchorY };
};

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<HTMLCanvasElement | null>(null);
  const glRef = useRef<WebGLRenderingContext | null>(null);
  const programRef = useRef<WebGLProgram | null>(null);
  const bufferRef = useRef<WebGLBuffer | null>(null);
  const pointProgramRef = useRef<WebGLProgram | null>(null);
  const pointBufferRef = useRef<WebGLBuffer | null>(null);
  const screenLineProgramRef = useRef<WebGLProgram | null>(null);
  const screenLineBufferRef = useRef<WebGLBuffer | null>(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 customSightIconBaseScaleRef = useRef<number | null>(null);
  const liveSightIconSizesRef = useRef<Map<number, number>>(new Map());
  const lastCameraLogScaleRef = useRef<number | null>(null);
  const lastDrawLogSignatureRef = useRef<string | null>(null);
  const [, forceSightIconScaleRender] = useState(0);

  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,
  });

  const routeBoundsRef = useRef<{
    minX: number;
    maxX: number;
    minY: number;
    maxY: number;
  } | null>(null);

  const store = useGeolocationStore();
  const {
    selectedSightId,
    setSelectedSightId,
    setIsManualSelection,
    setIsRightWidgetSelectorOpen,
    setIsGovernorWidgetOpen,
    setIsLeftWidgetOpen,
    activeClusterId,
    setActiveClusterId,
    nearestStationId,
    setNearestStationId,
    setIsTransferWidgetOpen,
    selectedLanguage,
    currentStationId,
  } = store;

  const {
    routeSights,
    routeSightsEn,
    routeSightsZh,
    sightArticles,
    sightArticlesEn,
    sightArticlesZh,
    orderedRouteStations,
    route: originalRouteData,
  } = apiStore;

  const mediaBaseUrl = apiBaseURL;
  const mediaToken = localStorage.getItem("token") || "";

  const clusterAutoCloseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
    null,
  );
  const hasInitializedCameraRef = useRef(false);

  useEffect(() => {
    if (
      routeData?.scale_min !== undefined &&
      routeData?.scale_max !== undefined
    ) {
      scaleLimitsRef.current = {
        min: routeData.scale_min / SCALE_FACTOR,
        max: routeData.scale_max / SCALE_FACTOR,
      };
    }
  }, [routeData?.scale_min, routeData?.scale_max]);

  useEffect(() => {
    customSightIconBaseScaleRef.current = null;
    liveSightIconSizesRef.current.clear();
  }, [routeData?.id]);

  useEffect(() => {
    if (!hasInitializedCameraRef.current) {
      return;
    }

    if (
      customSightIconBaseScaleRef.current == null &&
      Number.isFinite(scale) &&
      scale > 0
    ) {
      customSightIconBaseScaleRef.current = CUSTOM_SIGHT_ICON_BASE_SCALE;
      debugWebglLog("custom sight base scale initialized", {
        baseScale: CUSTOM_SIGHT_ICON_BASE_SCALE,
        currentScale: scale,
      });
      forceSightIconScaleRender((version) => version + 1);
    }
  }, [scale, routeData?.id, routeData?.scale_min, forceSightIconScaleRender]);

  const resolveSightIconSizePercent = useCallback(
    (sight?: SightData) => {
      const livePercent =
        sight != null ? liveSightIconSizesRef.current.get(sight.id) : undefined;
      if (typeof livePercent === "number" && Number.isFinite(livePercent)) {
        return livePercent;
      }

      if (
        sight != null &&
        typeof sight.icon_size === "number" &&
        Number.isFinite(sight.icon_size)
      ) {
        return sight.icon_size;
      }

      if (
        typeof routeData?.icon_size === "number" &&
        Number.isFinite(routeData.icon_size)
      ) {
        return routeData.icon_size;
      }

      if (
        typeof originalRouteData?.icon_size === "number" &&
        Number.isFinite(originalRouteData.icon_size)
      ) {
        return originalRouteData.icon_size;
      }

      return 100;
    },
    [routeData?.icon_size, originalRouteData?.icon_size],
  );

  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 clampPosition = useCallback(
    (pos: { x: number; y: number }, _currentScale: number) => {
      return pos;
    },
    [],
  );
  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]);

  useEffect(() => {
    if (
      lastCameraLogScaleRef.current != null &&
      Math.abs(lastCameraLogScaleRef.current - scale) < 1e-4
    ) {
      return;
    }

    lastCameraLogScaleRef.current = scale;
    debugWebglLog("camera state", {
      scale,
      position,
      scaleLimits: scaleLimitsRef.current,
    });
  }, [scale, position.x, position.y]);

  const rotationAngle = useMemo(() => {
    const deg = (routeData as any)?.rotate ?? 0;
    return (deg * Math.PI) / 180;
  }, [routeData]);

  const {
    position: animatedYellowDotPosition,
    animateTo: animateYellowDotTo,
    setPositionImmediate: setYellowDotImmediate,
    setRoutePath: setAnimatorRoutePath,
    getCurrentSegIndex,
  } = useRouteFollowingPosition(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,
  ]);

  const transformedTramCoordsRef = useRef<{
    x: number;
    y: number;
  } | null>(null);
  transformedTramCoordsRef.current = transformedTramCoords;

  useEffect(() => {
    if (routePath.length >= 4) {
      const t = transformedTramCoordsRef.current;
      setAnimatorRoutePath(routePath, t ? { x: t.x, y: t.y } : undefined);
    }
  }, [routePath, setAnimatorRoutePath]);

  useEffect(() => {
    const callback = (newPos: { x: number; y: number }, newZoom: number) => {
      setPosition(newPos);
      setScale(newZoom);
    };

    cameraAnimationStore.setUpdateCallback(callback);

    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 >= 120000 && !isAutoMode) {
        setIsAutoMode(true);

        setIsLeftWidgetOpen(false);
        setIsRightWidgetSelectorOpen(false);
        setIsTransferWidgetOpen(false);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [
    userActivityTimestamp,
    isAutoMode,
    setIsAutoMode,
    setIsLeftWidgetOpen,
    setIsRightWidgetSelectorOpen,
    setIsTransferWidgetOpen,
  ]);

  useEffect(() => {
    if (activeClusterId === null) {
      if (clusterAutoCloseTimerRef.current) {
        clearTimeout(clusterAutoCloseTimerRef.current);
        clusterAutoCloseTimerRef.current = null;
      }
      return;
    }

    if (clusterAutoCloseTimerRef.current) {
      clearTimeout(clusterAutoCloseTimerRef.current);
    }
    clusterAutoCloseTimerRef.current = setTimeout(() => {
      setActiveClusterId(null);
    }, 20000);

    return () => {
      if (clusterAutoCloseTimerRef.current) {
        clearTimeout(clusterAutoCloseTimerRef.current);
        clusterAutoCloseTimerRef.current = null;
      }
    };
  }, [activeClusterId, setActiveClusterId]);

  useEffect(() => {
    if (activeClusterId === null) {
      return;
    }

    const handleClickOutside = (e: MouseEvent | TouchEvent) => {
      const target = e.target as HTMLElement;

      const clusterElements = document.querySelectorAll("[data-cluster-id]");
      let clickedOnCluster = false;

      clusterElements.forEach((element) => {
        if (element.contains(target) || element === target) {
          clickedOnCluster = true;
        }
      });

      const expandedCircle = target.closest("[data-expanded-cluster]");
      if (expandedCircle) {
        clickedOnCluster = true;
      }

      if (!clickedOnCluster) {
        setActiveClusterId(null);
        if (clusterAutoCloseTimerRef.current) {
          clearTimeout(clusterAutoCloseTimerRef.current);
          clusterAutoCloseTimerRef.current = null;
        }
      }
    };

    document.addEventListener("mousedown", handleClickOutside);
    document.addEventListener("touchstart", handleClickOutside);

    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
      document.removeEventListener("touchstart", handleClickOutside);
    };
  }, [activeClusterId, setActiveClusterId]);

  useEffect(() => {
    if (!hasInitializedCameraRef.current) {
      return;
    }

    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;
        anchorX: number;
        anchorY: number;
        distance: number;
      }>;
    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;
      anchorX: number;
      anchorY: number;
      distance: number;
    }> = [];
    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 dpr = Math.max(
        1,
        (typeof window !== "undefined" && window.devicePixelRatio) || 1,
      );

      const DEFAULT_LABEL_OFFSET_X = 25;
      const DEFAULT_LABEL_OFFSET_Y = 0;
      const offsetXPixels =
        st.offset_x === 0 && st.offset_y === 0
          ? DEFAULT_LABEL_OFFSET_X
          : st.offset_x;
      const offsetYPixels =
        st.offset_x === 0 && st.offset_y === 0
          ? DEFAULT_LABEL_OFFSET_Y
          : st.offset_y;

      const backendAlign = st?.align ?? 3;

      const fontSizePercent = routeData?.font_size ?? 100;
      const fontScale = fontSizePercent / 100;
      const fontSize = 16 * fontScale;

      const lineHeight = fontSize * 1.2;
      const mainLabelHeight = lineHeight;

      const labelOffsetX = offsetXPixels / scale;
      const labelOffsetY = (offsetYPixels + mainLabelHeight / 2) / scale;

      const textBlockPositionX = rx + labelOffsetX;
      const textBlockPositionY = ry + labelOffsetY;

      const normalizedName = st.name.replace(/\\n|\n/g, "");
      const approximateTextWidth = normalizedName.length * fontSize * 0.6;
      const textWidthInMapCoords = approximateTextWidth / scale;

      let anchorXOffset = 0;
      if (backendAlign === 1) {
        anchorXOffset = 0;
      } else if (backendAlign === 3) {
        anchorXOffset = textWidthInMapCoords;
      } else {
        anchorXOffset = textWidthInMapCoords / 2;
      }

      const anchorX = textBlockPositionX + anchorXOffset;
      const anchorY = textBlockPositionY;

      const distanceInPixels = Math.hypot(offsetXPixels, offsetYPixels);

      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: normalizedName,
        sub: sub ? sub.replace(/\\n|\n/g, "") : sub,
        anchorX: anchorX,
        anchorY: anchorY,
        distance: distanceInPixels,
      });
    }
    return result;
  }, [
    stationData,
    stationDataEn as any,
    stationDataZh as any,
    position.x,
    position.y,
    scale,
    routeData?.center_latitude,
    routeData?.center_longitude,
    routeData?.font_size,
    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,
  ]);

  // Сегментный индекс каждой упорядоченной станции на routePath (canvas-пространство)
  const orderedStationSegs = useMemo(() => {
    if (!orderedRouteStations || !stationData || routePath.length < 4)
      return [] as number[];
    return (orderedRouteStations as any[]).map((ordStation) => {
      const stIdx = stationData.findIndex(
        (s: any) => String(s.id) === String(ordStation.id),
      );
      if (stIdx < 0) return -1;
      const sx = stationPoints[stIdx * 2];
      const sy = stationPoints[stIdx * 2 + 1];
      if (sx === undefined || sy === undefined) return -1;
      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 = ((sx - p1x) * dx + (sy - 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(sx - px, sy - py);
        if (d < bestD) {
          bestD = d;
          best = i / 2;
        }
      }
      return best;
    });
  }, [orderedRouteStations, stationData, stationPoints, routePath]);

  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,
  ]);

  const transformedSightData = useMemo(() => {
    if (!sightData || !routeData) return [];
    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);

    return sightData.map((s: any) => {
      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;

      return {
        ...s,
        longitude: rx,
        latitude: ry,
      };
    });
  }, [
    sightData,
    routeData?.center_latitude,
    routeData?.center_longitude,
    rotationAngle,
  ]);

  const pathPointsForClustering = useMemo(() => {
    const points: { x: number; y: number }[] = [];
    for (let i = 0; i < routePath.length; i += 2) {
      points.push({ x: routePath[i], y: routePath[i + 1] });
    }
    return points;
  }, [routePath]);

  const distanceThreshold = BASE_ICON_SIZE * 3;
  const clusteredSights = useSightClustering(
    transformedSightData,
    distanceThreshold,
    pathPointsForClustering,
  );

  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;
      const dpr = Math.max(1, window.devicePixelRatio || 1);
      debugWebglLog("container resize", {
        changed,
        cssWidth: canvas.clientWidth,
        cssHeight: canvas.clientHeight,
        dpr,
        displayWidth: canvas.width,
        displayHeight: canvas.height,
      });
      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);
    };
  }, []);

  const prevPositionIndexRef = useRef<number>(-1);

  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;

        const curIdx = apiStore.positionIndex;
        const prevIdx = prevPositionIndexRef.current;
        const pathLen = apiStore.route?.path?.length ?? 0;
        const isWrap =
          prevIdx >= 0 &&
          pathLen > 0 &&
          Math.abs(curIdx - prevIdx) > pathLen / 4;
        prevPositionIndexRef.current = curIdx;

        if (apiStore.simulationInstantMove || isWrap) {
          setYellowDotImmediate(rx, ry);
        } else {
          animateYellowDotTo(rx, ry);
        }
      }
    }
  }, [
    routeData?.center_latitude,
    routeData?.center_longitude,
    rotationAngle,
    apiStore?.context?.currentCoordinates?.latitude,
    apiStore?.context?.currentCoordinates?.longitude,
    animateYellowDotTo,
    setYellowDotImmediate,
  ]);

  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);

    let tramSegIndex = getCurrentSegIndex();
    const simulationDirection = apiStore.simulationDirection;

    const dpr = Math.max(1, window.devicePixelRatio || 1);
    const desiredRouteWidthCss = 7;
    const desiredStationDiameterCss = 12;
    const pointOuterSizePx = desiredStationDiameterCss * dpr;
    const pointInnerSizePx = pointOuterSizePx * 0.8;
    const lineWidthPreview =
      (desiredRouteWidthCss * dpr) / Math.max(scale, 1e-6);
    const drawSignature = [
      canvas.width,
      canvas.height,
      routePath.length,
      stationPoints.length,
      sightPoints.length,
      Math.round(scale * 1e4) / 1e4,
      Math.round(lineWidthPreview * 1e4) / 1e4,
    ].join("|");
    const shouldLogDraw = lastDrawLogSignatureRef.current !== drawSignature;
    if (shouldLogDraw) {
      lastDrawLogSignatureRef.current = drawSignature;
      debugWebglLog("drawScene:start", {
        dpr,
        canvasWidth: canvas.width,
        canvasHeight: canvas.height,
        routeVerticesCount: routePath.length / 2,
        stationVerticesCount: stationPoints.length / 2,
        sightVerticesCount: sightPoints.length / 2,
        scale,
        position,
      });
    }

    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 = (desiredRouteWidthCss * dpr) / scale;
      if (shouldLogDraw) {
        debugWebglLog("drawScene:route", {
          desiredRouteWidthCss,
          lineWidthWorldUnits: lineWidth,
        });
      }
      const r1 = ((PATH_COLOR >> 16) & 0xff) / 255;
      const g1 = ((PATH_COLOR >> 8) & 0xff) / 255;
      const b1 = (PATH_COLOR & 0xff) / 255;
      const r2 = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255;
      const g2 = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255;
      const b2 = (UNPASSED_STATION_COLOR & 0xff) / 255;

      const animatedPos = animatedYellowDotPosition;
      if (
        tramSegIndex >= 0 &&
        animatedPos &&
        animatedPos.x !== undefined &&
        animatedPos.y !== undefined
      ) {
        if (simulationDirection === 1) {
          // Вперёд: закрашено от начала до трамвая
          gl.uniform4f(uniforms.u_color, r1, g1, b1, 1);
          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);
          }
          gl.uniform4f(uniforms.u_color, r2, g2, b2, 1);
          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);
          }
        } else {
          // Назад: закрашено от трамвая до конца
          gl.uniform4f(uniforms.u_color, r2, g2, b2, 1);
          const unpassedPoints: number[] = [];
          for (let i = 0; i <= tramSegIndex; i++) {
            unpassedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
          }
          unpassedPoints.push(animatedPos.x, animatedPos.y);
          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);
          }
          gl.uniform4f(uniforms.u_color, r1, g1, b1, 1);
          const passedPoints: number[] = [];
          passedPoints.push(animatedPos.x, animatedPos.y);
          for (let i = tramSegIndex + 1; i < vertexCount; i++) {
            passedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
          }
          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);
          }
        }
      } else {
        // Позиция трамвая неизвестна — рисуем весь маршрут серым
        gl.uniform4f(uniforms.u_color, r2, g2, b2, 1);
        const allPoints: number[] = [];
        for (let i = 0; i < vertexCount; i++) {
          allPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
        }
        if (allPoints.length >= 4) {
          const thickLineVertices = generateThickLine(
            new Float32Array(allPoints),
            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, pointOuterSizePx);
      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, pointInnerSizePx);

      if (
        tramSegIndex >= 0 &&
        orderedRouteStations &&
        stationData &&
        orderedStationSegs.length > 0
      ) {
        const passedPts1: number[] = [];
        const unpassedPts1: number[] = [];
        for (let i = 0; i < orderedRouteStations.length; i++) {
          const orderedStation = (orderedRouteStations as any[])[i];
          const stationSeg = orderedStationSegs[i] ?? -1;
          if (!orderedStation || stationSeg < 0) continue;
          const isPassed =
            simulationDirection === 1
              ? stationSeg < tramSegIndex
              : stationSeg > tramSegIndex;
          const stIdx = stationData.findIndex(
            (s: any) => String(s.id) === String(orderedStation.id),
          );
          if (stIdx < 0) continue;
          const sx = stationPoints[stIdx * 2] as number;
          const sy = stationPoints[stIdx * 2 + 1] as number;
          if (isPassed) {
            passedPts1.push(sx, sy);
          } else {
            unpassedPts1.push(sx, sy);
          }
        }
        if (passedPts1.length > 0) {
          gl.uniform4f(
            u_color_pts,
            ((PATH_COLOR >> 16) & 0xff) / 255,
            ((PATH_COLOR >> 8) & 0xff) / 255,
            (PATH_COLOR & 0xff) / 255,
            1,
          );
          gl.bufferData(
            gl.ARRAY_BUFFER,
            new Float32Array(passedPts1),
            gl.STATIC_DRAW,
          );
          gl.drawArrays(gl.POINTS, 0, passedPts1.length / 2);
        }
        if (unpassedPts1.length > 0) {
          gl.uniform4f(
            u_color_pts,
            ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255,
            ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255,
            (UNPASSED_STATION_COLOR & 0xff) / 255,
            1,
          );
          gl.bufferData(
            gl.ARRAY_BUFFER,
            new Float32Array(unpassedPts1),
            gl.STATIC_DRAW,
          );
          gl.drawArrays(gl.POINTS, 0, unpassedPts1.length / 2);
        }
      } else {
        gl.uniform4f(
          u_color_pts,
          ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255,
          ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255,
          (UNPASSED_STATION_COLOR & 0xff) / 255,
          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);

    let activeStationIndex = -1;
    const tramCoords = apiStore?.context?.currentCoordinates;
    if (
      tramCoords &&
      stationData &&
      stationData.length > 0 &&
      Number.isFinite(tramCoords.latitude) &&
      Number.isFinite(tramCoords.longitude)
    ) {
      let bestD = Infinity;

      const DISTANCE_THRESHOLD = 0.00015;

      for (let i = 0; i < stationData.length; i++) {
        const station = stationData[i];
        if (
          Number.isFinite(station.latitude) &&
          Number.isFinite(station.longitude)
        ) {
          const d = getDistance(
            {
              latitude: tramCoords.latitude,
              longitude: tramCoords.longitude,
            },
            {
              latitude: station.latitude,
              longitude: station.longitude,
            },
          );
          if (d < bestD) {
            bestD = d;
            activeStationIndex = i;
          }
        }
      }

      if (bestD > DISTANCE_THRESHOLD) {
        activeStationIndex = -1;
      }
    }

    if (stationData && stationData.length > 0) {
      if (activeStationIndex >= 0 && stationData[activeStationIndex]) {
        const activeId = String(stationData[activeStationIndex].id);
        if (activeId !== nearestStationId) {
          setNearestStationId(activeId);
        }
      } else if (nearestStationId !== null) {
        setNearestStationId(null);
      }
    }

    const passedStationIds = new Set<string>();
    const unpassedStationIds = new Set<string>();

    if (
      tramSegIndex >= 0 &&
      orderedRouteStations &&
      orderedStationSegs.length === orderedRouteStations.length
    ) {
      for (let i = 0; i < orderedRouteStations.length; i++) {
        const station = (orderedRouteStations as any[])[i];
        const seg = orderedStationSegs[i] ?? -1;
        if (!station || seg < 0) continue;
        const isPassed =
          simulationDirection === 1 ? seg < tramSegIndex : seg > tramSegIndex;
        if (isPassed) passedStationIds.add(String(station.id));
        else unpassedStationIds.add(String(station.id));
      }
    } else {
      if (orderedRouteStations) {
        (orderedRouteStations as any[]).forEach((station) => {
          unpassedStationIds.add(String(station.id));
        });
      }
    }

    const passedStations: number[] = [];
    const unpassedStations: number[] = [];
    const activeStations: number[] = [];
    for (
      let i = 0, idx = 0;
      i < stationData.length && idx < stationPoints.length;
      i++, idx += 2
    ) {
      const sx = stationPoints[idx];
      const sy = stationPoints[idx + 1];
      const stationId = String(stationData[i].id);

      if (i === activeStationIndex) {
        activeStations.push(sx, sy);
      } else if (passedStationIds.has(stationId)) {
        passedStations.push(sx, sy);
      } else {
        unpassedStations.push(sx, sy);
      }
    }

    const outlineSize = pointOuterSizePx;
    const coreSize = pointInnerSizePx;
    if (shouldLogDraw) {
      debugWebglLog("drawScene:stations", {
        pointOuterSizePx: outlineSize,
        pointInnerSizePx: coreSize,
      });
    }

    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 (activeStations.length) {
      gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer);
      gl.bufferData(
        gl.ARRAY_BUFFER,
        toPointsArray(activeStations),
        gl.STREAM_DRAW,
      );
      gl.enableVertexAttribArray(a_pos_pts);
      gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0);

      gl.uniform1f(u_pointSize, outlineSize + 1);
      gl.uniform4f(
        u_color_pts,
        ((BACKGROUND_COLOR >> 16) & 255) / 255,
        ((BACKGROUND_COLOR >> 8) & 255) / 255,
        (BACKGROUND_COLOR & 255) / 255,
        1,
      );
      gl.drawArrays(gl.POINTS, 0, activeStations.length / 2);

      gl.uniform1f(u_pointSize, coreSize + 1);
      gl.uniform4f(
        u_color_pts,
        ((BUS_COLOR >> 16) & 255) / 255,
        ((BUS_COLOR >> 8) & 255) / 255,
        (BUS_COLOR & 255) / 255,
        1,
      );
      gl.drawArrays(gl.POINTS, 0, activeStations.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 = orderedRouteStations?.[0]
          ? stationData.find(
              (station: any) =>
                station.id.toString() === String(orderedRouteStations[0].id),
            )
          : stationData.find(
              (station: any) =>
                station.id.toString() === apiStore.context?.startStopId,
            );
        const endStationData = orderedRouteStations?.length
          ? stationData.find(
              (station: any) =>
                station.id.toString() ===
                String(
                  orderedRouteStations[orderedRouteStations.length - 1].id,
                ),
            )
          : stationData.find(
              (station: any) =>
                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 &&
                  (simulationDirection === 1
                    ? seg < tramSegIndex
                    : 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 &&
                  (simulationDirection === 1
                    ? seg < tramSegIndex
                    : 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);

          const terminalOuterSizePx = pointOuterSizePx * 0.9;
          const terminalInnerSizePx = pointOuterSizePx * 0.55;
          gl.uniform1f(u_pointSize, terminalOuterSizePx);

          const r_passed = ((PATH_COLOR >> 16) & 0xff) / 255;
          const g_passed = ((PATH_COLOR >> 8) & 0xff) / 255;
          const b_passed = (PATH_COLOR & 0xff) / 255;

          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;

          if (startStationData && endStationData) {
            const startIsPassed =
              simulationDirection === 1 ? true : isStartPassed;
            const endIsPassed = simulationDirection === -1 ? true : isEndPassed;
            gl.uniform4f(
              u_color_pts,
              startIsPassed ? r_passed : r_unpassed,
              startIsPassed ? g_passed : g_unpassed,
              startIsPassed ? b_passed : b_unpassed,
              1.0,
            );
            gl.drawArrays(gl.POINTS, 0, 1);
            gl.uniform4f(
              u_color_pts,
              endIsPassed ? r_passed : r_unpassed,
              endIsPassed ? g_passed : g_unpassed,
              endIsPassed ? b_passed : b_unpassed,
              1.0,
            );
            gl.drawArrays(gl.POINTS, 1, 1);
          } else {
            const isStartStation = startStationData !== undefined;
            const isPassed = startStationData ? isStartPassed : isEndPassed;
            if (isStartStation || isPassed) {
              gl.uniform4f(u_color_pts, r_passed, g_passed, b_passed, 1.0);
            } else {
              gl.uniform4f(
                u_color_pts,
                r_unpassed,
                g_unpassed,
                b_unpassed,
                1.0,
              );
            }
            gl.drawArrays(gl.POINTS, 0, terminalStations.length / 2);
          }

          gl.uniform1f(u_pointSize, terminalInnerSizePx);
          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);
        }
      }
    }
  }, [
    routePath,
    stationPoints,
    sightPoints,
    position.x,
    position.y,
    scale,
    animatedYellowDotPosition?.x,
    animatedYellowDotPosition?.y,
    nearestStationId,
    currentStationId,
    orderedRouteStations,
    orderedStationSegs,
    apiStore.simulationDirection,
  ]);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas || hasInitializedCameraRef.current) 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;

    routeBoundsRef.current = { minX, maxX, minY, maxY };

    const centerX = (minX + maxX) / 2;
    const centerY = (minY + maxY) / 2;
    const worldWidth = Math.max(1, maxX - minX);
    const worldHeight = Math.max(1, maxY - minY);
    const fitPadding = 0.1;
    const fitScale = Math.min(
      (canvas.width * (1 - fitPadding)) / worldWidth,
      (canvas.height * (1 - fitPadding)) / worldHeight,
    );

    const maxRouteScale = scaleLimitsRef.current.max;
    if (
      !(
        typeof maxRouteScale === "number" &&
        Number.isFinite(maxRouteScale) &&
        maxRouteScale > 0
      )
    ) {
      return;
    }
    const initialScale = maxRouteScale;
    const clampedScale = clampScale(initialScale);

    const initialPosition = {
      x: canvas.width / 2 - centerX * clampedScale,
      y: canvas.height / 2 - centerY * clampedScale,
    };
    setScaleRef.current(clampedScale);
    setPositionRef.current(initialPosition);
    hasInitializedCameraRef.current = true;
    if (
      customSightIconBaseScaleRef.current == null &&
      Number.isFinite(fitScale) &&
      fitScale > 0
    ) {
      customSightIconBaseScaleRef.current = CUSTOM_SIGHT_ICON_BASE_SCALE;
      debugWebglLog("custom sight base scale initialized", {
        baseScale: CUSTOM_SIGHT_ICON_BASE_SCALE,
        currentScale: clampedScale,
      });
      forceSightIconScaleRender((version) => version + 1);
    }
    cameraAnimationStore.syncState(initialPosition, clampedScale);
  }, [routePath, cameraAnimationStore]);

  useEffect(() => {
    if (!stationData?.length) return;

    const fontSizePercent =
      routeData?.font_size ?? originalRouteData?.font_size ?? 100;
    const fontScale = fontSizePercent / 100;
    const baseStationIconSizePx = 16 * fontScale * 1.2;

    for (const station of stationData) {
      const hasCustomStationIcon = !isMediaIdEmpty(station?.icon ?? null);
      if (!hasCustomStationIcon) continue;

      const iconSizePx = Math.round(baseStationIconSizePx);
      debugWebglLog("custom station icon size", {
        stationId: station?.id,
        fontSizePercent,
        baseStationIconSizePx,
        iconSizePx,
      });
    }
  }, [stationData, routeData?.font_size, originalRouteData?.font_size]);

  useEffect(() => {
    if (!transformedSightData?.length) return;

    for (const sight of transformedSightData) {
      const shouldUseCustomSightIcon =
        sight.is_default_icon === false && !isMediaIdEmpty(sight.icon ?? null);
      if (!shouldUseCustomSightIcon) continue;

      const customSightIconScaleFactor =
        scale / Math.max(customSightIconBaseScaleRef.current ?? 1, 1e-6);
      const sightIconSizePercent = resolveSightIconSizePercent(sight);
      const iconSizePx =
        SIGHT_ICON_BASE_SIZE *
        clamp(sightIconSizePercent / 100, 0.1, 10) *
        customSightIconScaleFactor;

      debugWebglLog("custom sight icon size", {
        sightId: sight.id,
        cameraScale: scale,
        baseScale: customSightIconBaseScaleRef.current,
        scaleFactor: customSightIconScaleFactor,
        iconPercent: sightIconSizePercent,
        iconSizePx,
      });
    }
  }, [transformedSightData, scale, resolveSightIconSizePercent]);

  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<number, { x: number; y: number }>();
    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) {
        isPinching = false;
        pinchStart = null;
        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),
            };
            const clampedPos = clampPosition(newPosition, newScale);
            setPositionRef.current(clampedPos);
            setScaleRef.current(newScale);
            positionRef.current = clampedPos;
            scaleRef.current = newScale;
          }
        }
      } else if (activePointers.size === 1) {
        isPinching = false;
        pinchStart = null;

        if (isDragging) {
          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"
          ) {
            return;
          }

          const dx = p.x - startMouse.x;
          const dy = p.y - startMouse.y;

          const newPos = { x: startPos.x + dx, y: startPos.y + dy };
          const clampedPos = clampPosition(newPos, scaleRef.current);
          setPositionRef.current(clampedPos);
          positionRef.current = clampedPos;
        }
      }
    };

    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),
      };
      const clampedPos = clampPosition(newPosition, newScale);
      setScaleRef.current(newScale);
      setPositionRef.current(clampedPos);
      scaleRef.current = newScale;
      positionRef.current = clampedPos;
    };

    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,
    clampPosition,
  ]);

  return (
    <div
      className="map-layer"
      style={{ width: "100%", height: "100vh", position: "relative" }}
    >
      <canvas
        ref={canvasRef}
        style={{ width: "100%", height: "100%", display: "block", zIndex: 0 }}
      />
      <div
        style={{
          position: "absolute",
          inset: 0,
          pointerEvents: "none",
          zIndex: 10005,
        }}
      >
        {stationLabels.map((l, idx) => {
          let station = stationData?.[idx];

          const backendAlign = station?.align ?? 3;
          const anchor = getAnchorFromOffset(backendAlign);
          const transformCss = `translate(${-anchor.x * 100}%, ${
            -anchor.y * 100
          }%)`;

          const fontSizePercent = routeData?.font_size ?? 100;
          const fontScale = fontSizePercent / 100;

          const clampedScale = Math.min(Math.max(scale, 1), 3);
          const scaleFactor = 1 + (clampedScale - 1) * 0.4;

          const primaryFontSize = 16 * fontScale * scaleFactor;
          const secondaryFontSize = 13 * fontScale * scaleFactor;
          const secondaryMarginTop = 5 * fontScale * scaleFactor;
          const secondaryLineHeight = 1.2 * scaleFactor;

          const alignment =
            backendAlign === 1
              ? "left"
              : backendAlign === 3
                ? "right"
                : "center";

          const secondaryPositionStyle =
            alignment === "left"
              ? { left: 0, transform: "none" }
              : alignment === "right"
                ? { right: 0, transform: "none" }
                : { left: "50%", transform: "translateX(-50%)" };

          const isMediaIdEmptyResult = isMediaIdEmpty(station?.icon);
          const iconSrc = isMediaIdEmptyResult
            ? null
            : buildMediaDownloadUrl(mediaBaseUrl, station!.icon!, mediaToken);
          const iconSizePx = Math.round(primaryFontSize * 1.2);

          return (
            <div
              key={idx}
              style={{
                position: "absolute",
                left: l.x,
                top: l.y,
                transform: transformCss,
                color: "#fff",
                fontFamily: "Roboto, sans-serif",
                textAlign: "left",
                pointerEvents: "none",
              }}
            >
              <div
                style={{
                  position: "relative",
                  display: "inline-flex",
                  alignItems: "center",
                  gap: iconSrc ? 6 : 0,
                  pointerEvents: "none",
                }}
              >
                {iconSrc ? (
                  <img
                    src={iconSrc}
                    alt=""
                    style={{
                      width: iconSizePx,
                      height: iconSizePx,
                      flexShrink: 0,
                      pointerEvents: "none",
                    }}
                  />
                ) : null}
                <div
                  style={{
                    fontWeight: 700,
                    fontSize: primaryFontSize,
                    textShadow: "0 0 4px rgba(0,0,0,0.6)",
                    pointerEvents: "none",
                    whiteSpace: "nowrap",
                  }}
                >
                  {l.name}
                </div>
                {l.sub ? (
                  <div
                    style={{
                      position: "absolute",
                      top: "100%",
                      marginTop: -1 * secondaryMarginTop,
                      fontWeight: 400,
                      fontSize: secondaryFontSize,
                      lineHeight: secondaryLineHeight,
                      color: "#CBCBCB",
                      textShadow: "0 0 3px rgba(0,0,0,0.4)",
                      whiteSpace: "nowrap",
                      ...secondaryPositionStyle,
                      pointerEvents: "none",
                    }}
                  >
                    {l.sub}
                  </div>
                ) : null}
              </div>
            </div>
          );
        })}
        {clusteredSights.map((item) => {
          const dpr = Math.max(
            1,
            (typeof window !== "undefined" && window.devicePixelRatio) || 1,
          );

          if (item.type === "point") {
            const s = item.data;
            const rx = s.longitude;
            const ry = s.latitude;
            const isSelected = selectedSightId === String(s.id);
            const hasIcon = !isMediaIdEmpty(s.icon ?? null);
            const isCustomIcon = s.is_default_icon === false && hasIcon;
            const hasSelectedAltIcon =
              isSelected && isCustomIcon && !isMediaIdEmpty(s.alt_icon ?? null);
            const defaultCustomIcon = isCustomIcon;
            const shouldUseCustomSightIcon =
              hasSelectedAltIcon || defaultCustomIcon;
            const customSightIconScaleFactor = shouldUseCustomSightIcon
              ? scale / Math.max(customSightIconBaseScaleRef.current ?? 1, 1e-6)
              : 1;
            const sightIconSizePercent = resolveSightIconSizePercent(s);
            const sightIconUrl = hasSelectedAltIcon
              ? buildMediaDownloadUrl(
                  mediaBaseUrl,
                  String(s.alt_icon),
                  mediaToken,
                )
              : defaultCustomIcon
                ? buildMediaDownloadUrl(
                    mediaBaseUrl,
                    String(s.icon),
                    mediaToken,
                  )
                : SIGHT_ICON_URL;
            const iconSize =
              SIGHT_ICON_BASE_SIZE *
              clamp(sightIconSizePercent / 100, 0.1, 10) *
              customSightIconScaleFactor;

            const screenX = (rx * scale + position.x) / dpr;
            const screenY = (ry * scale + position.y) / dpr;
            const iconLeft = screenX - iconSize;
            const iconTop = screenY - iconSize;

            const handleSightClick = () => {
              setIsGovernorWidgetOpen(false);
              setSelectedSightId(String(s.id));
              setIsManualSelection(true);
              setIsRightWidgetSelectorOpen(false);
              store.closeGovernorModal();
            };

            const handleSightTouch = (e: React.TouchEvent) => {
              e.stopPropagation();
              handleSightClick();
            };

            return (
              <div
                key={`sight-${s.id}`}
                onClick={handleSightClick}
                onTouchEnd={handleSightTouch}
                style={{
                  position: "absolute",
                  left: iconLeft,
                  top: iconTop,
                  height: iconSize,
                  display: "flex",
                  alignItems: "center",
                  pointerEvents: "auto",
                  cursor: "pointer",
                  userSelect: "none",
                  touchAction: "none",
                  ...(apiStore.showHitboxes && {
                    outline: "2px solid rgba(0,255,0,0.8)",
                    outlineOffset: "2px",
                    backgroundColor: "rgba(0,255,0,0.08)",
                  }),
                }}
              >
                <div
                  style={{
                    position: "relative",
                    width: iconSize,
                    height: iconSize,
                    flexShrink: 0,
                  }}
                >
                  <img
                    src={sightIconUrl}
                    alt=""
                    style={{
                      display: "block",
                      pointerEvents: "none",
                      width: "100%",
                      height: "100%",
                      filter:
                        isSelected && !hasSelectedAltIcon
                          ? YELLOW_ICON_FILTER
                          : "none",
                    }}
                  />
                </div>
              </div>
            );
          } else {
            const cluster = item.data;
            const rx = cluster.longitude;
            const ry = cluster.latitude;

            const screenX = (rx * scale + position.x) / dpr;
            const screenY = (ry * scale + position.y) / dpr;

            const isExpanded = activeClusterId === cluster.id;
            const selectedSightInCluster = cluster.sights.find(
              (sight) => selectedSightId === String(sight.id),
            );
            const selectedHasIconInCluster =
              selectedSightInCluster != null &&
              !isMediaIdEmpty(selectedSightInCluster.icon ?? null);
            const selectedIsCustomInCluster =
              selectedSightInCluster?.is_default_icon === false &&
              selectedHasIconInCluster;

            const iconSizePercent = resolveSightIconSizePercent(
              cluster.sights[0],
            );
            const clusterCustomScaleFactor = selectedIsCustomInCluster
              ? scale / Math.max(customSightIconBaseScaleRef.current ?? 1, 1e-6)
              : 1;
            const iconSize =
              SIGHT_ICON_BASE_SIZE *
              clamp(iconSizePercent / 100, 0.1, 10) *
              clusterCustomScaleFactor;

            const iconLeft = screenX - iconSize;
            const iconTop = screenY - iconSize;
            const hasSelectedAltIconInCluster =
              selectedSightInCluster != null &&
              selectedIsCustomInCluster &&
              !isMediaIdEmpty(selectedSightInCluster.alt_icon ?? null);
            const clusterIconUrl = hasSelectedAltIconInCluster
              ? buildMediaDownloadUrl(
                  mediaBaseUrl,
                  String(selectedSightInCluster?.alt_icon),
                  mediaToken,
                )
              : selectedIsCustomInCluster
                ? buildMediaDownloadUrl(
                    mediaBaseUrl,
                    String(selectedSightInCluster?.icon),
                    mediaToken,
                  )
                : SIGHT_ICON_URL;

            const handleClusterClick = (
              e: React.MouseEvent | React.TouchEvent,
            ) => {
              e.stopPropagation();
              if (isExpanded) {
                setActiveClusterId(null);
              } else {
                setActiveClusterId(cluster.id);
              }
            };

            const handleCircleInteraction = () => {
              if (isExpanded && clusterAutoCloseTimerRef.current) {
                clearTimeout(clusterAutoCloseTimerRef.current);
                clusterAutoCloseTimerRef.current = setTimeout(() => {
                  setActiveClusterId(null);
                }, 20000);
              }
            };

            const handleSightInClusterClick = (
              sightId: string,
              e: React.MouseEvent | React.TouchEvent,
            ) => {
              e.stopPropagation();
              setIsGovernorWidgetOpen(false);
              setSelectedSightId(sightId);
              setIsManualSelection(true);
              setIsRightWidgetSelectorOpen(false);
              store.closeGovernorModal();
              setActiveClusterId(null);
            };

            const hasSelectedSight = selectedSightId
              ? cluster.sights.some(
                  (sight) => String(sight.id) === selectedSightId,
                )
              : false;

            const badgeColor = "var(--carrier-main, #006F3A)";
            const listPanelWidth = 200;
            const listItemHeight = 30;
            const listMaxHeight = 250;
            const hasMoreThanTwo = cluster.sights.length > 2;
            const shouldShowScrollbar = hasMoreThanTwo;

            return (
              <React.Fragment key={`cluster-${cluster.id}`}>
                {!isExpanded ? (
                  <div
                    data-cluster-id={cluster.id}
                    onClick={handleClusterClick}
                    onTouchEnd={handleClusterClick}
                    style={{
                      position: "absolute",
                      left: iconLeft,
                      top: iconTop,
                      width: iconSize,
                      height: iconSize,
                      pointerEvents: "auto",
                      cursor: "pointer",
                      userSelect: "none",
                      touchAction: "none",
                      zIndex: 10,
                      ...(apiStore.showHitboxes && {
                        outline: "2px solid rgba(255,165,0,0.8)",
                        outlineOffset: "2px",
                        backgroundColor: "rgba(255,165,0,0.08)",
                      }),
                    }}
                  >
                    <div
                      style={{
                        position: "absolute",
                        width: iconSize,
                        height: iconSize,
                        flexShrink: 0,
                      }}
                    >
                      <img
                        src={clusterIconUrl}
                        alt=""
                        style={{
                          display: "block",
                          pointerEvents: "none",
                          width: "100%",
                          height: "100%",
                          filter: hasSelectedSight
                            ? hasSelectedAltIconInCluster
                              ? "none"
                              : YELLOW_ICON_FILTER
                            : "none",
                        }}
                      />
                      <div
                        style={{
                          position: "absolute",
                          top: -6,
                          right: -6,
                          zIndex: 1,
                          width: 15,
                          height: 15,
                          borderRadius: "10px",
                          backgroundColor: badgeColor,
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "center",
                          color: "white",
                          fontSize: 12,
                          fontWeight: "bold",
                          pointerEvents: "none",
                          padding: "0 6px",
                        }}
                      >
                        {cluster.count}
                      </div>
                    </div>
                  </div>
                ) : (
                  <div
                    data-expanded-cluster={cluster.id}
                    onTouchStart={handleCircleInteraction}
                    onMouseMove={handleCircleInteraction}
                    style={{
                      position: "absolute",
                      left: screenX - iconSize,
                      top: screenY - iconSize,
                      display: "flex",
                      alignItems: "flex-start",
                      pointerEvents: "auto",
                      zIndex: 100000000000000,
                      ...(apiStore.showHitboxes && {
                        outline: "2px solid rgba(0,180,255,0.8)",
                        outlineOffset: "2px",
                        backgroundColor: "rgba(0,180,255,0.08)",
                      }),
                    }}
                  >
                    <div
                      style={{
                        position: "relative",
                        marginRight: 8,
                        cursor: "pointer",
                      }}
                      onClick={handleClusterClick}
                    >
                      <img
                        src={clusterIconUrl}
                        alt=""
                        width={iconSize}
                        height={iconSize}
                        style={{
                          display: "block",
                          pointerEvents: "none",
                          filter: hasSelectedSight
                            ? hasSelectedAltIconInCluster
                              ? "none"
                              : YELLOW_ICON_FILTER
                            : "none",
                        }}
                      />
                      <div
                        style={{
                          position: "absolute",
                          top: -6,
                          right: -6,
                          width: 15,
                          height: 15,
                          borderRadius: "10px",
                          backgroundColor: badgeColor,
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "center",
                          color: "white",
                          fontSize: 12,
                          fontWeight: "bold",
                          pointerEvents: "none",
                          padding: "0 6px",
                        }}
                      >
                        {cluster.count}
                      </div>
                    </div>

                    <div
                      className="cluster-sights-list"
                      style={{
                        background: `linear-gradient(to bottom right, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%), rgba(var(--carrier-main-rgb, 0, 111, 58), 0.4)`,
                        backdropFilter: "blur(10px)",
                        borderRadius: "8px",
                        width: listPanelWidth,
                        maxHeight: hasMoreThanTwo ? listMaxHeight : undefined,
                        boxShadow:
                          "inset 0 0 0 1px rgba(255, 255, 255, 0.3), inset 4px 4px 12px 0 rgba(255, 255, 255, 0.12)",
                        display: "flex",
                        flexDirection: "column",
                      }}
                      onClick={(e) => e.stopPropagation()}
                    >
                      <OverlayScrollbarsWrapper
                        overflowY={shouldShowScrollbar ? "scroll" : "hidden"}
                        overflowX="hidden"
                        scrollbarVisibility={
                          shouldShowScrollbar ? "visible" : "hidden"
                        }
                        style={{
                          padding: "8px 13px",
                          height: "100%",

                          boxSizing: "border-box",
                        }}
                        {...({} as any)}
                      >
                        {cluster.sights.map((sight, index) => {
                          const isSelected =
                            selectedSightId === String(sight.id);
                          const getSightName = () => {
                            const originalSight =
                              selectedLanguage === "ru"
                                ? routeSights?.find((s) => s.id === sight.id)
                                : selectedLanguage === "en"
                                  ? routeSightsEn?.find(
                                      (s) => s.id === sight.id,
                                    )
                                  : routeSightsZh?.find(
                                      (s) => s.id === sight.id,
                                    );

                            if (originalSight?.short_name) {
                              return originalSight.short_name;
                            }

                            if (!originalSight?.left_article) {
                              return (
                                sight.name ||
                                `Достопримечательность ${index + 1}`
                              );
                            }

                            const leftArticleData =
                              selectedLanguage === "ru"
                                ? sightArticles.get(
                                    originalSight.left_article + "_ru",
                                  )
                                : selectedLanguage === "en"
                                  ? sightArticlesEn.get(
                                      originalSight.left_article + "_en",
                                    )
                                  : sightArticlesZh.get(
                                      originalSight.left_article + "_zh",
                                    );

                            return (
                              leftArticleData?.heading ||
                              sight.name ||
                              `Достопримечательность ${index + 1}`
                            );
                          };

                          const sightName = getSightName();

                          return (
                            <div
                              key={`cluster-sight-${sight.id}`}
                              data-cluster-id={cluster.id}
                              onClick={(e) =>
                                handleSightInClusterClick(String(sight.id), e)
                              }
                              onTouchEnd={(e) =>
                                handleSightInClusterClick(String(sight.id), e)
                              }
                              style={{
                                display: "flex",
                                alignItems: "center",
                                height: `${listItemHeight}px`,
                                cursor: "pointer",
                                userSelect: "none",
                                touchAction: "none",
                                padding: "0 4px",
                                zIndex: 1000000000000,
                                borderBottom:
                                  "1px solid rgba(255, 255, 255, 0.1)",
                                transition: "background-color 0.2s",
                              }}
                            >
                              {(() => {
                                const hasRowIcon = !isMediaIdEmpty(
                                  sight.icon ?? null,
                                );
                                const isRowCustomIcon =
                                  sight.is_default_icon === false && hasRowIcon;
                                const hasRowAltIcon =
                                  isSelected &&
                                  isRowCustomIcon &&
                                  !isMediaIdEmpty(sight.alt_icon ?? null);
                                const rowIconUrl = hasRowAltIcon
                                  ? buildMediaDownloadUrl(
                                      mediaBaseUrl,
                                      String(sight.alt_icon),
                                      mediaToken,
                                    )
                                  : isRowCustomIcon
                                    ? buildMediaDownloadUrl(
                                        mediaBaseUrl,
                                        String(sight.icon),
                                        mediaToken,
                                      )
                                    : SIGHT_ICON_URL;
                                return (
                                  <img
                                    src={rowIconUrl}
                                    alt=""
                                    width={14}
                                    height={14}
                                    style={{
                                      display: "block",
                                      marginRight: "8px",
                                      flexShrink: 0,
                                      filter:
                                        isSelected && !hasRowAltIcon
                                          ? YELLOW_ICON_FILTER
                                          : "none",
                                    }}
                                  />
                                );
                              })()}
                              <span
                                style={{
                                  color: "white",
                                  fontSize: "12px",
                                  lineHeight: "1.5",
                                  fontWeight: isSelected ? "600" : "400",
                                  overflow: "hidden",
                                  textOverflow: "ellipsis",
                                  whiteSpace: "nowrap",
                                  flex: 1,
                                }}
                              >
                                {sightName}
                              </span>
                            </div>
                          );
                        })}
                      </OverlayScrollbarsWrapper>
                    </div>
                  </div>
                )}
              </React.Fragment>
            );
          }
        })}

        {(() => {
          if (!routeData) return null;
          const centerLat = routeData.center_latitude;
          const centerLon = routeData.center_longitude;
          if (centerLat === undefined || centerLon === undefined) return null;

          if (!apiStore?.context?.currentCoordinates) return null;

          const rx = animatedYellowDotPosition.x;
          const ry = animatedYellowDotPosition.y;
          if (rx === 0 && ry === 0) return null;

          const dpr2 = Math.max(
            1,
            (typeof window !== "undefined" && window.devicePixelRatio) || 1,
          );
          const cosR = Math.cos(rotationAngle);
          const sinR = Math.sin(rotationAngle);
          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,
              align: st.align,
            };
          });
          let tramSegIndex = getCurrentSegIndex();
          if (tramSegIndex < 0 && 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 = 100;
            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;
                let md = Infinity;

                const LABEL_WIDTH_PX = 120;

                const labelWidthMap = LABEL_WIDTH_PX / scale;

                for (const st of stationsForAngle) {
                  const align = st.align ?? 3;

                  let lx = st.longitude;
                  let ly = st.latitude;

                  if (align === 1 || align === 0) {
                    lx += labelWidthMap / 2;
                  } else if (align === 3) {
                    lx -= labelWidthMap / 2;
                  }

                  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;
          })();

          const offsetBack = 5 / scale;
          const adjustedRx = rx - Math.cos(optimalAngle) * offsetBack;
          const adjustedRy = ry - Math.sin(optimalAngle) * offsetBack;
          const screenX = (adjustedRx * scale + position.x) / dpr2;
          const screenY = (adjustedRy * scale + position.y) / dpr2;

          const screenXDot = (rx * scale + position.x) / dpr2;
          const screenYDot = (ry * scale + position.y) / dpr2;

          let activeStationIndex = -1;
          const tramCoords = apiStore?.context?.currentCoordinates;

          if (
            tramCoords &&
            stationData &&
            stationData.length > 0 &&
            Number.isFinite(tramCoords.latitude) &&
            Number.isFinite(tramCoords.longitude)
          ) {
            let bestD = Infinity;
            const DISTANCE_THRESHOLD = 0.00015;

            for (let i = 0; i < stationData.length; i++) {
              const station = stationData[i];
              if (
                Number.isFinite(station.latitude) &&
                Number.isFinite(station.longitude)
              ) {
                const d = getDistance(
                  {
                    latitude: tramCoords.latitude,
                    longitude: tramCoords.longitude,
                  },
                  {
                    latitude: station.latitude,
                    longitude: station.longitude,
                  },
                );
                if (d < bestD) {
                  bestD = d;
                  activeStationIndex = i;
                }
              }
            }

            if (bestD > DISTANCE_THRESHOLD) {
              activeStationIndex = -1;
            }
          }

          const isAtStation = activeStationIndex !== -1;

          const dotColor = isAtStation ? "#fcd500" : "white";

          return (
            <>
              <div
                style={{
                  position: "absolute",
                  left: screenXDot,
                  top: screenYDot,
                  width: "11px",
                  height: "11px",
                  backgroundColor: dotColor,
                  borderRadius: "50%",
                  transform: "translate(-50%, -50%)",
                  zIndex: 10001,
                  pointerEvents: "none",
                  boxShadow: "0 0 4px rgba(0,0,0,0.3)",
                  transition: "background-color 0.3s ease",
                }}
              />
              <TramIconWebGL
                x={screenX}
                y={screenY}
                optimalAngle={optimalAngle}
                scale={scale}
                routeIcon={(routeData as { icon?: string })?.icon ?? null}
              />
            </>
          );
        })()}
      </div>
    </div>
  );
});

export default WebGLMap;
