2026-08-06 09:52:04 +08:00
(() => {
"use strict" ;
const token = new URLSearchParams ( window . location . search ). get ( "token" ) || "" ;
const svgNs = "http://www.w3.org/2000/svg" ;
const state = { staticSnapshot : null , frame : null , history : [], refreshRateHz : 10 , ended : false , activeTab : "overview" , redrawEnabled : {} };
const liveState = document . getElementById ( "live-state" );
const occupancyCanvas = document . getElementById ( "occupancy-grid" );
const overlay = document . getElementById ( "world-overlay" );
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 = [];
2026-08-07 09:46:51 +08:00
const include = points => safeArray ( points ). forEach ( point => { if ( point && finite ( point . x ) && finite ( point . y )) values . push ( point ); });
2026-08-06 09:52:04 +08:00
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 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 renderWorld () {
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 })); };
2026-08-07 09:46:51 +08:00
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 );
}
2026-08-06 09:52:04 +08:00
}
2026-08-07 09:46:51 +08:00
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 ();
}
2026-08-06 09:52:04 +08:00
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 );
2026-08-07 09:46:51 +08:00
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 );
}
2026-08-06 09:52:04 +08:00
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" }));
2026-08-07 09:46:51 +08:00
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 );
2026-08-06 09:52:04 +08:00
}
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" ); } }
2026-08-07 09:46:51 +08:00
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 ();
}
2026-08-06 09:52:04 +08:00
})();