feat(javaTest): map full topology and route differences

This commit is contained in:
zgw
2026-08-05 16:50:47 +08:00
parent fad6faf949
commit ff8aabdf6a
2 changed files with 295 additions and 0 deletions

View File

@@ -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;
/**
* 把权威网格拓扑和新旧路线差异转换为页面可直接审计的证据。
*
* <p>本类只读取 {@link GridMap} 已建立的邻接关系,不根据坐标猜测道路,也不调用路径规划器。
* 历史路径即使包含未知站点或不相邻跳点,也只会形成无效段记录,不会被包装成真实地图边。
*/
final class RouteTopologyEvidenceMapper {
/**
* 导出完整地图中的全部无向道路边,供页面绘制当前楼层的完整地图底图。
*
* <p>{@code GridMap} 为每条道路保存正反两个邻接引用。本方法把端点按站点编号规范化并用
* 有序集合去重,因此页面对同一条物理道路只绘制一次,且不同运行得到稳定顺序。
*/
List<Map<String, Object>> fullMapEdges(GridMap gridMap) {
requireMap(gridMap);
Set<EdgeKey> edges = new TreeSet<EdgeKey>();
// 遍历权威邻接表,而不是用 x/y 相差 1 来推测道路是否存在。
for (String siteId : new TreeSet<String>(gridMap.getSites().keySet())) {
for (String neighbor : gridMap.getNeighbors(siteId)) {
edges.add(new EdgeKey(siteId, neighbor));
}
}
return edgeMaps(gridMap, edges);
}
/**
* 比较历史参考路径与调度中心新规划路径使用的物理边。
*
* <p>共同边用于显示两条路线重合的位置;两侧专属边用于显示分叉和汇合。比较采用无向物理
* 边,符合地图资源口径;原有节点序列级 {@code pathChanged} 仍由上层保留,不在这里改写。
*/
Map<String, Object> compare(GridMap gridMap, List<String> historicalPath,
List<String> plannedPath) {
requireMap(gridMap);
RouteEdges historical = routeEdges(gridMap, historicalPath, true);
RouteEdges planned = routeEdges(gridMap, plannedPath, false);
Set<EdgeKey> shared = intersection(historical.valid, planned.valid);
Set<EdgeKey> plannedOnly = difference(planned.valid, shared);
Set<EdgeKey> historicalOnly = difference(historical.valid, shared);
Map<String, Object> value = new LinkedHashMap<String, Object>();
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);
}
/**
* 将一条节点序列转换为经过校验的物理边集合。
*
* <p>历史路径需要向使用者解释脏数据,因此记录未知站点和非邻接跳点;新规划来自当前算法,
* 若出现无效段则同样不会伪造地图边,但该异常仍应由既有规划连续性测试负责阻断。
*/
private RouteEdges routeEdges(GridMap gridMap, List<String> 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<Map<String, Object>> edgeMaps(GridMap gridMap, Set<EdgeKey> edges) {
List<Map<String, Object>> values = new ArrayList<Map<String, Object>>();
for (EdgeKey edge : edges) values.add(edge(gridMap, edge));
return Collections.unmodifiableList(values);
}
/** 返回页面绘制一条真实地图边所需的最小业务字段。 */
private Map<String, Object> edge(GridMap gridMap, EdgeKey edge) {
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("fromSite", edge.from);
value.put("toSite", edge.to);
value.put("floor", gridMap.getSite(edge.from).getFloor());
return Collections.unmodifiableMap(value);
}
/** 返回一段不能映射到当前完整地图的历史路径诊断。 */
private Map<String, Object> invalid(String from, String to, String reason) {
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("fromSite", from);
value.put("toSite", to);
value.put("reason", reason);
return Collections.unmodifiableMap(value);
}
/** 计算两个路径边集合的交集,不修改调用方集合。 */
private Set<EdgeKey> intersection(Set<EdgeKey> left, Set<EdgeKey> right) {
Set<EdgeKey> values = new TreeSet<EdgeKey>(left);
values.retainAll(right);
return values;
}
/** 计算某条路径独有的边,不修改调用方集合。 */
private Set<EdgeKey> difference(Set<EdgeKey> source, Set<EdgeKey> removed) {
Set<EdgeKey> values = new TreeSet<EdgeKey>(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<EdgeKey> valid = new TreeSet<EdgeKey>();
private final List<Map<String, Object>> invalid =
new ArrayList<Map<String, Object>>();
}
/**
* 用端点排序后的业务主键表示无向物理边,避免 A-B 与 B-A 被统计为两条道路。
*/
private static final class EdgeKey implements Comparable<EdgeKey> {
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();
}
}
}

View File

@@ -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<Map<String, Object>> 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<String, Object> 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<String, Object> comparison = new RouteTopologyEvidenceMapper().compare(map,
Arrays.asList("A", "UNKNOWN", "C", "E"),
Arrays.asList("A", "B", "C", "D"));
List<Map<String, Object>> 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<String, Object> 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<String, Object> 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<Map<String, Object>> listOfMaps(Object value) {
List<Map<String, Object>> result = (List<Map<String, Object>>) value;
assertTrue(result != null);
return result;
}
}