feat(javaTest): inspect bounded route decisions on full map
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user