| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- import {
- buildRelationArray,
- transformToTopologyData,
- LAYOUT,
- ROOT_IN_ID,
- ROOT_OUT_ID,
- } from '../src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.data';
- import type { TopologyData } from '../src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.data';
- import {
- orderNodesByLevel,
- countCrossings,
- countTotalCrossings,
- } from '../src/views/analysis/warningAnalysis/windPointManage/windTopology/hooks/useTopologyOrdering';
- import { computeLayout } from '../src/views/analysis/warningAnalysis/windPointManage/windTopology/hooks/useTopologyLayout';
- /** 构造测风地点(MineArea) */
- const area = (id: string, level: number, airVolume = 100, extra: Record<string, any> = {}) => ({
- id,
- name: id,
- level,
- airVolume,
- mineCode: 'M1',
- ...extra,
- });
- /** 构造巷道关系(MineAreaRelation) */
- const rel = (id: string, parentId: string, childId: string, extra: Record<string, any> = {}) => ({
- id,
- mineCode: 'M1',
- parentId,
- childId,
- ...extra,
- });
- /** 相邻层边方向规范化:排序不关心渲染方向(parent→child),只关心端点所属层 */
- function pairEdgesOf(data: TopologyData): Map<string, Array<[string, string]>> {
- // 复算 orderNodesByLevel 内部的相邻层边表(与实现保持一致:level 差恰为 1 的巷道边)
- const levelById = new Map<string, number>();
- for (const n of data.nodes) {
- if (n.category !== 1) continue;
- const lv = Number(n.level);
- if (Number.isFinite(lv) && lv >= 0 && lv <= 6) levelById.set(n.id, lv);
- }
- const map = new Map<string, Array<[string, string]>>();
- for (const l of data.links) {
- if (l.kind !== 'roadway') continue;
- const sl = levelById.get(l.source);
- const tl = levelById.get(l.target);
- if (sl === undefined || tl === undefined || Math.abs(sl - tl) !== 1) continue;
- const key = `${Math.min(sl, tl)}-${Math.max(sl, tl)}`;
- if (!map.has(key)) map.set(key, []);
- map.get(key)!.push(sl < tl ? [l.source, l.target] : [l.target, l.source]);
- }
- return map;
- }
- describe('countCrossings 交叉数统计', () => {
- test('秩相对顺序相反的一对边计 1 交叉,相同不计', () => {
- // a 在 x 上方、y 在 b 上方:边 a-y 与 b-x 交叉
- const rankA = new Map([
- ['a', 0],
- ['b', 1],
- ]);
- const rankB = new Map([
- ['x', 0],
- ['y', 1],
- ]);
- expect(
- countCrossings(rankA, rankB, [
- ['a', 'y'],
- ['b', 'x'],
- ])
- ).toBe(1);
- expect(
- countCrossings(rankA, rankB, [
- ['a', 'x'],
- ['b', 'y'],
- ])
- ).toBe(0);
- // 无公共端点秩(防御:缺失端点跳过不计数)
- expect(
- countCrossings(rankA, rankB, [
- ['a', 'y'],
- ['unknown', 'x'],
- ])
- ).toBe(0);
- });
- });
- describe('orderNodesByLevel median 扫描法', () => {
- test('2×2 交叉场景:排序后相邻层交叉数为 0', () => {
- // lv1=[a1,a2]、lv2=[b1,b2](输入序);边 a1→b2、a2→b1 → 初始 1 交叉
- const areas = [area('a1', 1), area('a2', 1), area('b1', 2), area('b2', 2)];
- const data = transformToTopologyData(areas, buildRelationArray(areas, [rel('r1', 'a1', 'b2'), rel('r2', 'a2', 'b1')]));
- const order = orderNodesByLevel(data);
- // 参与层存在且含全部节点
- expect(order.get('1')).toHaveLength(2);
- expect(order.get('2')).toHaveLength(2);
- expect(countTotalCrossings(data, order)).toBe(0);
- });
- test('确定性:同一输入两次调用结果完全一致(含 barycenter 方法)', () => {
- const areas = [area('a1', 1), area('a2', 1), area('a3', 1), area('b1', 2), area('b2', 2), area('b3', 2)];
- const relations = buildRelationArray(areas, [
- rel('r1', 'a1', 'b2'),
- rel('r2', 'a2', 'b3'),
- rel('r3', 'a3', 'b1'),
- rel('r1b', 'a1', 'b3'),
- rel('r2b', 'a2', 'b1'),
- ]);
- const data = transformToTopologyData(areas, relations);
- for (const method of ['median', 'barycenter'] as const) {
- const o1 = orderNodesByLevel(data, { method });
- const o2 = orderNodesByLevel(data, { method });
- expect(o1.get('1')).toEqual(o2.get('1'));
- expect(o1.get('2')).toEqual(o2.get('2'));
- }
- });
- test('孤立节点(无相邻层边)排在层尾且保持相对输入序', () => {
- // x1/x2 邻居均为 a1(位置相同 → 保持输入序);y 无任何相邻层边 → +Infinity 排层尾
- const areas = [area('a1', 1), area('x1', 2), area('x2', 2), area('y', 2)];
- const data = transformToTopologyData(areas, buildRelationArray(areas, [rel('r1', 'a1', 'x1'), rel('r2', 'a1', 'x2')]));
- const order = orderNodesByLevel(data);
- expect(order.get('2')).toEqual(['point:x1', 'point:x2', 'point:y']);
- });
- test('未分级列保持输入序,不参与扫描', () => {
- // level 0/6 已是有效层级(进风井/回风井),未分级用越界值 7 表示
- const areas = [area('g1', 7), area('g2', 7), area('g3', 7)];
- const data = transformToTopologyData(areas, []);
- const order = orderNodesByLevel(data);
- expect(order.get('ungraded')).toEqual(['point:g1', 'point:g2', 'point:g3']);
- });
- test('跨层边(level 差 >1)不参与排序:中间层保持输入序,不抛错', () => {
- // 仅 a1(lv1)→c1(lv3) 跨层边;lv2 的 b1/b2 无相邻层边 → 均排层尾且保持相对顺序
- const areas = [area('a1', 1), area('b1', 2), area('b2', 2), area('c1', 3)];
- const data = transformToTopologyData(areas, buildRelationArray(areas, [rel('r1', 'a1', 'c1')]));
- const order = orderNodesByLevel(data);
- expect(order.get('2')).toEqual(['point:b1', 'point:b2']);
- expect(countTotalCrossings(data, order)).toBe(0);
- });
- test('空数据(仅地面节点):各层空数组 + ungraded 空数组,不抛错', () => {
- const data: TopologyData = {
- nodes: [
- { id: ROOT_IN_ID, name: '地面', category: 0 },
- { id: ROOT_OUT_ID, name: '地面', category: 0 },
- ],
- links: [],
- };
- const order = orderNodesByLevel(data);
- for (const key of ['0', '1', '2', '3', '4', '5', '6', 'ungraded']) {
- expect(order.get(key)).toEqual([]);
- }
- });
- test('sweepRounds 配置生效且不抛错', () => {
- const areas = [area('a1', 1), area('a2', 1), area('b1', 2), area('b2', 2)];
- const data = transformToTopologyData(areas, buildRelationArray(areas, [rel('r1', 'a1', 'b2'), rel('r2', 'a2', 'b1')]));
- const order = orderNodesByLevel(data, { sweepRounds: 1 });
- expect(countTotalCrossings(data, order)).toBe(0);
- });
- test('初始序为输入序(无相邻层边时列序完全不变)', () => {
- // 无任何巷道边:各层节点保持 data.nodes 输入序
- const areas = [area('a1', 1, 10), area('a2', 1, 90), area('b1', 2, 50), area('b2', 2, 30)];
- const data = transformToTopologyData(areas, []);
- const order = orderNodesByLevel(data);
- expect(order.get('1')).toEqual(['point:a1', 'point:a2']);
- expect(order.get('2')).toEqual(['point:b1', 'point:b2']);
- });
- test('新增层级 lv0(进风井)/lv6(回风井) 参与排序:相邻层对 0-1、5-6 交叉消除', () => {
- // lv0=[a0,a1]、lv1=[b1,b2]:边 a0→b2、a1→b1 → 初始 1 交叉
- // lv5=[e1,e2]、lv6=[f1,f2]:边 e1→f2、e2→f1 → 初始 1 交叉
- const areas = [area('a0', 0), area('a1', 0), area('b1', 1), area('b2', 1), area('e1', 5), area('e2', 5), area('f1', 6), area('f2', 6)];
- const relations = buildRelationArray(areas, [rel('r1', 'a0', 'b2'), rel('r2', 'a1', 'b1'), rel('r3', 'e1', 'f2'), rel('r4', 'e2', 'f1')]);
- const data = transformToTopologyData(areas, relations);
- const order = orderNodesByLevel(data);
- expect(order.get('0')).toHaveLength(2);
- expect(order.get('1')).toHaveLength(2);
- expect(order.get('5')).toHaveLength(2);
- expect(order.get('6')).toHaveLength(2);
- expect(countTotalCrossings(data, order)).toBe(0);
- });
- });
- describe('computeLayout 与排序集成', () => {
- const centerY = 350; // 700 / 2
- const laneX = (idx: number) => LAYOUT.margin + LAYOUT.colGap * idx;
- test('2×2 交叉场景:排序后坐标合法、x 列序不变、lv2 按交叉最小化重排', () => {
- const areas = [area('a1', 1), area('a2', 1), area('b1', 2), area('b2', 2)];
- const data = transformToTopologyData(areas, buildRelationArray(areas, [rel('r1', 'a1', 'b2'), rel('r2', 'a2', 'b1')]));
- const pos = computeLayout(data, 1200, 700).positions;
- for (const a of areas) {
- const p = pos[`point:${a.id}`];
- expect(p).toBeDefined();
- expect(Number.isFinite(p.x)).toBe(true);
- expect(Number.isFinite(p.y)).toBe(true);
- expect(p.y).toBeGreaterThanOrEqual(0);
- expect(p.y).toBeLessThan(700);
- }
- // x 列序不变:lv1 同列、lv2 同列
- expect(pos['point:a1'].x).toBe(pos['point:a2'].x);
- expect(pos['point:b1'].x).toBe(pos['point:b2'].x);
- expect(pos['point:a1'].x).toBe(laneX(2)); // 总进(0) lv0(1) lv1(2)
- expect(pos['point:b1'].x).toBe(laneX(3));
- // 排序生效:lv2 重排为 [b2, b1](b2 在 b1 上方),消除交叉
- expect(pos['point:b2'].y).toBeLessThan(pos['point:b1'].y);
- // 每列仍围绕中心对称
- expect((pos['point:a1'].y + pos['point:a2'].y) / 2).toBeCloseTo(centerY, 5);
- expect((pos['point:b1'].y + pos['point:b2'].y) / 2).toBeCloseTo(centerY, 5);
- });
- test('5 层线状模型(laneTopo 等价):全部坐标合法,交叉数为 0', () => {
- const areas = [area('a1', 1), area('b1', 2), area('c1', 3), area('d1', 4), area('e1', 5)];
- const relations = buildRelationArray(areas, [rel('r1', 'a1', 'b1'), rel('r2', 'b1', 'c1'), rel('r3', 'c1', 'd1'), rel('r4', 'd1', 'e1')]);
- const data = transformToTopologyData(areas, relations);
- const order = orderNodesByLevel(data);
- expect(countTotalCrossings(data, order)).toBe(0);
- const pos = computeLayout(data, 1200, 700).positions;
- for (const a of areas) {
- const p = pos[`point:${a.id}`];
- expect(Number.isFinite(p.x)).toBe(true);
- expect(Number.isFinite(p.y)).toBe(true);
- expect(p.y).toBeGreaterThanOrEqual(0);
- expect(p.y).toBeLessThan(700);
- }
- });
- test('排序不改变列序与总进/总回地面节点位置', () => {
- const areas = [area('a1', 1), area('b1', 2), area('c1', 3), area('d1', 4), area('e1', 5)];
- const relations = buildRelationArray(areas, [rel('r1', 'a1', 'b1'), rel('r2', 'b1', 'c1'), rel('r3', 'c1', 'd1'), rel('r4', 'd1', 'e1')]);
- const data = transformToTopologyData(areas, relations);
- const pos = computeLayout(data, 1200, 700).positions;
- expect(pos[ROOT_IN_ID].x).toBe(laneX(0));
- expect(pos['point:a1'].x).toBe(laneX(2)); // 总进(0) lv0(1) lv1(2)
- expect(pos['point:e1'].x).toBe(laneX(6)); // lv5 在 lv0..lv6 列序中为第 6 列
- expect(pos[ROOT_OUT_ID].x).toBe(laneX(9)); // 未分级列之后为总回
- expect(pos[ROOT_IN_ID].y).toBe(centerY);
- expect(pos[ROOT_OUT_ID].y).toBe(centerY);
- });
- test('pairEdgesOf 复算与 countTotalCrossings 一致(防实现漂移)', () => {
- const areas = [area('a1', 1), area('a2', 1), area('b1', 2), area('b2', 2), area('c1', 3)];
- const relations = buildRelationArray(areas, [
- rel('r1', 'a1', 'b2'),
- rel('r2', 'a2', 'b1'),
- rel('r3', 'b2', 'c1'),
- rel('r4', 'a1', 'c1'), // 跨层边:不参与
- ]);
- const data = transformToTopologyData(areas, relations);
- const order = orderNodesByLevel(data);
- // 独立复算相邻层边,再统计交叉数,应与 countTotalCrossings 一致
- const edges = pairEdgesOf(data);
- const rankOf = (key: string) => {
- const r = new Map<string, number>();
- (order.get(key) || []).forEach((id, i) => r.set(id, i));
- return r;
- };
- let manual = 0;
- for (let i = 0; i <= 5; i++) {
- const es = edges.get(`${i}-${i + 1}`);
- if (!es) continue;
- manual += countCrossings(rankOf(String(i)), rankOf(String(i + 1)), es);
- }
- expect(manual).toBe(countTotalCrossings(data, order));
- });
- });
|