feat(javaTest): inspect bounded route decisions on full map

This commit is contained in:
zgw
2026-08-06 18:49:15 +08:00
parent 58de793143
commit bd7decf4fd
12 changed files with 728 additions and 22 deletions

View File

@@ -0,0 +1,147 @@
"""Run repeatable Chromium checks against the real javaTest visualizer server."""
import os
import re
import sys
from glob import glob
from playwright.sync_api import expect, sync_playwright
BASE_URL = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:18084"
OUTPUT_DIR = sys.argv[2] if len(sys.argv) > 2 else "outputs/browser-qa"
def require(condition, message):
"""Fail the QA run with a business-readable assertion message."""
if not condition:
raise AssertionError(message)
def horizontal_overflow(page):
"""Return true when the document spills outside the active viewport."""
return page.evaluate(
"document.documentElement.scrollWidth > document.documentElement.clientWidth + 1"
)
def run_point_to_point(page):
"""Execute the real point simulation and verify route evidence is rendered."""
page.locator("#run-point").click()
page.locator("#task-status").wait_for(state="visible", timeout=120_000)
page.locator("#loading-state").wait_for(state="hidden", timeout=120_000)
expect(page.locator("#task-status")).to_have_text("COMPLETED", timeout=120_000)
require(page.locator("#route-candidate-body tr").count() > 0,
"point simulation did not render route candidates")
require(page.locator("#route-quality-body tr").count() == 6,
"route quality tuple must contain six ordered metrics")
require(page.locator("#route-comparability").inner_text() != "--",
"historical comparability badge was not populated")
def verify_map_interactions(page):
"""Check full-map authority, route layers, viewport controls and timeline stability."""
map_edges = page.locator(".map-floor-layer:not([hidden]) .map-edge").count()
dynamic_edges = page.locator("#dynamic-route-layer .route-segment").count()
require(map_edges > 20, "complete floor map is missing")
require(map_edges > dynamic_edges, "map background should exceed one task route")
full_view = page.locator("#route-map").get_attribute("viewBox")
page.locator("#path-fit").click()
path_view = page.locator("#route-map").get_attribute("viewBox")
require(path_view != full_view, "path fit did not change the map viewport")
page.locator("#zoom-in").click()
zoomed_view = page.locator("#route-map").get_attribute("viewBox")
require(zoomed_view != path_view, "zoom control did not change the viewport")
page.locator("#reset-view").click()
require(page.locator("#route-map").get_attribute("viewBox") == "0 0 1000 560",
"reset did not restore the full-map viewport")
page.locator("#show-current-route").uncheck()
require(page.locator("#dynamic-route-layer .planned-only").count() == 0,
"current route remained visible after its layer was disabled")
require(page.locator(".map-floor-layer:not([hidden]) .map-edge").count() == map_edges,
"disabling current route changed the authoritative map")
page.locator("#show-current-route").check()
page.locator("#show-historical").uncheck()
require(page.locator("#dynamic-route-layer .historical-only").count() == 0,
"historical route remained visible after its layer was disabled")
page.locator("#show-historical").check()
page.locator("#show-initial-route").uncheck()
require(page.locator("#dynamic-route-layer .initial-service").count() == 0,
"initial route remained visible after its layer was disabled")
page.locator("#show-initial-route").check()
before_tick_view = page.locator("#route-map").get_attribute("viewBox")
slider = page.locator("#timeline-slider")
slider.fill(slider.get_attribute("max"))
slider.dispatch_event("input")
require(page.locator("#route-map").get_attribute("viewBox") == before_tick_view,
"timeline movement unexpectedly changed the user viewport")
page.locator("#show-move-trace").uncheck()
require(page.locator("#dynamic-route-layer .traversed").count() == 0,
"MOVE trace remained visible after its layer was disabled")
page.locator("#show-move-trace").check()
def verify_historical_mode(page):
"""Run the real 240-task endpoint and verify task selection keeps decision evidence live."""
page.locator(".mode-tab[data-mode='history']").click()
page.locator("#run-history").click()
expect(page.locator("#history-run-state")).to_have_text(
re.compile(r"^最近运行"), timeout=180_000
)
require("240" in page.locator("#task-filter-count").inner_text(),
"historical result did not expose all 240 tasks")
require(page.locator("#history-task-body tr").count() == 240,
"historical task table row count is not 240")
page.locator("#history-task-body tr").nth(1).click()
require(page.locator("#route-candidate-body tr").count() > 0,
"selected historical task lost route candidates")
def verify_viewport(browser, viewport, filename, run_history):
"""Verify one desktop or mobile viewport and save its full-page evidence image."""
context = browser.new_context(viewport=viewport, locale="zh-CN")
page = context.new_page()
errors = []
page.on("console", lambda message: errors.append(message.text)
if message.type == "error" else None)
page.on("pageerror", lambda error: errors.append(str(error)))
page.goto(BASE_URL, wait_until="networkidle", timeout=120_000)
page.locator(".map-floor-layer:not([hidden]) .map-edge").first.wait_for(
state="attached", timeout=60_000
)
require(not horizontal_overflow(page), "page has horizontal overflow before simulation")
run_point_to_point(page)
verify_map_interactions(page)
if run_history:
verify_historical_mode(page)
require(not horizontal_overflow(page), "page has horizontal overflow after simulation")
page.screenshot(path=os.path.join(OUTPUT_DIR, filename), full_page=True)
require(not errors, "browser console/page errors: " + " | ".join(errors))
context.close()
def main():
"""Run desktop and mobile checks using one headless Chromium process."""
os.makedirs(OUTPUT_DIR, exist_ok=True)
with sync_playwright() as playwright:
cached_candidates = sorted(glob(os.path.join(
os.environ.get("LOCALAPPDATA", ""), "ms-playwright", "chromium-*",
"chrome-win64", "chrome.exe"
)), reverse=True)
launch_options = {"headless": True}
if cached_candidates:
launch_options["executable_path"] = cached_candidates[0]
browser = playwright.chromium.launch(**launch_options)
verify_viewport(browser, {"width": 1440, "height": 1000}, "desktop.png", True)
verify_viewport(browser, {"width": 390, "height": 844}, "mobile.png", False)
browser.close()
print("visualizer browser QA passed: desktop=1440x1000 mobile=390x844")
if __name__ == "__main__":
main()

View File

