fix: repair EM observation web charts
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
|
||||
function getBounds() {
|
||||
const values = [];
|
||||
const include = points => safeArray(points).forEach(point => { if (finite(point.x) && finite(point.y)) values.push(point); });
|
||||
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]));
|
||||
@@ -52,21 +52,89 @@
|
||||
const bounds = getBounds(); drawOccupancy(bounds); overlay.replaceChildren(); overlay.setAttribute("viewBox", `${bounds.xMin} ${-bounds.yMax} ${bounds.xMax - bounds.xMin} ${bounds.yMax - bounds.yMin}`); overlay.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||
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 })); };
|
||||
safeArray(snapshot.staticPolylines).forEach(line => addLine(line, "world-static"));
|
||||
safeArray(snapshot.directionSegments).forEach(segment => addLine(segment, segment.segmentIndex === frame.activeSegmentIndex ? "world-active" : segment.segmentIndex < frame.activeSegmentIndex ? "world-static" : "world-future"));
|
||||
safeArray(frame.dynamicPolylines).forEach(line => addLine(line, line.lineStyle === 1 ? "world-previous" : "world-active"));
|
||||
safeArray(snapshot.staticMarkers).concat(safeArray(frame.dynamicMarkers)).forEach(marker => { if (!marker.position || !finite(marker.position.x) || !finite(marker.position.y)) return; const diamond = svg("path", { d: `M ${marker.position.x} ${-marker.position.y - .18} L ${marker.position.x + .18} ${-marker.position.y} L ${marker.position.x} ${-marker.position.y + .18} L ${marker.position.x - .18} ${-marker.position.y} Z`, class: "world-marker" }); overlay.append(diamond); const label = svg("text", { x: marker.position.x + .2, y: -marker.position.y - .2, fill: "#20252b", "font-size": ".32" }); label.textContent = marker.labelChinese || ""; overlay.append(label); });
|
||||
if (frame.vehiclePose && finite(frame.vehiclePose.x) && finite(frame.vehiclePose.y)) overlay.append(svg("circle", { cx: frame.vehiclePose.x, cy: -frame.vehiclePose.y, r: ".18", fill: "#20252b" }));
|
||||
const staticClass = line => { const kind = String(line.kind || "").toLowerCase(); if (kind === "global" || kind === "coarse") return "world-coarse"; if (kind.includes("local-g2") || kind.includes("g2")) return "world-local-g2"; return "world-static"; };
|
||||
const dynamicClass = line => { const kind = String(line.kind || "").toLowerCase(); if (kind === "current") return "world-current"; if (kind === "previous" || line.lineStyle === 1) return "world-previous"; return "world-current"; };
|
||||
safeArray(snapshot.staticPolylines).forEach(line => addLine(line, staticClass(line)));
|
||||
safeArray(snapshot.directionSegments).forEach(segment => addLine(segment, segment.segmentIndex === frame.activeSegmentIndex ? "world-segment-active" : "world-direction-inactive"));
|
||||
safeArray(frame.dynamicPolylines).forEach(line => addLine(line, dynamicClass(line)));
|
||||
const hasPath = safeArray(snapshot.staticPolylines).concat(safeArray(snapshot.directionSegments), safeArray(frame.dynamicPolylines))
|
||||
.some(line => safeArray(line && line.points).some(point => point && finite(point.x) && finite(point.y)));
|
||||
const appendMarker = marker => {
|
||||
if (!marker.position || !finite(marker.position.x) || !finite(marker.position.y)) return;
|
||||
const x = marker.position.x, y = -marker.position.y; const kind = String(marker.kind || "").toLowerCase();
|
||||
if (kind === "vehicle") {
|
||||
const pose = frame.vehiclePose; const heading = pose && finite(pose.headingRadians) ? pose.headingRadians : 0;
|
||||
const cos = Math.cos(heading), sin = Math.sin(heading);
|
||||
const corners = [[.38, .18], [-.38, .18], [-.38, -.18], [.38, -.18]].map(([dx, dy]) => {
|
||||
const wx = marker.position.x + dx * cos - dy * sin; const wy = marker.position.y + dx * sin + dy * cos; return `${wx} ${-wy}`;
|
||||
});
|
||||
const group = svg("g", { class: "marker-vehicle" });
|
||||
group.append(svg("path", { d: `M ${corners[0]} L ${corners[1]} L ${corners[2]} L ${corners[3]} Z`, class: "vehicle-outline" }));
|
||||
group.append(svg("line", { x1: x, y1: y, x2: x + .22 * cos, y2: y - .22 * sin, class: "vehicle-heading" }));
|
||||
overlay.append(group);
|
||||
} else if (kind === "plan-start") {
|
||||
overlay.append(svg("circle", { cx: x, cy: y, r: ".12", class: "marker-plan-start" }));
|
||||
} else if (kind === "gear-switch" || kind === "gear-switch-end") {
|
||||
const size = ".14"; overlay.append(svg("path", { d: `M ${x} ${y - size} L ${x + size} ${y} L ${x} ${y + size} L ${x - size} ${y} Z`, class: "marker-gear-switch" }));
|
||||
} else if (kind === "final-goal") {
|
||||
const size = ".13"; overlay.append(svg("path", { d: `M ${x - size} ${y - size} L ${x + size} ${y - size} L ${x + size} ${y + size} L ${x - size} ${y + size} Z`, class: "marker-final-goal" }));
|
||||
} else {
|
||||
const size = ".18"; overlay.append(svg("path", { d: `M ${x} ${y - size} L ${x + size} ${y} L ${x} ${y + size} L ${x - size} ${y} Z`, class: "world-marker" }));
|
||||
}
|
||||
const label = svg("text", { x: x + .2, y: y - .2, fill: "#20252b", "font-size": ".32" }); label.textContent = marker.labelChinese || ""; overlay.append(label);
|
||||
};
|
||||
safeArray(snapshot.staticMarkers).concat(safeArray(frame.dynamicMarkers)).forEach(appendMarker);
|
||||
if (!hasPath) {
|
||||
const empty = svg("text", { x: (bounds.xMin + bounds.xMax) / 2, y: -((bounds.yMin + bounds.yMax) / 2), class: "world-empty-state", "text-anchor": "middle" });
|
||||
empty.textContent = "未收到 Local G2 路径"; overlay.append(empty);
|
||||
}
|
||||
}
|
||||
|
||||
function range(points) { const flat = safeArray(points).filter(point => finite(point.x) && finite(point.y)); if (!flat.length) return { x0: 0, x1: 1, y0: 0, y1: 1 }; let x0 = Math.min(...flat.map(point => point.x)), x1 = Math.max(...flat.map(point => point.x)), y0 = Math.min(...flat.map(point => point.y)), y1 = Math.max(...flat.map(point => point.y)); const xPad = Math.max(x1 - x0, 1) * .05, yPad = Math.max(y1 - y0, 1) * .08; return { x0: x0 - xPad, x1: x1 + xPad, y0: y0 - yPad, y1: y1 + yPad }; }
|
||||
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 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 all = chart ? safeArray(chart.series).flatMap(series => safeArray(series.points)) : []; const domain = range(all); const width = 640, height = 244, left = 52, right = 16, top = 8, bottom = 36; const sx = value => left + (value - domain.x0) * (width - left - right) / (domain.x1 - domain.x0); const sy = value => height - bottom - (value - domain.y0) * (height - top - bottom) / (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 * (width - left - right) / 4, y = top + tick * (height - top - bottom) / 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", "text-anchor": "middle" }); xt.textContent = (domain.x0 + tick * (domain.x1 - domain.x0) / 4).toPrecision(3); graph.append(xt); }
|
||||
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));
|
||||
const domain = { x0: xDomain.min, x1: xDomain.max, y0: yDomain.min, y1: yDomain.max };
|
||||
const width = 640, height = 244, left = 58, right = 16, top = 8, bottom = 36;
|
||||
const plotWidth = width - left - right, plotHeight = height - top - bottom;
|
||||
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 lineClass = series.lineStyle === 1 ? "chart-dashed" : series.lineStyle === 2 ? "chart-limit" : "chart-solid"; graph.append(svg("path", { d, class: "chart-data " + lineClass })); });
|
||||
const xLabel = svg("text", { x: (left + width - right) / 2, y: height - 4, class: "axis-label", "text-anchor": "middle" }); xLabel.textContent = chart ? chart.xAxisLabel : "x"; const yLabel = svg("text", { x: 13, y: (top + height - bottom) / 2, class: "axis-label", transform: `rotate(-90 13 ${(top + height - bottom) / 2})`, "text-anchor": "middle" }); yLabel.textContent = chart ? chart.yAxisLabel : "y"; graph.append(xLabel, yLabel); host.append(graph);
|
||||
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); }); }
|
||||
@@ -79,5 +147,17 @@
|
||||
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"); } }
|
||||
window.addEventListener("error", () => { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); }); window.addEventListener("unhandledrejection", () => { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); }); boot();
|
||||
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();
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user