feat(javaTest): map full topology and route differences
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user