(() => {
  "use strict";
  const token = new URLSearchParams(window.location.search).get("token") || "";
  const svgNs = "http://www.w3.org/2000/svg";
  const chartWidth = 640, chartHeight = 244, chartLeft = 58, chartRight = 16, chartTop = 8, chartBottom = 36;
  const chartPlotWidth = chartWidth - chartLeft - chartRight, chartPlotHeight = chartHeight - chartTop - chartBottom;
  const state = { staticSnapshot: null, frame: null, history: [], refreshRateHz: 10, ended: false, activeTab: "overview", redrawEnabled: {}, chartViewports: {} };
  const liveState = document.getElementById("live-state");
  const occupancyCanvas = document.getElementById("occupancy-grid");
  const overlay = document.getElementById("world-overlay");
  const worldLegend = document.getElementById("world-legend");

  function authorized(path) { return path + "?token=" + encodeURIComponent(token); }
  function finite(value) { return typeof value === "number" && Number.isFinite(value); }
  function element(name, className, text) { const node = document.createElement(name); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; }
  function svg(name, attributes) { const node = document.createElementNS(svgNs, name); Object.keys(attributes || {}).forEach(key => node.setAttribute(key, String(attributes[key]))); return node; }
  function safeArray(value) { return Array.isArray(value) ? value : []; }
  function setLiveState(text, className) { liveState.textContent = text; liveState.className = className || ""; }

  function installTabs() {
    document.querySelectorAll("nav button[data-tab]").forEach(button => button.addEventListener("click", () => {
      state.activeTab = button.dataset.tab;
      document.querySelectorAll("nav button[data-tab]").forEach(item => item.setAttribute("aria-selected", String(item === button)));
      document.querySelectorAll("main > section").forEach(section => { section.hidden = section.id !== state.activeTab; });
      renderActiveTab();
    }));
  }

  function getBounds() {
    const values = [];
    const include = points => safeArray(points).forEach(point => { if (point && finite(point.x) && finite(point.y)) values.push(point); });
    const snapshot = state.staticSnapshot || {};
    include([ { x: snapshot.worldBounds && snapshot.worldBounds.xMin, y: snapshot.worldBounds && snapshot.worldBounds.yMin }, { x: snapshot.worldBounds && snapshot.worldBounds.xMax, y: snapshot.worldBounds && snapshot.worldBounds.yMax } ]);
    safeArray(snapshot.staticPolylines).forEach(line => include(line.points)); safeArray(snapshot.directionSegments).forEach(line => include(line.points)); safeArray(snapshot.staticMarkers).forEach(marker => include([marker.position]));
    const frame = state.frame || {}; safeArray(frame.dynamicPolylines).forEach(line => include(line.points)); safeArray(frame.dynamicMarkers).forEach(marker => include([marker.position])); include([frame.vehiclePose]);
    if (!values.length) return { xMin: 0, xMax: 1, yMin: 0, yMax: 1 };
    let xMin = Math.min(...values.map(point => point.x)), xMax = Math.max(...values.map(point => point.x)), yMin = Math.min(...values.map(point => point.y)), yMax = Math.max(...values.map(point => point.y));
    const pad = Math.max(xMax - xMin, yMax - yMin, 1) * .05; return { xMin: xMin - pad, xMax: xMax + pad, yMin: yMin - pad, yMax: yMax + pad };
  }

  function drawOccupancy(bounds) {
    const grid = state.staticSnapshot && state.staticSnapshot.occupancyGrid;
    const box = occupancyCanvas.getBoundingClientRect(); const scale = window.devicePixelRatio || 1;
    occupancyCanvas.width = Math.max(1, Math.round(box.width * scale)); occupancyCanvas.height = Math.max(1, Math.round(box.height * scale));
    const context = occupancyCanvas.getContext("2d"); context.setTransform(scale, 0, 0, scale, 0, 0); context.clearRect(0, 0, box.width, box.height);
    if (!grid || !grid.occupancyBitsBase64 || !grid.rows || !grid.columns) return;
    const bits = Uint8Array.from(atob(grid.occupancyBitsBase64), character => character.charCodeAt(0));
    const factor = Math.min(box.width / (bounds.xMax - bounds.xMin), box.height / (bounds.yMax - bounds.yMin)); const offsetX = (box.width - (bounds.xMax - bounds.xMin) * factor) / 2; const offsetY = (box.height - (bounds.yMax - bounds.yMin) * factor) / 2;
    context.fillStyle = "#edf0f2";
    for (let row = 0; row < grid.rows; row += 1) for (let column = 0; column < grid.columns; column += 1) { const index = row * grid.columns + column; if ((bits[index >> 3] & (1 << (index & 7))) !== 0) { const x = offsetX + (grid.bounds.xMin + column * grid.resolutionMeters - bounds.xMin) * factor; const y = offsetY + (bounds.yMax - (grid.bounds.yMin + (row + 1) * grid.resolutionMeters)) * factor; context.fillRect(x, y, grid.resolutionMeters * factor, grid.resolutionMeters * factor); } }
  }

  function niceWorldStep(span, targetTicks = 7) {
    if (!finite(span) || span <= 0) return 1;
    const rough = span / Math.max(2, targetTicks);
    const magnitude = Math.pow(10, Math.floor(Math.log10(rough)));
    const normalized = rough / magnitude;
    const factor = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
    return factor * magnitude;
  }

  function worldMetrics(bounds) {
    const box = overlay.getBoundingClientRect();
    const pixelWidth = box.width > 0 ? box.width : 800;
    const pixelHeight = box.height > 0 ? box.height : 520;
    const spanX = Math.max(bounds.xMax - bounds.xMin, 1e-9);
    const spanY = Math.max(bounds.yMax - bounds.yMin, 1e-9);
    return { pixelWidth, pixelHeight, spanX, spanY,
      unitsPerPixel: Math.max(spanX / pixelWidth, spanY / pixelHeight, 1e-9) };
  }

  function renderWorldCoordinates(bounds, metrics) {
    const group = svg("g", { class: "world-coordinate-system", "aria-hidden": "true" });
    const xStep = niceWorldStep(metrics.spanX, 8);
    const yStep = niceWorldStep(metrics.spanY, 8);
    const epsilon = Math.max(xStep, yStep) * 1e-9;
    let count = 0;
    for (let x = Math.ceil(bounds.xMin / xStep) * xStep; x <= bounds.xMax + epsilon && count < 100; x += xStep, count += 1) {
      const normalized = Math.abs(x) < epsilon ? 0 : x;
      group.append(svg("line", { x1: normalized, y1: -bounds.yMax, x2: normalized, y2: -bounds.yMin, class: "world-coordinate-grid" }));
      const tick = svg("text", { x: normalized, y: -bounds.yMin - 7 * metrics.unitsPerPixel, class: "world-axis-tick", "font-size": 11 * metrics.unitsPerPixel, "text-anchor": "middle" });
      tick.textContent = formatTick(normalized);
      group.append(tick);
    }
    count = 0;
    for (let y = Math.ceil(bounds.yMin / yStep) * yStep; y <= bounds.yMax + epsilon && count < 100; y += yStep, count += 1) {
      const normalized = Math.abs(y) < epsilon ? 0 : y;
      group.append(svg("line", { x1: bounds.xMin, y1: -normalized, x2: bounds.xMax, y2: -normalized, class: "world-coordinate-grid" }));
      const tick = svg("text", { x: bounds.xMin + 6 * metrics.unitsPerPixel, y: -normalized - 3 * metrics.unitsPerPixel, class: "world-axis-tick", "font-size": 11 * metrics.unitsPerPixel, "text-anchor": "start" });
      tick.textContent = formatTick(normalized);
      group.append(tick);
    }
    if (bounds.xMin <= 0 && bounds.xMax >= 0)
      group.append(svg("line", { x1: 0, y1: -bounds.yMax, x2: 0, y2: -bounds.yMin, class: "world-axis-zero" }));
    if (bounds.yMin <= 0 && bounds.yMax >= 0)
      group.append(svg("line", { x1: bounds.xMin, y1: 0, x2: bounds.xMax, y2: 0, class: "world-axis-zero" }));
    const xLabel = svg("text", { x: bounds.xMax - 8 * metrics.unitsPerPixel, y: -bounds.yMin - 8 * metrics.unitsPerPixel, class: "world-axis-label", "font-size": 12 * metrics.unitsPerPixel, "text-anchor": "end" });
    xLabel.textContent = "X (m)";
    const yLabel = svg("text", { x: bounds.xMin + 8 * metrics.unitsPerPixel, y: -bounds.yMax + 16 * metrics.unitsPerPixel, class: "world-axis-label", "font-size": 12 * metrics.unitsPerPixel, "text-anchor": "start" });
    yLabel.textContent = "Y (m)";
    group.append(xLabel, yLabel);
    overlay.append(group);
  }

  function worldPath(points) { return safeArray(points).filter(point => finite(point.x) && finite(point.y)).map((point, index) => (index ? "L" : "M") + point.x + " " + (-point.y)).join(" "); }
  function lineKind(line) { return String(line && line.kind || "").toLowerCase(); }

  function markerTransform(position, unitsPerPixel, headingRadians = 0) {
    const headingDegrees = (finite(headingRadians) ? headingRadians : 0) * 180 / Math.PI;
    return `translate(${position.x} ${-position.y}) rotate(${-headingDegrees}) scale(${unitsPerPixel})`;
  }

  function appendWorldMarker(marker, frame, metrics) {
    if (!marker.position || !finite(marker.position.x) || !finite(marker.position.y)) return;
    const kind = String(marker.kind || "").toLowerCase();
    const pose = frame.vehiclePose;
    const heading = kind === "vehicle" && pose && finite(pose.headingRadians) ? pose.headingRadians : 0;
    const group = svg("g", {
      class: `world-marker-group marker-${kind || "generic"}`,
      transform: markerTransform(marker.position, metrics.unitsPerPixel, heading)
    });
    if (kind === "vehicle") {
      group.append(svg("path", { d: "M -9 -5 L 5 -5 L 9 0 L 5 5 L -9 5 Z", class: "world-marker-shape vehicle-outline" }));
      group.append(svg("line", { x1: -4, y1: 0, x2: 7, y2: 0, class: "vehicle-heading" }));
    } else if (kind === "smooth-start") {
      group.append(svg("circle", { cx: 0, cy: 0, r: 5, class: "world-marker-shape marker-smooth-start" }));
    } else if (kind === "plan-start") {
      group.append(svg("circle", { cx: 0, cy: 0, r: 5, class: "world-marker-shape marker-plan-start" }));
    } else if (kind === "gear-switch" || kind === "gear-switch-end") {
      group.append(svg("path", { d: "M 0 -6 L 6 0 L 0 6 L -6 0 Z", class: "world-marker-shape marker-gear-switch" }));
    } else if (kind === "final-goal") {
      group.append(svg("path", { d: "M -6 -6 L 6 -6 L 6 6 L -6 6 Z", class: "world-marker-shape marker-final-goal" }));
    } else {
      group.append(svg("path", { d: "M 0 -6 L 6 0 L 0 6 L -6 0 Z", class: "world-marker-shape world-marker" }));
    }
    overlay.append(group);
    const labelY = kind === "plan-start" ? 17 : -9;
    const labelGroup = svg("g", { transform: `translate(${marker.position.x} ${-marker.position.y}) scale(${metrics.unitsPerPixel})` });
    const label = svg("text", { x: 10, y: labelY, class: "world-marker-label" });
    label.textContent = marker.labelChinese || "";
    labelGroup.append(label);
    overlay.append(labelGroup);
  }

  function renderWorldLegend(snapshot, frame) {
    if (!worldLegend) return;
    worldLegend.replaceChildren();
    const staticLines = safeArray(snapshot.staticPolylines);
    const dynamicLines = safeArray(frame.dynamicPolylines);
    const markers = safeArray(snapshot.staticMarkers).concat(safeArray(frame.dynamicMarkers));
    const entries = [];
    const seen = new Set();
    const add = (key, label, swatchClass) => {
      if (seen.has(key)) return;
      seen.add(key);
      entries.push({ label, swatchClass });
    };
    const coarse = staticLines.find(line => lineKind(line) === "global" || lineKind(line) === "coarse");
    const smooth = staticLines.find(line => lineKind(line).includes("local-g2") || lineKind(line).includes("g2"));
    const active = dynamicLines.find(line => lineKind(line) === "active-segment");
    const previous = dynamicLines.find(line => lineKind(line) === "previous" || line.lineStyle === 1);
    const current = dynamicLines.find(line => lineKind(line) !== "active-segment" && lineKind(line) !== "previous" && line.lineStyle !== 1);
    if (coarse) add("coarse", coarse.legendChinese || "粗路径", "legend-line legend-coarse");
    if (smooth) add("smooth", smooth.legendChinese || "完整 Local G2 路径", "legend-line legend-smooth");
    if (active)
      add("segment", active.legendChinese || "当前方向段", "legend-line legend-segment");
    if (previous) add("previous", previous.legendChinese || "上一条 EM 轨迹", "legend-line legend-previous");
    if (current) add("current", current.legendChinese || "当前 EM 轨迹", "legend-line legend-current");
    const smoothStartMarker = markers.find(marker => lineKind(marker) === "smooth-start");
    const planStartMarker = markers.find(marker => lineKind(marker) === "plan-start");
    const vehicleMarker = markers.find(marker => lineKind(marker) === "vehicle");
    const gearMarker = markers.find(marker => lineKind(marker) === "gear-switch" || lineKind(marker) === "gear-switch-end");
    const goalMarker = markers.find(marker => lineKind(marker) === "final-goal");
    if (smoothStartMarker)
      add("smooth-start", smoothStartMarker.labelChinese || "平滑路径起点", "legend-marker legend-smooth-start");
    if (planStartMarker)
      add("plan-start", planStartMarker.labelChinese || "EM 规划起点", "legend-marker legend-plan-start");
    if (vehicleMarker)
      add("vehicle", vehicleMarker.labelChinese || "当前车辆", "legend-marker legend-vehicle");
    if (gearMarker)
      add("gear", gearMarker.labelChinese || "换向点 / s_end", "legend-marker legend-gear");
    if (goalMarker)
      add("goal", goalMarker.labelChinese || "平滑路径终点", "legend-marker legend-goal");
    entries.forEach(entry => {
      const item = element("div", "world-legend-item");
      item.append(element("span", "world-legend-swatch " + entry.swatchClass),
        element("span", "world-legend-label", entry.label));
      worldLegend.append(item);
    });
    worldLegend.hidden = entries.length === 0;
  }

  function renderWorld() {
    const bounds = getBounds();
    const metrics = worldMetrics(bounds);
    drawOccupancy(bounds);
    overlay.replaceChildren();
    overlay.setAttribute("viewBox", `${bounds.xMin} ${-bounds.yMax} ${bounds.xMax - bounds.xMin} ${bounds.yMax - bounds.yMin}`);
    overlay.setAttribute("preserveAspectRatio", "xMidYMid meet");
    renderWorldCoordinates(bounds, metrics);
    const snapshot = state.staticSnapshot || {};
    const frame = state.frame || {};
    const addLine = (line, css) => {
      const path = worldPath(line.points);
      if (path) overlay.append(svg("path", { d: path, class: "world-line " + css }));
    };
    const staticLines = safeArray(snapshot.staticPolylines);
    const dynamicLines = safeArray(frame.dynamicPolylines);
    const hasDynamicActiveSegment = dynamicLines.some(line => lineKind(line) === "active-segment");
    staticLines.filter(line => lineKind(line) === "global" || lineKind(line) === "coarse")
      .forEach(line => addLine(line, "world-coarse"));
    staticLines.filter(line => lineKind(line).includes("local-g2") || lineKind(line).includes("g2"))
      .forEach(line => addLine(line, "world-local-g2"));
    staticLines.filter(line => lineKind(line) !== "global" && lineKind(line) !== "coarse" && !lineKind(line).includes("local-g2") && !lineKind(line).includes("g2"))
      .forEach(line => addLine(line, "world-static"));
    safeArray(snapshot.directionSegments)
      .filter(segment => !hasDynamicActiveSegment || segment.segmentIndex !== frame.activeSegmentIndex)
      .forEach(segment => addLine(segment, "world-direction-inactive"));
    dynamicLines.filter(line => lineKind(line) === "active-segment")
      .forEach(line => addLine(line, "world-segment-active"));
    dynamicLines.filter(line => lineKind(line) === "previous" || line.lineStyle === 1)
      .forEach(line => addLine(line, "world-previous"));
    dynamicLines.filter(line => lineKind(line) !== "active-segment" && lineKind(line) !== "previous" && line.lineStyle !== 1)
      .forEach(line => addLine(line, "world-current"));
    const allLines = staticLines.concat(safeArray(snapshot.directionSegments), dynamicLines);
    const hasPath = allLines.some(line => safeArray(line && line.points)
      .some(point => point && finite(point.x) && finite(point.y)));
    safeArray(snapshot.staticMarkers).concat(safeArray(frame.dynamicMarkers))
      .forEach(marker => appendWorldMarker(marker, frame, metrics));
    renderWorldLegend(snapshot, frame);
    if (!hasPath) {
      const emptyGroup = svg("g", { transform: `translate(${(bounds.xMin + bounds.xMax) / 2} ${-((bounds.yMin + bounds.yMax) / 2)}) scale(${metrics.unitsPerPixel})` });
      const empty = svg("text", { x: 0, y: 0, class: "world-empty-state", "text-anchor": "middle" });
      empty.textContent = "未收到 Local G2 路径";
      emptyGroup.append(empty);
      overlay.append(emptyGroup);
    }
  }

  function niceDomain(values, padding = .05, unitFloor = 1e-6) {
    const flat = safeArray(values).filter(finite);
    if (!flat.length) return { min: 0, max: 1 };
    let min = Math.min(...flat), max = Math.max(...flat);
    const span = max - min;
    if (span < unitFloor) {
      const pad = Math.max(Math.abs(min) * padding, unitFloor); min -= pad; max += pad;
    } else {
      const pad = span * padding; min -= pad; max += pad;
    }
    const tickSpan = max - min;
    if (tickSpan > 0) {
      const roughStep = tickSpan / 4;
      const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep)));
      const normalized = roughStep / magnitude;
      const step = normalized >= 5 ? 5 : normalized >= 2 ? 2 : 1;
      const niceStep = step * magnitude; min = Math.floor(min / niceStep) * niceStep; max = Math.ceil(max / niceStep) * niceStep;
    }
    if (!(max > min)) max = min + 1;
    return { min, max };
  }
  function formatTick(value) {
    if (!finite(value)) return "";
    if (value === 0) return "0";
    const abs = Math.abs(value); const precision = abs >= 100 ? 1 : abs >= 1 ? 2 : 4;
    return Number(value.toFixed(precision)).toString();
  }
  function fullChartDomain(chart) {
    const all = chart ? safeArray(chart.series).flatMap(series => safeArray(series.points)) : [];
    const xDomain = niceDomain(all.map(point => point.x)), yDomain = niceDomain(all.map(point => point.y));
    return { x0: xDomain.min, x1: xDomain.max, y0: yDomain.min, y1: yDomain.max };
  }
  function currentChartDomain(id, chart) {
    const viewport = state.chartViewports[id];
    if (viewport && finite(viewport.x0) && finite(viewport.x1) && finite(viewport.y0) && finite(viewport.y1) && viewport.x1 > viewport.x0 && viewport.y1 > viewport.y0) return viewport;
    return fullChartDomain(chart);
  }
  function chartClientGeometry(host) {
    const svgNode = host.querySelector("svg");
    const rect = svgNode && typeof svgNode.getBoundingClientRect === "function" ? svgNode.getBoundingClientRect() : null;
    const usable = rect && rect.width > 0 && rect.height > 0 ? rect : null;
    const width = usable ? usable.width : chartWidth, height = usable ? usable.height : chartHeight;
    const left = usable ? usable.left : 0, top = usable ? usable.top : 0;
    const scaleX = width / chartWidth, scaleY = height / chartHeight;
    return { left, top, scaleX, scaleY, plotWidth: width - (chartLeft + chartRight) * scaleX, plotHeight: height - (chartTop + chartBottom) * scaleY };
  }
  function clientToData(host, domain, clientX, clientY) {
    const geometry = chartClientGeometry(host);
    if (!(geometry.plotWidth > 0) || !(geometry.plotHeight > 0)) return { x: (domain.x0 + domain.x1) / 2, y: (domain.y0 + domain.y1) / 2 };
    const x = domain.x0 + (clientX - geometry.left - chartLeft * geometry.scaleX) * (domain.x1 - domain.x0) / geometry.plotWidth;
    const y = domain.y1 - (clientY - geometry.top - chartTop * geometry.scaleY) * (domain.y1 - domain.y0) / geometry.plotHeight;
    return { x, y };
  }
  function dataToChartPoint(domain, point) {
    return {
      x: chartLeft + (point.x - domain.x0) * chartPlotWidth / (domain.x1 - domain.x0),
      y: chartTop + (domain.y1 - point.y) * chartPlotHeight / (domain.y1 - domain.y0)
    };
  }
  function updateZoomBox(host, id, start, current) {
    const graph = host.querySelector("svg");
    if (!graph) return;
    graph.querySelectorAll(".chart-zoom-box").forEach(node => node.remove());
    if (!start || !current) return;
    const domain = currentChartDomain(id, chartById(id));
    const a = dataToChartPoint(domain, clientToData(host, domain, start.startX, start.startY));
    const b = dataToChartPoint(domain, clientToData(host, domain, current.clientX, current.clientY));
    const x = Math.min(a.x, b.x), y = Math.min(a.y, b.y), width = Math.abs(a.x - b.x), height = Math.abs(a.y - b.y);
    graph.append(svg("rect", { x, y, width, height, class: "chart-zoom-box" }));
  }
  function installChartInteractions(host, id) {
    if (host.__chartInteractionsInstalled) return;
    host.__chartInteractionsInstalled = true;
    host.addEventListener("click", event => {
      const button = event.target.closest && event.target.closest("[data-action]");
      if (!button) return;
      const action = button.dataset.action;
      if (action === "reset") {
        delete state.chartViewports[id];
        renderChart(id, chartById(id));
      } else if (action === "fullscreen") {
        document.querySelectorAll(".chart.is-fullscreen").forEach(node => node.classList.remove("is-fullscreen"));
        if (typeof host.requestFullscreen === "function") { try { host.requestFullscreen(); } catch (error) { /* fall back to CSS fullscreen */ } }
        host.classList.add("is-fullscreen");
      }
    });
    let drag = null;
    host.addEventListener("wheel", event => {
      const chart = chartById(id);
      if (!chart || (event.target.closest && event.target.closest("button"))) return;
      event.preventDefault();
      const domain = currentChartDomain(id, chart);
      const anchor = clientToData(host, domain, event.clientX, event.clientY);
      const factor = event.deltaY > 0 ? 1.2 : 1 / 1.2;
      const next = {
        x0: anchor.x - (anchor.x - domain.x0) * factor,
        x1: anchor.x + (domain.x1 - anchor.x) * factor,
        y0: anchor.y - (anchor.y - domain.y0) * factor,
        y1: anchor.y + (domain.y1 - anchor.y) * factor
      };
      if (next.x1 > next.x0 && next.y1 > next.y0) {
        state.chartViewports[id] = next;
        renderChart(id, chartById(id));
      }
    }, { passive: false });
    host.addEventListener("pointerdown", event => {
      if (!chartById(id) || event.button !== 0 || (event.target.closest && event.target.closest("button"))) return;
      drag = { startX: event.clientX, startY: event.clientY, pointerId: event.pointerId };
      event.preventDefault();
      if (typeof host.setPointerCapture === "function") host.setPointerCapture(event.pointerId);
    });
    host.addEventListener("pointermove", event => {
      if (!drag || event.pointerId !== drag.pointerId) return;
      updateZoomBox(host, id, drag, { clientX: event.clientX, clientY: event.clientY });
    });
    host.addEventListener("pointerup", event => {
      if (!drag || event.pointerId !== drag.pointerId) return;
      const chart = chartById(id);
      const distance = Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY);
      if (chart && distance >= 4) {
        const domain = currentChartDomain(id, chart);
        const start = clientToData(host, domain, drag.startX, drag.startY);
        const end = clientToData(host, domain, event.clientX, event.clientY);
        const next = {
          x0: Math.min(start.x, end.x),
          x1: Math.max(start.x, end.x),
          y0: Math.min(start.y, end.y),
          y1: Math.max(start.y, end.y)
        };
        if (next.x1 > next.x0 && next.y1 > next.y0) state.chartViewports[id] = next;
      }
      drag = null;
      renderChart(id, chartById(id));
    });
    host.addEventListener("pointercancel", () => { drag = null; renderChart(id, chartById(id)); });
  }
  function renderChart(id, chart) {
    const host = document.getElementById(id); if (!host) return; host.replaceChildren(); const title = element("div", "chart-title", chart ? chart.chineseTitle : id); const noteText = id === "jerk-t" ? "末点后无时间区间" : chart ? chart.noteChinese : "等待当前周期数据"; const note = element("div", "chart-note", noteText); host.append(title, note);
    const tools = element("div", "chart-tools");
    const resetButton = element("button", "", "↺");
    resetButton.type = "button";
    resetButton.dataset.action = "reset";
    resetButton.title = "重置视图";
    resetButton.setAttribute("aria-label", "重置视图");
    const fullscreenButton = element("button", "", "⛶");
    fullscreenButton.type = "button";
    fullscreenButton.dataset.action = "fullscreen";
    fullscreenButton.title = "全屏";
    fullscreenButton.setAttribute("aria-label", "全屏");
    tools.append(resetButton, fullscreenButton);
    host.append(tools);
    installChartInteractions(host, id);
    const domain = currentChartDomain(id, chart);
    const width = chartWidth, height = chartHeight, left = chartLeft, right = chartRight, top = chartTop, bottom = chartBottom;
    const plotWidth = chartPlotWidth, plotHeight = chartPlotHeight;
    const sx = value => left + (value - domain.x0) * plotWidth / (domain.x1 - domain.x0); const sy = value => height - bottom - (value - domain.y0) * plotHeight / (domain.y1 - domain.y0); const graph = svg("svg", { viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": chart ? chart.chineseTitle : id });
    for (let tick = 0; tick <= 4; tick += 1) {
      const x = left + tick * plotWidth / 4, y = top + tick * plotHeight / 4;
      graph.append(svg("line", { x1: x, y1: top, x2: x, y2: height - bottom, class: "chart-grid" }), svg("line", { x1: left, y1: y, x2: width - right, y2: y, class: "chart-grid" }));
      const xt = svg("text", { x, y: height - bottom + 14, class: "axis-tick axis-tick-x", "text-anchor": "middle" }); xt.textContent = formatTick(domain.x0 + tick * (domain.x1 - domain.x0) / 4); graph.append(xt);
      const yt = svg("text", { x: left - 7, y, class: "axis-tick axis-tick-y", "text-anchor": "end", "dominant-baseline": "middle" }); yt.textContent = formatTick(domain.y1 - tick * (domain.y1 - domain.y0) / 4); graph.append(yt);
    }
    graph.append(svg("line", { x1: left, y1: top, x2: left, y2: height - bottom, class: "chart-axis" }), svg("line", { x1: left, y1: height - bottom, x2: width - right, y2: height - bottom, class: "chart-axis" }));
    safeArray(chart && chart.series).forEach(series => { const points = safeArray(series.points).filter(point => finite(point.x) && finite(point.y)); if (!points.length) return; const d = points.map((point, index) => (index ? "L" : "M") + sx(point.x) + " " + sy(point.y)).join(" "); const kind = String(series.kind || "").toLowerCase(); const lineClass = kind === "limit" || series.lineStyle === 2 ? "chart-limit" : kind === "previous" || series.lineStyle === 1 ? "chart-dashed" : "chart-solid"; graph.append(svg("path", { d, class: "chart-data " + lineClass })); });
    safeArray(chart && chart.annotations).forEach(annotation => { if (!finite(annotation.x)) return; const ax = sx(annotation.x); graph.append(svg("line", { x1: ax, y1: top, x2: ax, y2: height - bottom, class: "chart-annotation-line" })); if (finite(annotation.y)) graph.append(svg("circle", { cx: ax, cy: sy(annotation.y), r: 3, class: "chart-annotation-point" })); const label = svg("text", { x: Math.min(ax + 4, width - right - 2), y: top + 10, class: "chart-annotation-label" }); label.textContent = annotation.labelChinese || ""; graph.append(label); });
    const xLabel = svg("text", { x: (left + width - right) / 2, y: height - 4, class: "axis-label axis-label-x", "text-anchor": "middle" }); xLabel.textContent = chart ? chart.xAxisLabel : "x"; const yLabel = svg("text", { x: 13, y: (top + height - bottom) / 2, class: "axis-label axis-label-y", transform: `rotate(-90 13 ${(top + height - bottom) / 2})`, "text-anchor": "middle" }); yLabel.textContent = chart ? chart.yAxisLabel : "y"; graph.append(xLabel, yLabel); host.append(graph);
  }

  function renderOverview() { renderWorld(); const summary = document.getElementById("overview-summary"); summary.replaceChildren(); const frame = state.frame || {}; safeArray(frame.statusValues).concat([{ chineseName: "活动方向段", rawName: "SegmentIndex", value: String(frame.activeSegmentIndex ?? "不可用"), unit: "", severity: "normal" }, { chineseName: "方向", rawName: "Direction", value: frame.activeDirection || "不可用", unit: "", severity: "normal" }]).forEach(value => { const cell = element("div", "summary-value " + (value.severity || "")); const name = element("small", "", `${value.chineseName || "不可用"} · ${value.rawName || ""}`); const data = element("div", "", `${value.value || "不可用"} ${value.unit || ""}`.trim()); cell.append(name, data); summary.append(cell); }); }
  function chartById(id) { return safeArray(state.frame && state.frame.charts).find(chart => chart.id === id); }
  function renderCharts(ids) { ids.forEach(id => renderChart(id, chartById(id))); }
  function renderHistoryConfig() { const history = document.getElementById("cycle-history"); history.replaceChildren(); const table = element("table"); const head = element("tr"); ["周期", "时间", "状态", "发布", "耗时 (ms)", "段", "方向", "模式", "终端", "失败原因"].forEach(label => head.append(element("th", "", label))); table.append(head); safeArray(state.history).forEach(item => { const row = element("tr"); [item.cycleVersion, item.occurredAtUtc, item.status, item.published, item.planningElapsedMilliseconds, item.segmentIndex, item.direction, item.longitudinalMode, item.terminalType, item.failureReason || ""].forEach(value => row.append(element("td", "", String(value ?? "不可用")))); table.append(row); }); history.append(table); const configuration = document.getElementById("configuration"); configuration.replaceChildren(); safeArray(state.staticSnapshot && state.staticSnapshot.configurationGroups).forEach(group => { const panel = element("div", "config-group"); panel.append(element("h3", "", group.chineseTitle)); safeArray(group.entries).forEach(entry => { const line = element("div", "config-entry " + (entry.severity || "")); line.append(element("span", "", entry.chineseName), element("span", "raw", entry.rawName), element("span", "", entry.value), element("span", "", entry.unit)); panel.append(line); }); configuration.append(panel); }); }
  function renderActiveTab() { if (!state.redrawEnabled[state.activeTab]) return; try { if (state.activeTab === "overview") renderOverview(); else if (state.activeTab === "ls-st") renderCharts(["ls", "st"]); else if (state.activeTab === "kinematics") renderCharts(["curvature-s", "curvature-t", "velocity-t", "acceleration-t", "jerk-t", "yaw-rate-t"]); else renderHistoryConfig(); } catch (error) { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); } }
  function renderAll() { renderActiveTab(); }
  function updateConnectionState() { if (state.ended) return; if (!state.lastFrameAt || Date.now() - state.lastFrameAt > 2000 / state.refreshRateHz * 1000) setLiveState("数据已过期", "failure"); }
  function receiveFrame(payload) { state.frame = payload.snapshot || null; state.history = safeArray(payload.history); state.lastFrameAt = Date.now(); if (!state.ended) setLiveState(state.frame && state.frame.sessionStateChinese || "运行中"); renderAll(); }
  function startEvents() { const events = new EventSource(authorized("/api/events")); events.addEventListener("frame", event => { try { receiveFrame(JSON.parse(event.data)); } catch (error) { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); } }); events.addEventListener("end", () => { state.ended = true; events.close(); setLiveState("会话已结束"); }); events.onerror = () => { if (!state.ended) setLiveState("连接中断，正在重连", "notice"); }; }
  async function boot() { installTabs(); ["overview", "ls-st", "kinematics", "history-config"].forEach(tab => { state.redrawEnabled[tab] = true; }); try { const response = await fetch(authorized("/api/bootstrap"), { cache: "no-store" }); if (!response.ok) throw new Error("bootstrap failed"); const bootstrap = await response.json(); state.staticSnapshot = bootstrap.staticSnapshot || bootstrap; state.refreshRateHz = finite(bootstrap.refreshRateHz) && bootstrap.refreshRateHz > 0 ? bootstrap.refreshRateHz : 10; renderAll(); startEvents(); window.setInterval(updateConnectionState, 250); } catch (error) { setLiveState("页面绘图异常", "failure"); } }
  let overviewResizeFrame = 0;
  window.addEventListener("resize", () => {
    if (overviewResizeFrame || state.activeTab !== "overview" || !state.redrawEnabled.overview) return;
    overviewResizeFrame = window.requestAnimationFrame(() => {
      overviewResizeFrame = 0;
      renderWorld();
    });
  });
  window.addEventListener("error", () => { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); }); window.addEventListener("unhandledrejection", () => { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); });
  if (window.__TRAJECTORY_VISUALIZATION_TEST__ === true) {
    ["overview", "ls-st", "kinematics", "history-config"].forEach(tab => { state.redrawEnabled[tab] = true; });
    window.__trajectoryVisualizationTestHooks = Object.freeze({
      setBootstrap(snapshot) { state.staticSnapshot = snapshot || null; },
      receiveFrame,
      renderActiveTab,
      installTabs
    });
    installTabs();
  } else {
    boot();
  }
})();
