From ff8aabdf6aedbdeeff48534bd22ab153647b3ba2 Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Wed, 5 Aug 2026 16:50:47 +0800 Subject: [PATCH] feat(javaTest): map full topology and route differences --- .../RouteTopologyEvidenceMapper.java | 186 ++++++++++++++++++ .../RouteTopologyEvidenceMapperTest.java | 109 ++++++++++ 2 files changed, 295 insertions(+) create mode 100644 javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/RouteTopologyEvidenceMapper.java create mode 100644 javaTest/src/test/java/com/ft546/wcs/shadow/history/visualizer/RouteTopologyEvidenceMapperTest.java diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/RouteTopologyEvidenceMapper.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/RouteTopologyEvidenceMapper.java new file mode 100644 index 0000000..34f7d23 --- /dev/null +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/RouteTopologyEvidenceMapper.java @@ -0,0 +1,186 @@ +package com.ft546.wcs.shadow.history.visualizer; + +import com.ft546.wcs.demo.model.Site; +import com.ft546.wcs.demo.traffic.GridMap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * 把权威网格拓扑和新旧路线差异转换为页面可直接审计的证据。 + * + *

本类只读取 {@link GridMap} 已建立的邻接关系,不根据坐标猜测道路,也不调用路径规划器。 + * 历史路径即使包含未知站点或不相邻跳点,也只会形成无效段记录,不会被包装成真实地图边。 + */ +final class RouteTopologyEvidenceMapper { + + /** + * 导出完整地图中的全部无向道路边,供页面绘制当前楼层的完整地图底图。 + * + *

{@code GridMap} 为每条道路保存正反两个邻接引用。本方法把端点按站点编号规范化并用 + * 有序集合去重,因此页面对同一条物理道路只绘制一次,且不同运行得到稳定顺序。 + */ + List> fullMapEdges(GridMap gridMap) { + requireMap(gridMap); + Set edges = new TreeSet(); + + // 遍历权威邻接表,而不是用 x/y 相差 1 来推测道路是否存在。 + for (String siteId : new TreeSet(gridMap.getSites().keySet())) { + for (String neighbor : gridMap.getNeighbors(siteId)) { + edges.add(new EdgeKey(siteId, neighbor)); + } + } + return edgeMaps(gridMap, edges); + } + + /** + * 比较历史参考路径与调度中心新规划路径使用的物理边。 + * + *

共同边用于显示两条路线重合的位置;两侧专属边用于显示分叉和汇合。比较采用无向物理 + * 边,符合地图资源口径;原有节点序列级 {@code pathChanged} 仍由上层保留,不在这里改写。 + */ + Map compare(GridMap gridMap, List historicalPath, + List plannedPath) { + requireMap(gridMap); + RouteEdges historical = routeEdges(gridMap, historicalPath, true); + RouteEdges planned = routeEdges(gridMap, plannedPath, false); + + Set shared = intersection(historical.valid, planned.valid); + Set plannedOnly = difference(planned.valid, shared); + Set historicalOnly = difference(historical.valid, shared); + + Map value = new LinkedHashMap(); + value.put("sharedEdges", edgeMaps(gridMap, shared)); + value.put("plannedOnlyEdges", edgeMaps(gridMap, plannedOnly)); + value.put("historicalOnlyEdges", edgeMaps(gridMap, historicalOnly)); + value.put("invalidHistoricalSegments", + Collections.unmodifiableList(historical.invalid)); + value.put("sharedEdgeCount", shared.size()); + value.put("plannedOnlyEdgeCount", plannedOnly.size()); + value.put("historicalOnlyEdgeCount", historicalOnly.size()); + return Collections.unmodifiableMap(value); + } + + /** + * 将一条节点序列转换为经过校验的物理边集合。 + * + *

历史路径需要向使用者解释脏数据,因此记录未知站点和非邻接跳点;新规划来自当前算法, + * 若出现无效段则同样不会伪造地图边,但该异常仍应由既有规划连续性测试负责阻断。 + */ + private RouteEdges routeEdges(GridMap gridMap, List path, + boolean reportInvalid) { + RouteEdges result = new RouteEdges(); + if (path == null || path.size() < 2) return result; + + for (int index = 1; index < path.size(); index++) { + String from = path.get(index - 1); + String to = path.get(index); + Site fromSite = gridMap.getSite(from); + Site toSite = gridMap.getSite(to); + if (fromSite == null || toSite == null) { + if (reportInvalid) { + result.invalid.add(invalid(from, to, "UNKNOWN_SITE")); + } + continue; + } + if (!gridMap.getNeighbors(from).contains(to)) { + if (reportInvalid) { + result.invalid.add(invalid(from, to, "NOT_ADJACENT")); + } + continue; + } + result.valid.add(new EdgeKey(from, to)); + } + return result; + } + + /** 把规范化边转换为包含端点和楼层的 JSON 友好结构。 */ + private List> edgeMaps(GridMap gridMap, Set edges) { + List> values = new ArrayList>(); + for (EdgeKey edge : edges) values.add(edge(gridMap, edge)); + return Collections.unmodifiableList(values); + } + + /** 返回页面绘制一条真实地图边所需的最小业务字段。 */ + private Map edge(GridMap gridMap, EdgeKey edge) { + Map value = new LinkedHashMap(); + value.put("fromSite", edge.from); + value.put("toSite", edge.to); + value.put("floor", gridMap.getSite(edge.from).getFloor()); + return Collections.unmodifiableMap(value); + } + + /** 返回一段不能映射到当前完整地图的历史路径诊断。 */ + private Map invalid(String from, String to, String reason) { + Map value = new LinkedHashMap(); + value.put("fromSite", from); + value.put("toSite", to); + value.put("reason", reason); + return Collections.unmodifiableMap(value); + } + + /** 计算两个路径边集合的交集,不修改调用方集合。 */ + private Set intersection(Set left, Set right) { + Set values = new TreeSet(left); + values.retainAll(right); + return values; + } + + /** 计算某条路径独有的边,不修改调用方集合。 */ + private Set difference(Set source, Set removed) { + Set values = new TreeSet(source); + values.removeAll(removed); + return values; + } + + /** 防止页面证据在缺少地图上下文时静默生成空结果。 */ + private void requireMap(GridMap gridMap) { + if (gridMap == null) throw new IllegalArgumentException("gridMap is required"); + } + + /** 保存一条路线转换后的有效边和历史数据质量问题。 */ + private static final class RouteEdges { + private final Set valid = new TreeSet(); + private final List> invalid = + new ArrayList>(); + } + + /** + * 用端点排序后的业务主键表示无向物理边,避免 A-B 与 B-A 被统计为两条道路。 + */ + private static final class EdgeKey implements Comparable { + private final String from; + private final String to; + + private EdgeKey(String left, String right) { + if (left.compareTo(right) <= 0) { + this.from = left; + this.to = right; + } else { + this.from = right; + this.to = left; + } + } + + @Override + public int compareTo(EdgeKey other) { + int fromOrder = from.compareTo(other.from); + return fromOrder != 0 ? fromOrder : to.compareTo(other.to); + } + + @Override + public boolean equals(Object other) { + return other instanceof EdgeKey && compareTo((EdgeKey) other) == 0; + } + + @Override + public int hashCode() { + return 31 * from.hashCode() + to.hashCode(); + } + } +} diff --git a/javaTest/src/test/java/com/ft546/wcs/shadow/history/visualizer/RouteTopologyEvidenceMapperTest.java b/javaTest/src/test/java/com/ft546/wcs/shadow/history/visualizer/RouteTopologyEvidenceMapperTest.java new file mode 100644 index 0000000..682c48f --- /dev/null +++ b/javaTest/src/test/java/com/ft546/wcs/shadow/history/visualizer/RouteTopologyEvidenceMapperTest.java @@ -0,0 +1,109 @@ +package com.ft546.wcs.shadow.history.visualizer; + +import com.ft546.wcs.demo.model.Site; +import com.ft546.wcs.demo.traffic.GridMap; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RouteTopologyEvidenceMapperTest { + + @Test + void exportsEachUndirectedMapEdgeOnceInStableOrder() { + GridMap map = branchMap(); + + List> edges = + new RouteTopologyEvidenceMapper().fullMapEdges(map); + + assertEquals(5, edges.size()); + assertEdge(edges.get(0), "A", "B", 1); + assertEdge(edges.get(1), "B", "C", 1); + assertEdge(edges.get(2), "B", "E", 1); + assertEdge(edges.get(3), "C", "D", 1); + assertEdge(edges.get(4), "D", "E", 1); + } + + @Test + void comparesSharedHistoricalOnlyAndPlannedOnlyEdges() { + GridMap map = branchMap(); + + Map comparison = new RouteTopologyEvidenceMapper().compare(map, + Arrays.asList("A", "B", "C", "D"), + Arrays.asList("A", "B", "E", "D")); + + assertEquals(1, comparison.get("sharedEdgeCount")); + assertEquals(2, comparison.get("plannedOnlyEdgeCount")); + assertEquals(2, comparison.get("historicalOnlyEdgeCount")); + assertEquals(Collections.emptyList(), comparison.get("invalidHistoricalSegments")); + assertEdge(listOfMaps(comparison.get("sharedEdges")).get(0), "A", "B", 1); + assertEdge(listOfMaps(comparison.get("plannedOnlyEdges")).get(0), "B", "E", 1); + assertEdge(listOfMaps(comparison.get("plannedOnlyEdges")).get(1), "D", "E", 1); + assertEdge(listOfMaps(comparison.get("historicalOnlyEdges")).get(0), "B", "C", 1); + assertEdge(listOfMaps(comparison.get("historicalOnlyEdges")).get(1), "C", "D", 1); + } + + @Test + void reportsUnknownAndNonAdjacentHistoricalSegmentsWithoutInventingEdges() { + GridMap map = branchMap(); + + Map comparison = new RouteTopologyEvidenceMapper().compare(map, + Arrays.asList("A", "UNKNOWN", "C", "E"), + Arrays.asList("A", "B", "C", "D")); + + List> invalid = + listOfMaps(comparison.get("invalidHistoricalSegments")); + assertEquals(3, invalid.size()); + assertInvalid(invalid.get(0), "A", "UNKNOWN", "UNKNOWN_SITE"); + assertInvalid(invalid.get(1), "UNKNOWN", "C", "UNKNOWN_SITE"); + assertInvalid(invalid.get(2), "C", "E", "NOT_ADJACENT"); + assertEquals(0, comparison.get("sharedEdgeCount")); + assertEquals(3, comparison.get("plannedOnlyEdgeCount")); + assertEquals(0, comparison.get("historicalOnlyEdgeCount")); + } + + private static GridMap branchMap() { + GridMap map = new GridMap(); + add(map, "A", 0, 0); + add(map, "B", 1, 0); + add(map, "C", 2, 0); + add(map, "D", 3, 0); + add(map, "E", 2, 1); + map.connect("A", "B"); + map.connect("B", "C"); + map.connect("C", "D"); + map.connect("B", "E"); + map.connect("E", "D"); + return map; + } + + private static void add(GridMap map, String id, int x, int y) { + map.addSite(new Site(id, 1, x, y, "TEST")); + } + + private static void assertEdge(Map edge, String from, + String to, int floor) { + assertEquals(from, edge.get("fromSite")); + assertEquals(to, edge.get("toSite")); + assertEquals(floor, edge.get("floor")); + } + + private static void assertInvalid(Map segment, String from, + String to, String reason) { + assertEquals(from, segment.get("fromSite")); + assertEquals(to, segment.get("toSite")); + assertEquals(reason, segment.get("reason")); + } + + @SuppressWarnings("unchecked") + private static List> listOfMaps(Object value) { + List> result = (List>) value; + assertTrue(result != null); + return result; + } +}