@@ -1064,9 +1064,12 @@ public final class HistoricalDispatchSimulator {
current.execution.pathVersion = nextVersion;
current.execution.rerouteCount++;
current.execution.planningEvidence.addAll(newEvidence);
events.add(event(tick, now, "REROUTE", taskId, vehicle.getVehicleId(),
"STRICTLY_BETTER_TRAFFIC_QUALITY", oldRemaining.get(0),
newRemaining.get(newRemaining.size() - 1)));
// 只有预约和活动路线都提交成功后才发布前后证据,页面看到的版本一定真实生效。
events.add(new SimulationTickEvent(tick, now, "REROUTE", taskId,
vehicle.getVehicleId(), "STRICTLY_BETTER_TRAFFIC_QUALITY",
oldRemaining.get(0), newRemaining.get(newRemaining.size() - 1),
new RerouteDecisionEvidence(nextVersion, oldRemaining, newRemaining,
oldQuality, newQuality)));
return true;
}

View File

@@ -0,0 +1,63 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.demo.traffic.RouteQuality;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** 成功替换交通预约时保存的重规划前后剩余路线和质量证据。 */
public final class RerouteDecisionEvidence {
private final int pathVersion;
private final List<String> oldRemainingPath;
private final List<String> newRemainingPath;
private final RouteQuality oldQuality;
private final RouteQuality newQuality;
/**
* 固化一次已经提交的路线替换,供报告和地图检查器还原当时的业务判断。
*
* <p>只有预约原子替换成功后才创建该对象,因此它代表实际生效版本,而不是一次尝试候选。
* @param pathVersion 替换成功后的路线版本。
* @param oldRemainingPath 替换前从车辆当前位置开始的剩余路线。
* @param newRemainingPath 替换后从车辆当前位置开始的剩余路线。
* @param oldQuality 旧剩余路线在触发时交通快照中的质量。
* @param newQuality 新剩余路线在同一交通快照中的质量。
*/
public RerouteDecisionEvidence(int pathVersion,
List<String> oldRemainingPath,
List<String> newRemainingPath,
RouteQuality oldQuality,
RouteQuality newQuality) {
if (pathVersion <= 1 || oldRemainingPath == null || oldRemainingPath.isEmpty()
|| newRemainingPath == null || newRemainingPath.isEmpty()
|| oldQuality == null || newQuality == null) {
throw new IllegalArgumentException("complete reroute evidence is required");
}
this.pathVersion = pathVersion;
this.oldRemainingPath = immutable(oldRemainingPath);
this.newRemainingPath = immutable(newRemainingPath);
this.oldQuality = oldQuality;
this.newQuality = newQuality;
}
/** 返回原子替换成功后采用的路线版本号。 */
public int getPathVersion() { return pathVersion; }
/** 返回替换前仍待车辆执行的真实路线段。 */
public List<String> getOldRemainingPath() { return oldRemainingPath; }
/** 返回替换后已经取得交通预约的真实路线段。 */
public List<String> getNewRemainingPath() { return newRemainingPath; }
/** 返回触发重规划时旧剩余路线的质量元组。 */
public RouteQuality getOldQuality() { return oldQuality; }
/** 返回同一快照下新剩余路线的质量元组。 */
public RouteQuality getNewQuality() { return newQuality; }
/** 对外暴露防御性、不可变路径,防止报告层改写仿真事实。 */
private List<String> immutable(List<String> values) {
return Collections.unmodifiableList(new ArrayList<String>(values));
}
}

View File

@@ -9,6 +9,7 @@ public final class SimulationTickEvent {
private final String reason;
private final String fromSite;
private final String toSite;
private final RerouteDecisionEvidence rerouteDecision;
/**
* 创建 {@code SimulationTickEvent} 实例并初始化其内部状态。
@@ -26,6 +27,26 @@ public final class SimulationTickEvent {
public SimulationTickEvent(long tick, long simulationMillis, String type,
String taskId, String vehicleId, String reason,
String fromSite, String toSite) {
this(tick, simulationMillis, type, taskId, vehicleId, reason,
fromSite, toSite, null);
}
/**
* 创建带结构化重规划决定的审计事件;该扩展仅用于已经成功提交的 {@code REROUTE}。
* @param tick 事件所在仿真 tick。
* @param simulationMillis 相对仿真时间。
* @param type 事件类型。
* @param taskId 任务标识。
* @param vehicleId 执行车辆。
* @param reason 业务原因。
* @param fromSite 事件起点。
* @param toSite 事件终点。
* @param rerouteDecision 重规划前后路径与质量;普通事件为 {@code null}。
*/
public SimulationTickEvent(long tick, long simulationMillis, String type,
String taskId, String vehicleId, String reason,
String fromSite, String toSite,
RerouteDecisionEvidence rerouteDecision) {
this.tick = tick;
this.simulationMillis = simulationMillis;
this.type = type;
@@ -34,6 +55,7 @@ public final class SimulationTickEvent {
this.reason = reason;
this.fromSite = fromSite;
this.toSite = toSite;
this.rerouteDecision = rerouteDecision;
}
/**
@@ -92,4 +114,7 @@ public final class SimulationTickEvent {
* @return 本次处理产生的结果;调用方据此继续后续流程。
*/
public String getToSite() { return toSite; }
/** 返回成功重规划事件的结构化决定;非 REROUTE 事件返回 {@code null}。 */
public RerouteDecisionEvidence getRerouteDecision() { return rerouteDecision; }
}

View File

@@ -9,6 +9,7 @@ import com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulationResul
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInput;
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationTask;
import com.ft546.wcs.shadow.history.simulation.RoutePlanningEvidence;
import com.ft546.wcs.shadow.history.simulation.RerouteDecisionEvidence;
import com.ft546.wcs.shadow.history.simulation.SimulationInvariantSummary;
import com.ft546.wcs.shadow.history.simulation.SimulationMetrics;
import com.ft546.wcs.shadow.history.simulation.SimulationTaskResult;
@@ -108,7 +109,7 @@ public final class SimulationVisualizationMapper {
List<Map<String, Object>> tasks = new ArrayList<Map<String, Object>>();
for (SimulationTaskResult task : orderedTasks) {
tasks.add(task(task, definitions.get(task.getSimulationTaskId()),
result.getAcceleration(), input.getGridMap()));
result.getAcceleration(), input.getGridMap(), result.getEvents()));
}
List<Map<String, Object>> events = new ArrayList<Map<String, Object>>();
for (SimulationTickEvent event : result.getEvents()) events.add(event(event));
@@ -182,7 +183,8 @@ public final class SimulationVisualizationMapper {
private Map<String, Object> task(SimulationTaskResult task,
HistoricalSimulationTask definition,
int acceleration,
GridMap gridMap) {
GridMap gridMap,
List<SimulationTickEvent> allEvents) {
HistoricalQueueRecord record = task.getRecord();
List<String> historicalPath = historicalPath(record.getPathInfo());
Long historicalDuration = historicalDuration(task);
@@ -242,9 +244,27 @@ public final class SimulationVisualizationMapper {
value.put("servicePlanningEvidence", planningEvidence(task,
RoutePlanningEvidence.PlanningStage.SERVICE));
value.put("rerouteCount", task.getRerouteCount());
Set<Integer> pathVersions = new TreeSet<Integer>();
for (RoutePlanningEvidence evidence : task.getPlanningEvidence()) {
pathVersions.add(evidence.getPathVersion());
}
value.put("pathVersions", new ArrayList<Integer>(pathVersions));
value.put("rerouteEvents", rerouteEvents(task.getSimulationTaskId(), allEvents));
return value;
}
/** 返回当前任务的重规划触发、拒绝和成功事件,供前端按版本还原决策链。 */
private List<Map<String, Object>> rerouteEvents(
String taskId, List<SimulationTickEvent> allEvents) {
List<Map<String, Object>> values = new ArrayList<Map<String, Object>>();
for (SimulationTickEvent event : allEvents) {
if (!taskId.equals(event.getTaskId())
|| !event.getType().startsWith("REROUTE")) continue;
values.add(event(event));
}
return values;
}
/** 按阶段转换每次实际采用的候选、质量、距离边界和版本证据。 */
private List<Map<String, Object>> planningEvidence(
SimulationTaskResult task, RoutePlanningEvidence.PlanningStage stage) {
@@ -347,6 +367,19 @@ public final class SimulationVisualizationMapper {
value.put("reason", event.getReason());
value.put("fromSite", event.getFromSite());
value.put("toSite", event.getToSite());
value.put("rerouteDecision", rerouteDecision(event.getRerouteDecision()));
return value;
}
/** 把成功路线替换转换为明确的旧/新剩余路径和同快照质量。 */
private Map<String, Object> rerouteDecision(RerouteDecisionEvidence source) {
if (source == null) return null;
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("pathVersion", source.getPathVersion());
value.put("oldRemainingPath", source.getOldRemainingPath());
value.put("newRemainingPath", source.getNewRemainingPath());
value.put("oldQuality", quality(source.getOldQuality()));
value.put("newQuality", quality(source.getNewQuality()));
return value;
}

View File

@@ -59,7 +59,8 @@ button:focus-visible, input:focus-visible, select:focus-visible {
}
.brand-block, .header-status, .button-row, .playback-buttons, .route-tools, .map-actions,
.history-actions, .timeline-controls, .table-toolbar, .inline-field, .toggle-field {
.history-actions, .timeline-controls, .table-toolbar, .inline-field, .toggle-field,
.layer-toggles, .comparison-status, .decision-selectors {
display: flex;
align-items: center;
}
@@ -224,7 +225,7 @@ h3 { font-size: 14px; line-height: 1.3; }
.workspace {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
grid-template-columns: minmax(0, 1fr) minmax(360px, 410px);
min-height: 520px;
background: var(--color-surface);
border-bottom: 1px solid var(--color-line);
@@ -252,6 +253,7 @@ h3 { font-size: 14px; line-height: 1.3; }
.inline-field select { width: auto; min-width: 74px; height: 32px; }
.toggle-field { gap: 6px; }
.toggle-field input { width: 16px; height: 16px; accent-color: var(--color-green); }
.layer-toggles { gap: 8px; padding-left: 8px; border-left: 1px solid var(--color-line); }
.route-legend {
min-height: 34px;
display: flex;
@@ -271,7 +273,10 @@ h3 { font-size: 14px; line-height: 1.3; }
.legend-line.shared { height: 6px; border-top: 6px solid var(--color-service-path); border-bottom: 2px dotted var(--color-historical-path); }
.legend-line.planned-only { border-color: var(--color-service-path); border-top-width: 5px; }
.legend-line.historical-only { border-color: var(--color-historical-path); border-top-style: dotted; }
.legend-line.initial-service { border-color: #50736a; border-top-style: dashed; }
.legend-line.traversed { border-color: var(--color-traversed-path); border-top-width: 5px; }
.legend-line.reroute-old { border-color: var(--color-red); border-top-style: dashed; }
.legend-line.reroute-new { border-color: var(--color-blue); border-top-width: 5px; }
.legend-dot { width: 10px; height: 10px; border: 2px solid; border-radius: 50%; }
.legend-dot.hold { border-color: var(--color-amber); }
@@ -287,9 +292,15 @@ h3 { font-size: 14px; line-height: 1.3; }
.route-segment.historical { stroke: var(--color-historical-path); stroke-width: 3; stroke-dasharray: 3 7; opacity: .82; }
.route-segment.planned-only { stroke: var(--color-service-path); stroke-width: 6; }
.route-segment.historical-only { stroke: var(--color-historical-path); stroke-width: 4; stroke-dasharray: 3 7; }
.route-segment.initial-service { stroke: #50736a; stroke-width: 3; stroke-dasharray: 11 7; opacity: .9; }
.route-segment.shared-base { stroke: var(--color-service-path); stroke-width: 7; }
.route-segment.shared-overlay { stroke: var(--color-historical-path); stroke-width: 3; stroke-dasharray: 2 8; }
.route-segment.traversed { stroke: var(--color-traversed-path); stroke-width: 8; }
.route-segment.reroute-old { stroke: var(--color-red); stroke-width: 5; stroke-dasharray: 12 8; opacity: .82; }
.route-segment.reroute-new { stroke: var(--color-blue); stroke-width: 5; }
.traffic-event-marker { fill: #fff; stroke-width: 4; vector-effect: non-scaling-stroke; }
.traffic-event-marker.hold-point { stroke: var(--color-amber); }
.traffic-event-marker.reroute-point { stroke: var(--color-red); }
.map-site { fill: #d8dfdc; stroke: #b6c1bd; stroke-width: 1; vector-effect: non-scaling-stroke; }
.map-node { fill: #fff; stroke: #7d8a86; stroke-width: 2; vector-effect: non-scaling-stroke; }
.map-node.passed { fill: var(--color-blue-soft); stroke: var(--color-blue); }
@@ -326,6 +337,38 @@ h3 { font-size: 14px; line-height: 1.3; }
.evidence-list > div { display: grid; grid-template-columns: 96px 1fr; gap: 10px; padding: 10px 16px; border-bottom: 1px solid #e4eae8; }
.evidence-list dt { color: var(--color-muted); font-size: 11px; }
.evidence-list dd { margin: 0; text-align: right; font-family: var(--font-mono); font-size: 12px; overflow-wrap: anywhere; }
.decision-audit, .reroute-audit { border-bottom: 1px solid var(--color-line); }
.inspector-section-bar { min-height: 54px; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 9px 14px; background: var(--color-surface-muted); }
.comparison-status { max-width: 240px; gap: 5px; justify-content: flex-end; flex-wrap: wrap; }
.evidence-badge { min-height: 23px; display: inline-flex; align-items: center; padding: 2px 7px; border: 1px solid var(--color-line); border-radius: 999px; font-size: 10px; font-weight: 700; white-space: nowrap; }
.evidence-badge.is-neutral { color: var(--color-muted); background: #eef2f0; }
.evidence-badge.is-pass { color: #0e674c; background: var(--color-green-soft); border-color: #b8dfd0; }
.evidence-badge.is-warning { color: #8b5108; background: var(--color-amber-soft); border-color: #efd09d; }
.evidence-badge.is-fail { color: #9e3030; background: var(--color-red-soft); border-color: #edb9b5; }
.decision-selectors { gap: 10px; padding: 8px 14px; border-top: 1px solid var(--color-line); border-bottom: 1px solid var(--color-line); }
.decision-selectors .inline-field { flex: 1; }
.decision-selectors select { min-width: 0; flex: 1; }
.decision-summary { margin: 0; display: grid; grid-template-columns: 1fr 1fr; }
.decision-summary > div { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 8px 12px; border-bottom: 1px solid #e4eae8; }
.decision-summary > div:nth-child(odd) { border-right: 1px solid #e4eae8; }
.decision-summary > div.span-all { grid-column: 1 / -1; border-right: 0; }
.decision-summary dt { color: var(--color-muted); font-size: 10px; }
.decision-summary dd { margin: 0; text-align: right; font-family: var(--font-mono); font-size: 11px; overflow-wrap: anywhere; }
.compact-table-scroll { overflow: auto; }
.candidate-scroll { max-height: 210px; border-top: 1px solid var(--color-line); }
.compact-evidence-table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 10px; }
.compact-evidence-table th { padding: 6px 8px; color: var(--color-muted); background: #f0f4f2; text-align: left; font-weight: 700; }
.compact-evidence-table td { padding: 6px 8px; border-top: 1px solid #e5ebe8; vertical-align: top; overflow-wrap: anywhere; }
.compact-evidence-table code { font-family: var(--font-mono); font-size: 10px; white-space: normal; }
.quality-table th:first-child { width: 44%; }
.candidate-table th:first-child { width: 58%; }
.candidate-table th:nth-child(2) { width: 18%; }
.candidate-table tr.is-selected-candidate { background: var(--color-green-soft); box-shadow: inset 3px 0 var(--color-green); }
#reroute-select { min-width: 130px; max-width: 210px; height: 30px; padding: 0 7px; border: 1px solid var(--color-line-strong); border-radius: var(--radius); background: var(--color-surface); font-size: 10px; }
.reroute-comparison { display: grid; }
.reroute-comparison > div { display: grid; grid-template-columns: 78px minmax(0, 1fr); gap: 8px; padding: 8px 12px; border-top: 1px solid #e4eae8; }
.reroute-comparison span { color: var(--color-muted); font-size: 10px; }
.reroute-comparison output { font-family: var(--font-mono); font-size: 10px; line-height: 1.55; overflow-wrap: anywhere; }
.path-proof, .reason-proof { display: grid; gap: 7px; padding: 13px 16px; border-bottom: 1px solid var(--color-line); }
.path-proof span, .reason-proof span { color: var(--color-muted); font-size: 11px; }
.path-proof output, .reason-proof output { font-family: var(--font-mono); font-size: 11px; line-height: 1.7; overflow-wrap: anywhere; }
@@ -412,9 +455,14 @@ h3 { font-size: 14px; line-height: 1.3; }
.route-tools { display: grid; grid-template-columns: auto auto; gap: 5px 7px; justify-items: end; }
.view-mode { grid-column: 1; }
.map-actions { grid-column: 2; }
.layer-toggles { grid-column: 1 / -1; padding: 5px 0 0; border-left: 0; border-top: 1px solid var(--color-line); flex-wrap: wrap; justify-content: flex-end; }
.route-legend { gap: 9px; padding: 6px 12px; }
.route-canvas-wrap, #route-map { min-height: 340px; }
.evidence-list > div { grid-template-columns: 88px 1fr; padding: 9px 12px; }
.comparison-status { max-width: 190px; }
.decision-summary { grid-template-columns: 1fr; }
.decision-summary > div, .decision-summary > div:nth-child(odd) { border-right: 0; }
.decision-summary > div.span-all { grid-column: 1; }
.timeline-controls { padding: 10px 12px; }
.playback-buttons .icon-button { width: 34px; }
.tick-readout { min-width: 76px; }

View File

@@ -8,6 +8,9 @@
historyResult: null,
mode: "point",
selectedTaskId: null,
selectedPlanningStage: "SERVICE",
selectedPathVersion: null,
selectedRerouteIndex: null,
eventTicks: [0],
tickIndex: 0,
timer: null,
@@ -37,11 +40,17 @@
"task-search", "task-status-filter", "task-filter-count", "history-task-body",
"task-workspace", "route-map", "route-empty", "loading-state", "loading-label",
"floor-select", "show-historical", "map-fit", "path-fit", "zoom-in", "zoom-out",
"reset-view", "inspector-task-id", "task-status",
"reset-view", "show-initial-route", "show-current-route", "show-move-trace",
"show-hold-points", "inspector-task-id", "task-status",
"evidence-endpoints", "evidence-vehicle", "evidence-history-vehicle",
"evidence-assigned-time", "evidence-waiting", "evidence-completion",
"evidence-hold-retry", "evidence-plc", "route-difference",
"invalid-history-segments", "planned-path-output", "reason-output",
"route-comparability", "route-quality-comparison", "route-stage-select",
"route-version-select", "decision-shortest-distance",
"decision-selected-distance", "decision-detour-ratio", "decision-candidate-count",
"decision-reason", "route-quality-body", "route-candidate-body",
"reroute-select", "reroute-reason", "reroute-old-path", "reroute-new-path",
"timeline-panel", "step-back", "play-timeline", "pause-timeline", "step-forward",
"tick-readout", "timeline-slider", "playback-speed", "event-type-filter",
"event-count", "event-table-body", "safety-view", "safety-metrics",
@@ -64,7 +73,24 @@
renderRoute();
fitCurrentViewMode();
});
elements["show-historical"].addEventListener("change", renderRoute);
["show-historical", "show-initial-route", "show-current-route", "show-move-trace",
"show-hold-points"].forEach(function (id) {
elements[id].addEventListener("change", renderRoute);
});
elements["route-stage-select"].addEventListener("change", function () {
state.selectedPlanningStage = elements["route-stage-select"].value;
state.selectedPathVersion = null;
renderRouteDecision(selectedTask());
});
elements["route-version-select"].addEventListener("change", function () {
state.selectedPathVersion = Number(elements["route-version-select"].value);
renderRouteDecision(selectedTask());
});
elements["reroute-select"].addEventListener("change", function () {
state.selectedRerouteIndex = Number(elements["reroute-select"].value);
renderRerouteComparison(selectedTask());
renderRoute();
});
elements["map-fit"].addEventListener("click", function () { setViewMode("map"); });
elements["path-fit"].addEventListener("click", function () { setViewMode("path"); });
elements["zoom-in"].addEventListener("click", function () { zoomRoute(0.8); });
@@ -220,6 +246,9 @@
function selectResult(result, taskId) {
state.selectedTaskId = taskId;
state.tickIndex = 0;
state.selectedPlanningStage = "SERVICE";
state.selectedPathVersion = null;
state.selectedRerouteIndex = null;
var selected = result.tasks.find(function (task) {
return task.simulationTaskId === taskId;
});
@@ -283,11 +312,162 @@
+ " / 历史 " + comparison.historicalOnlyEdgeCount);
setText("invalid-history-segments", formatInvalidSegments(
comparison.invalidHistoricalSegments));
setEvidenceBadge("route-comparability", task.routeComparability,
comparabilityLabel(task.routeComparability));
setEvidenceBadge("route-quality-comparison", task.routeQualityComparison,
qualityCategoryLabel(task.routeQualityComparison));
elements["route-stage-select"].value = state.selectedPlanningStage;
renderRouteDecision(task);
renderRerouteComparison(task);
elements["planned-path-output"].textContent = task.plannedPath.length
? task.plannedPath.join(" → ") : "无规划路径";
elements["reason-output"].textContent = task.reason || task.status;
}
/** 使用后端保存的阶段证据渲染候选、距离边界和固定质量元组。 */
function renderRouteDecision(task) {
var stage = state.selectedPlanningStage;
var evidence = task ? planningEvidence(task, stage) : [];
var versions = evidence.map(function (item) { return Number(item.pathVersion); })
.filter(function (value, index, values) { return values.indexOf(value) === index; })
.sort(function (left, right) { return left - right; });
var versionSelect = elements["route-version-select"];
var options = document.createDocumentFragment();
versions.forEach(function (version) {
var option = document.createElement("option");
option.value = String(version);
option.textContent = "v" + version + (version === 1 ? " · 初始" : " · 重规划");
options.appendChild(option);
});
versionSelect.replaceChildren(options);
versionSelect.disabled = !versions.length;
if (!versions.length) {
state.selectedPathVersion = null;
clearRouteDecision();
return;
}
if (versions.indexOf(state.selectedPathVersion) < 0) {
state.selectedPathVersion = versions[versions.length - 1];
}
versionSelect.value = String(state.selectedPathVersion);
var decision = evidence.find(function (item) {
return Number(item.pathVersion) === state.selectedPathVersion;
});
if (!decision) {
clearRouteDecision();
return;
}
setText("decision-shortest-distance", formatDistance(decision.shortestPhysicalDistance));
setText("decision-selected-distance", formatDistance(decision.selectedPhysicalDistance));
setText("decision-detour-ratio", formatDetour(decision.detourRatio));
setText("decision-candidate-count", String(decision.candidateCount)
+ (decision.candidateLimitReached ? " · 达上限" : ""));
setText("decision-reason", decision.decisionReason || "PHYSICAL_SHORTEST");
renderQualityTable(decision.quality, decision.historicalReferenceQuality);
renderCandidateTable(decision.candidates || []);
}
/** 接近段和服务段分别读取各自的已采用版本,不在浏览器中重新运行规划。 */
function planningEvidence(task, stage) {
return stage === "APPROACH"
? (task.approachPlanningEvidence || [])
: (task.servicePlanningEvidence || []);
}
/** 质量表固定使用调度器字典序中的六项业务指标。 */
function renderQualityTable(selected, historical) {
var rows = [
["对向冲突", "oppositeDirectionConflicts"],
["占用节点", "occupiedNodes"],
["不兼容预约", "incompatibleReservedEdges"],
["活动路线重叠", "activeRouteOverlapEdges"],
["转弯", "turns"],
["坐标距离", "physicalDistance"]
];
var fragment = document.createDocumentFragment();
rows.forEach(function (metric) {
var row = document.createElement("tr");
appendCell(row, metric[0], false);
appendCell(row, qualityValue(selected, metric[1]), true);
appendCell(row, qualityValue(historical, metric[1]), true);
fragment.appendChild(row);
});
elements["route-quality-body"].replaceChildren(fragment);
}
/** 候选表展示 Java 已评价的全部有界候选,并突出真实采用项。 */
function renderCandidateTable(candidates) {
var fragment = document.createDocumentFragment();
candidates.forEach(function (candidate, index) {
var row = document.createElement("tr");
if (candidate.selected) row.className = "is-selected-candidate";
appendCell(row, (candidate.selected ? "✓ " : "")
+ (candidate.path || []).join(" → "), true);
appendCell(row, formatDistance(candidate.physicalDistance), true);
appendCell(row, compactQuality(candidate.quality), true);
row.dataset.candidateIndex = String(index);
fragment.appendChild(row);
});
if (!candidates.length) {
var empty = document.createElement("tr");
var cell = appendCell(empty, "无候选证据", false);
cell.colSpan = 3;
fragment.appendChild(empty);
}
elements["route-candidate-body"].replaceChildren(fragment);
}
/** 重规划记录直接消费事件中的旧/新剩余路线,用于地图双层叠加。 */
function renderRerouteComparison(task) {
var events = task ? (task.rerouteEvents || []) : [];
var select = elements["reroute-select"];
var options = document.createDocumentFragment();
events.forEach(function (event, index) {
var option = document.createElement("option");
option.value = String(index);
option.textContent = "tick " + event.tick + " · " + event.eventType;
options.appendChild(option);
});
select.replaceChildren(options);
select.disabled = !events.length;
if (!events.length) {
state.selectedRerouteIndex = null;
setText("reroute-reason", "无重规划记录");
setText("reroute-old-path", "--");
setText("reroute-new-path", "--");
return;
}
if (state.selectedRerouteIndex === null
|| state.selectedRerouteIndex >= events.length) {
state.selectedRerouteIndex = latestSuccessfulReroute(events);
}
select.value = String(state.selectedRerouteIndex);
var event = events[state.selectedRerouteIndex];
var detail = event.rerouteDecision;
setText("reroute-reason", event.eventType + " · " + (event.reason || "--"));
setText("reroute-old-path", detail
? detail.oldRemainingPath.join(" → ") : "未发生路线替换");
setText("reroute-new-path", detail
? detail.newRemainingPath.join(" → ") : "未发生路线替换");
}
function latestSuccessfulReroute(events) {
for (var index = events.length - 1; index >= 0; index -= 1) {
if (events[index].rerouteDecision) return index;
}
return events.length - 1;
}
function clearRouteDecision() {
["decision-shortest-distance", "decision-selected-distance", "decision-detour-ratio",
"decision-candidate-count", "decision-reason"].forEach(function (id) {
setText(id, "--");
});
elements["route-quality-body"].replaceChildren();
elements["route-candidate-body"].replaceChildren();
}
function populateFloors() {
var task = selectedTask();
if (!state.bootstrap) return;
@@ -347,8 +527,12 @@
var traversedMoves = moves.filter(function (event) { return Number(event.tick) <= currentTick; });
var approach = approachPath(task, moves);
var historical = elements["show-historical"].checked ? task.historicalPath : [];
var initialService = initialServicePath(task);
var rerouteDecision = selectedRerouteDecision(task);
var currentSite = currentVehicleSite(task, traversedMoves);
var allIds = unique([].concat(task.plannedPath, historical, approach,
var allIds = unique([].concat(task.plannedPath, historical, approach, initialService,
rerouteDecision ? rerouteDecision.oldRemainingPath : [],
rerouteDecision ? rerouteDecision.newRemainingPath : [],
moves.reduce(function (values, event) {
if (event.fromSite) values.push(event.fromSite);
if (event.toSite) values.push(event.toSite);
@@ -365,8 +549,20 @@
hideMapHint();
drawPath(layer, approach, "approach", project);
if (elements["show-initial-route"].checked) {
drawPath(layer, initialService, "initial-service", project);
}
drawRouteComparison(layer, task, project);
drawMoveSegments(layer, traversedMoves, "traversed", project);
if (rerouteDecision) {
drawPath(layer, rerouteDecision.oldRemainingPath, "reroute-old", project);
drawPath(layer, rerouteDecision.newRemainingPath, "reroute-new", project);
}
if (elements["show-move-trace"].checked) {
drawMoveSegments(layer, traversedMoves, "traversed", project);
}
if (elements["show-hold-points"].checked) {
drawTrafficEventMarkers(layer, events, currentTick, project);
}
var passedIds = new Set();
traversedMoves.forEach(function (event) {
@@ -374,7 +570,8 @@
if (event.toSite) passedIds.add(event.toSite);
});
var lastEvent = lastEventAt(events, currentTick);
var holdSite = lastEvent && isHold(lastEvent.eventType)
var holdSite = elements["show-hold-points"].checked
&& lastEvent && isHold(lastEvent.eventType)
? (lastEvent.toSite || lastEvent.fromSite) : null;
var comparisonNodes = comparisonNodeIds(task,
elements["show-historical"].checked);
@@ -466,24 +663,65 @@
/** 按后端给出的边集合差异叠加共同、仅新规划和仅历史路线。 */
function drawRouteComparison(layer, task, project) {
var comparison = task.pathComparison;
var showCurrent = elements["show-current-route"].checked;
var showHistorical = elements["show-historical"].checked;
if (!comparison) {
if (elements["show-historical"].checked) {
if (showHistorical) {
drawPath(layer, task.historicalPath || [], "historical", project);
}
drawPath(layer, task.plannedPath || [], "service", project);
if (showCurrent) drawPath(layer, task.plannedPath || [], "service", project);
return;
}
drawEvidenceEdges(layer, comparison.sharedEdges,
"shared-base planned-only", project);
drawEvidenceEdges(layer, comparison.plannedOnlyEdges, "planned-only", project);
if (elements["show-historical"].checked) {
if (showCurrent) {
drawEvidenceEdges(layer, comparison.sharedEdges,
"shared-overlay historical-only", project);
"shared-base planned-only", project);
drawEvidenceEdges(layer, comparison.plannedOnlyEdges, "planned-only", project);
}
if (showHistorical) {
drawEvidenceEdges(layer, comparison.sharedEdges,
showCurrent ? "shared-overlay historical-only" : "historical-only", project);
drawEvidenceEdges(layer, comparison.historicalOnlyEdges,
"historical-only", project);
}
}
/** 返回初次采用的完整服务路径,专门与运行中的最终版本叠加。 */
function initialServicePath(task) {
var initial = (task.servicePlanningEvidence || []).find(function (evidence) {
return Number(evidence.pathVersion) === 1 && !evidence.reroute;
});
return initial ? initial.selectedPath : (task.plannedPath || []);
}
/** 返回检查器当前选择的成功换路证据;触发或拒绝事件没有路线叠加。 */
function selectedRerouteDecision(task) {
var events = task.rerouteEvents || [];
if (state.selectedRerouteIndex === null) return null;
var selected = events[state.selectedRerouteIndex];
return selected ? selected.rerouteDecision : null;
}
/** 在事件实际发生的节点绘制 HOLD 与 REROUTE 点,不将目标站误画成触发位置。 */
function drawTrafficEventMarkers(layer, events, currentTick, project) {
events.filter(function (event) {
return Number(event.tick) <= currentTick
&& (isHold(event.eventType) || event.eventType.indexOf("REROUTE") === 0);
}).forEach(function (event) {
var site = siteById(event.fromSite || event.toSite);
var point = project(site);
if (!point) return;
var reroute = event.eventType.indexOf("REROUTE") === 0;
var marker = svgElement("circle", {
cx: point.x, cy: point.y, r: reroute ? 8 : 6,
class: "traffic-event-marker " + (reroute ? "reroute-point" : "hold-point")
});
var title = svgElement("title");
title.textContent = event.eventType + " · " + (event.reason || "--");
marker.appendChild(title);
layer.appendChild(marker);
});
}
/** 将证据 DTO 中的端点直接画成线,不在浏览器中重新判断或规划邻接关系。 */
function drawEvidenceEdges(layer, edges, className, project) {
(edges || []).forEach(function (edge) {
@@ -611,7 +849,10 @@
function selectedRouteSiteIds() {
var task = selectedTask();
if (!task) return [];
var reroute = selectedRerouteDecision(task);
var ids = [].concat(task.plannedPath || [], task.historicalPath || [],
initialServicePath(task), reroute ? reroute.oldRemainingPath : [],
reroute ? reroute.newRemainingPath : [],
[task.physicalStartSite, task.physicalEndSite]);
eventsForTask(currentResult(), task.simulationTaskId).forEach(function (event) {
if (event.fromSite) ids.push(event.fromSite);
@@ -1083,9 +1324,71 @@
"evidence-hold-retry", "evidence-plc", "route-difference",
"invalid-history-segments", "planned-path-output", "reason-output"]
.forEach(function (id) { setText(id, "--"); });
setEvidenceBadge("route-comparability", null, "--");
setEvidenceBadge("route-quality-comparison", null, "--");
elements["route-version-select"].replaceChildren();
elements["route-version-select"].disabled = true;
elements["reroute-select"].replaceChildren();
elements["reroute-select"].disabled = true;
setText("reroute-reason", "--");
setText("reroute-old-path", "--");
setText("reroute-new-path", "--");
clearRouteDecision();
setTaskStatus("--");
}
/** 将历史路径可比性和路线质量类别转换为紧凑状态徽标。 */
function setEvidenceBadge(id, value, label) {
var passValues = ["COMPARABLE", "EXACT_MATCH", "SHORTER_PHYSICAL_DISTANCE",
"SAME_DISTANCE_BETTER_QUALITY", "BOUNDED_DETOUR_FEWER_CONFLICTS"];
var warningValues = ["ENDPOINT_MISMATCH", "WORSE_QUALITY"];
var className = passValues.indexOf(value) >= 0 ? "is-pass"
: warningValues.indexOf(value) >= 0 ? "is-warning"
: value === "INVALID_HISTORY_PATH" ? "is-fail" : "is-neutral";
elements[id].textContent = label;
elements[id].className = "evidence-badge " + className;
elements[id].title = value || "无比较结论";
}
function comparabilityLabel(value) {
return value === "COMPARABLE" ? "可公平比较"
: value === "ENDPOINT_MISMATCH" ? "端点不一致"
: value === "INVALID_HISTORY_PATH" ? "历史路径无效" : "无比较";
}
function qualityCategoryLabel(value) {
var labels = {
EXACT_MATCH: "路径一致",
SHORTER_PHYSICAL_DISTANCE: "物理距离更短",
SAME_DISTANCE_BETTER_QUALITY: "同距质量更优",
BOUNDED_DETOUR_FEWER_CONFLICTS: "有界绕行降冲突",
WORSE_QUALITY: "未优于历史"
};
return labels[value] || "不参与质量比较";
}
function qualityValue(quality, field) {
if (!quality || quality[field] === null || quality[field] === undefined) return "--";
return field === "physicalDistance"
? Number(quality[field]).toFixed(2) : String(quality[field]);
}
function compactQuality(quality) {
if (!quality) return "--";
return [quality.oppositeDirectionConflicts, quality.occupiedNodes,
quality.incompatibleReservedEdges, quality.activeRouteOverlapEdges,
quality.turns].join("/");
}
function formatDistance(value) {
return value === null || value === undefined ? "--" : Number(value).toFixed(2);
}
function formatDetour(value) {
if (value === null || value === undefined) return "--";
return ((Number(value) - 1) * 100).toFixed(1) + "%";
}
function setTaskStatus(status) {
var className = status === "COMPLETED" ? "status-pass"
: status === "FAILED" ? "status-fail"

View File

@@ -124,7 +124,13 @@
<button id="reset-view" type="button" class="icon-button compact" title="复位为完整地图" aria-label="复位为完整地图">⌂</button>
</div>
<label class="inline-field"><span>楼层</span><select id="floor-select"></select></label>
<label class="toggle-field"><input id="show-historical" type="checkbox" checked><span>历史参考</span></label>
<div class="layer-toggles" aria-label="路线图层">
<label class="toggle-field"><input id="show-historical" type="checkbox" checked><span>历史</span></label>
<label class="toggle-field"><input id="show-initial-route" type="checkbox" checked><span>初始</span></label>
<label class="toggle-field"><input id="show-current-route" type="checkbox" checked><span>当前</span></label>
<label class="toggle-field"><input id="show-move-trace" type="checkbox" checked><span>MOVE</span></label>
<label class="toggle-field"><input id="show-hold-points" type="checkbox" checked><span>HOLD</span></label>
</div>
</div>
</div>
<div class="route-legend" aria-label="路径图例">
@@ -133,7 +139,10 @@
<span><i class="legend-line shared"></i>共同路段</span>
<span><i class="legend-line planned-only"></i>仅新规划</span>
<span><i class="legend-line historical-only"></i>仅历史</span>
<span><i class="legend-line initial-service"></i>初始规划</span>
<span><i class="legend-line traversed"></i>已通过</span>
<span><i class="legend-line reroute-old"></i>换路前</span>
<span><i class="legend-line reroute-new"></i>换路后</span>
<span><i class="legend-dot hold"></i>HOLD</span>
</div>
<div class="route-canvas-wrap">
@@ -175,6 +184,52 @@
<div><dt>路径边差</dt><dd id="route-difference">共同 -- / 新增 -- / 历史 --</dd></div>
<div><dt>历史无效段</dt><dd id="invalid-history-segments">--</dd></div>
</dl>
<section class="decision-audit" aria-labelledby="decision-audit-title">
<div class="inspector-section-bar">
<div>
<span class="section-kicker">路线决策</span>
<h3 id="decision-audit-title">候选与质量</h3>
</div>
<div class="comparison-status">
<span id="route-comparability" class="evidence-badge is-neutral">--</span>
<span id="route-quality-comparison" class="evidence-badge is-neutral">--</span>
</div>
</div>
<div class="decision-selectors">
<label class="inline-field"><span>阶段</span><select id="route-stage-select"><option value="SERVICE">服务段</option><option value="APPROACH">接近段</option></select></label>
<label class="inline-field"><span>版本</span><select id="route-version-select"></select></label>
</div>
<dl id="route-decision-summary" class="decision-summary">
<div><dt>最短距离</dt><dd id="decision-shortest-distance">--</dd></div>
<div><dt>选中距离</dt><dd id="decision-selected-distance">--</dd></div>
<div><dt>绕行比例</dt><dd id="decision-detour-ratio">--</dd></div>
<div><dt>候选数量</dt><dd id="decision-candidate-count">--</dd></div>
<div class="span-all"><dt>选择原因</dt><dd id="decision-reason">--</dd></div>
</dl>
<div class="compact-table-scroll">
<table class="compact-evidence-table quality-table">
<thead><tr><th>质量项</th><th>选中</th><th>历史</th></tr></thead>
<tbody id="route-quality-body"></tbody>
</table>
</div>
<div class="compact-table-scroll candidate-scroll">
<table class="compact-evidence-table candidate-table">
<thead><tr><th>候选路径</th><th>距离</th><th>质量</th></tr></thead>
<tbody id="route-candidate-body"></tbody>
</table>
</div>
</section>
<section class="reroute-audit" aria-labelledby="reroute-audit-title">
<div class="inspector-section-bar">
<div><span class="section-kicker">交通 HOLD</span><h3 id="reroute-audit-title">重规划记录</h3></div>
<select id="reroute-select" aria-label="选择重规划记录"></select>
</div>
<div id="reroute-comparison" class="reroute-comparison">
<div><span>触发 / 结果</span><output id="reroute-reason">--</output></div>
<div><span>换路前</span><output id="reroute-old-path">--</output></div>
<div><span>换路后</span><output id="reroute-new-path">--</output></div>
</div>
</section>
<div class="path-proof">
<span>调度中心 plannedPath</span>
<output id="planned-path-output">--</output>

View File

@@ -124,8 +124,16 @@ class HistoricalDispatchSimulatorTest {
assertEquals(1, task.getRerouteCount());
assertTrue(result.getEvents().stream().anyMatch(event ->
"REROUTE_TRIGGER".equals(event.getType())));
assertTrue(result.getEvents().stream().anyMatch(event ->
"REROUTE".equals(event.getType())));
SimulationTickEvent reroute = result.getEvents().stream()
.filter(event -> "REROUTE".equals(event.getType()))
.findFirst().orElseThrow(AssertionError::new);
assertEquals(Arrays.asList("A", "B", "D"),
reroute.getRerouteDecision().getOldRemainingPath());
assertEquals(Arrays.asList("A", "C", "D"),
reroute.getRerouteDecision().getNewRemainingPath());
assertEquals(2, reroute.getRerouteDecision().getPathVersion());
assertTrue(reroute.getRerouteDecision().getNewQuality()
.compareTo(reroute.getRerouteDecision().getOldQuality()) < 0);
assertEquals(2, task.getPlanningEvidence().stream()
.map(RoutePlanningEvidence::getPathVersion)
.distinct().count());

View File

@@ -116,6 +116,18 @@ class HistoricalDispatchVisualizerServerTest {
assertTrue(page.body.contains("id=\"reset-view\""));
assertTrue(page.body.contains("id=\"route-difference\""));
assertTrue(page.body.contains("id=\"invalid-history-segments\""));
assertTrue(page.body.contains("id=\"show-initial-route\""));
assertTrue(page.body.contains("id=\"show-current-route\""));
assertTrue(page.body.contains("id=\"show-move-trace\""));
assertTrue(page.body.contains("id=\"show-hold-points\""));
assertTrue(page.body.contains("id=\"route-comparability\""));
assertTrue(page.body.contains("id=\"route-quality-comparison\""));
assertTrue(page.body.contains("id=\"route-stage-select\""));
assertTrue(page.body.contains("id=\"route-version-select\""));
assertTrue(page.body.contains("id=\"route-quality-body\""));
assertTrue(page.body.contains("id=\"route-candidate-body\""));
assertTrue(page.body.contains("id=\"reroute-select\""));
assertTrue(page.body.contains("id=\"reroute-comparison\""));
assertEquals(200, stylesheet.status);
assertEquals("text/css; charset=utf-8", stylesheet.contentType);
assertTrue(stylesheet.body.contains("--color-service-path"));
@@ -123,9 +135,15 @@ class HistoricalDispatchVisualizerServerTest {
assertTrue(stylesheet.body.contains(".planned-only"));
assertTrue(stylesheet.body.contains(".historical-only"));
assertTrue(stylesheet.body.contains(".shared-overlay"));
assertTrue(stylesheet.body.contains(".initial-service"));
assertTrue(stylesheet.body.contains(".reroute-old"));
assertTrue(stylesheet.body.contains(".reroute-new"));
assertEquals(200, script.status);
assertEquals("application/javascript; charset=utf-8", script.contentType);
assertTrue(script.body.contains("/api/simulations/point-to-point"));
assertTrue(script.body.contains("approachPlanningEvidence"));
assertTrue(script.body.contains("servicePlanningEvidence"));
assertTrue(script.body.contains("rerouteEvents"));
}
@Test

View File

@@ -104,6 +104,8 @@ class SimulationVisualizationMapperTest {
assertTrue(serviceEvidence.containsKey("quality"));
assertTrue(serviceEvidence.containsKey("candidates"));
assertEquals(sourceTask.getRerouteCount(), tasks.get(0).get("rerouteCount"));
assertEquals(Collections.singletonList(1), tasks.get(0).get("pathVersions"));
assertTrue(tasks.get(0).containsKey("rerouteEvents"));
Map<String, Object> comparison = map(tasks.get(0).get("pathComparison"));
assertEquals(0, comparison.get("sharedEdgeCount"));
assertEquals(sourceTask.getPlannedPath().size() - 1,