| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- 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 { 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,
- });
- /** 构造一个完整巷道模型拓扑(5 级巷道、4 条巷道关系) */
- function laneTopo(): TopologyData {
- const areas = [area('a1', 1), area('b1', 2, 90), 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')]);
- return transformToTopologyData(areas, relations);
- }
- describe('buildRelationArray 关系数组生成', () => {
- test('过滤 parent/child 引用缺失的脏数据,保留关系主键 id', () => {
- const areas = [area('1', 1), area('2', 2)];
- const relations = [rel('r1', '1', '2'), rel('r2', '9', '2'), rel('r3', '1', '8'), rel('r4', '', '2')];
- const arr = buildRelationArray(areas, relations);
- expect(arr).toHaveLength(1);
- expect(arr[0].id).toBe('r1');
- expect(arr[0].parent.id).toBe('1');
- expect(arr[0].child.id).toBe('2');
- });
- });
- describe('transformToTopologyData 巷道线模型', () => {
- test('生成总进/总回地面节点;全部点位入节点;关系连线带 relationId/pointId;根边无 relationId;无布点节点;流向正确', () => {
- const areas = [area('a1', 1), area('b1', 2, 90), area('c1', 3), area('d1', 4), area('e1', 5), area('u1', 3, 10)];
- const relations = [rel('r1', 'a1', 'b1'), rel('r2', 'b1', 'c1'), rel('r3', 'c1', 'd1'), rel('r4', 'd1', 'e1')];
- const data = transformToTopologyData(areas, buildRelationArray(areas, relations));
- // 总进/总回地面节点
- expect(data.nodes.some((n) => n.id === ROOT_IN_ID && n.name === '地面')).toBe(true);
- expect(data.nodes.some((n) => n.id === ROOT_OUT_ID && n.name === '地面')).toBe(true);
- expect(data.nodes.filter((n) => n.name === '地面')).toHaveLength(2);
- // 全部点位入节点(含无关系点位 u1)
- for (const a of areas) {
- expect(data.nodes.some((n) => n.id === `point:${a.id}`)).toBe(true);
- }
- // 关系连线带 relationId + pointId(子节点)
- const l = data.links.find((x) => x.relationId === 'r1');
- expect(l?.source).toBe('point:a1');
- expect(l?.target).toBe('point:b1');
- expect(l?.pointId).toBe('point:b1');
- // 主要数据点位:进风/用风取子节点、回风取父节点
- expect(data.links.find((x) => x.relationId === 'r1')?.mainPointId).toBe('point:b1'); // a1→b1 进风,子节点
- expect(data.links.find((x) => x.relationId === 'r2')?.mainPointId).toBe('point:c1'); // b1→c1 用风,子节点
- expect(data.links.find((x) => x.relationId === 'r3')?.mainPointId).toBe('point:c1'); // c1→d1 回风,父节点
- expect(data.links.find((x) => x.relationId === 'r4')?.mainPointId).toBe('point:d1'); // d1→e1 回风,父节点
- // 根边 地面→lv1、lv5→地面 均无 relationId,主要数据分别为 lv1 子节点 / lv5 父节点
- const rootIn = data.links.find((x) => x.source === ROOT_IN_ID && x.target === 'point:a1');
- expect(rootIn?.relationId).toBeUndefined();
- expect(rootIn?.mainPointId).toBe('point:a1');
- const rootOut = data.links.find((x) => x.source === 'point:e1' && x.target === ROOT_OUT_ID);
- expect(rootOut?.relationId).toBeUndefined();
- expect(rootOut?.mainPointId).toBe('point:e1');
- // 无测风装置节点
- expect(data.nodes.some((n) => n.category === 2)).toBe(false);
- // 流向:子节点 level ≤3 进风、>3 回风
- expect(data.links.find((x) => x.relationId === 'r1')?.flow).toBe('intake');
- expect(data.links.find((x) => x.relationId === 'r3')?.flow).toBe('return');
- });
- test('judgeAreaList 字段有内容 → suspected=true;无内容 → false(兼容 alarmList)', () => {
- const areas = [
- area('a1', 1, 100, { judgeAreaList: [{ id: 'x' }] }),
- area('b1', 2, 90, { judgeAreaList: null }),
- area('c1', 3, 80, { judgeAreaList: [] }),
- area('d1', 4, 70, { alarmList: ['y'] }), // 历史字段兼容
- ];
- const data = transformToTopologyData(areas, []);
- expect(data.nodes.find((n) => n.id === 'point:a1')?.suspected).toBe(true);
- expect(data.nodes.find((n) => n.id === 'point:b1')?.suspected).toBe(false);
- expect(data.nodes.find((n) => n.id === 'point:c1')?.suspected).toBe(false);
- expect(data.nodes.find((n) => n.id === 'point:d1')?.suspected).toBe(true);
- });
- test('topologyX/topologyY 透传到节点(数字与字符串输入)', () => {
- const areas = [area('a1', 1, 100, { topologyX: 123.5, topologyY: 456 }), area('b1', 2, 90, { topologyX: '200', topologyY: '300' })];
- const data = transformToTopologyData(areas, []);
- expect(data.nodes.find((n) => n.id === 'point:a1')?.topologyX).toBe(123.5);
- expect(data.nodes.find((n) => n.id === 'point:a1')?.topologyY).toBe(456);
- expect(data.nodes.find((n) => n.id === 'point:b1')?.topologyX).toBe('200');
- expect(data.nodes.find((n) => n.id === 'point:b1')?.topologyY).toBe('300');
- // 地面节点无该字段
- expect(data.nodes.find((n) => n.id === ROOT_IN_ID)?.topologyX).toBeUndefined();
- });
- test('动态根边:有 lv0/lv6 时 总进→lv0、lv6→总回;缺失时回退 lv1/lv5', () => {
- // 场景一:存在 lv0(进风井) 与 lv6(回风井) → 根边连 lv0/lv6,不再连 lv1/lv5
- const areas1 = [area('z0', 0), area('a1', 1), area('e1', 5), area('z6', 6)];
- const data1 = transformToTopologyData(areas1, []);
- expect(data1.links.some((x) => x.source === ROOT_IN_ID && x.target === 'point:z0')).toBe(true);
- expect(data1.links.some((x) => x.source === ROOT_IN_ID && x.target === 'point:a1')).toBe(false);
- expect(data1.links.some((x) => x.source === 'point:z6' && x.target === ROOT_OUT_ID)).toBe(true);
- expect(data1.links.some((x) => x.source === 'point:e1' && x.target === ROOT_OUT_ID)).toBe(false);
- // 场景二:无 lv0/lv6 → 回退 总进→lv1、lv5→总回
- const areas2 = [area('a1', 1), area('e1', 5)];
- const data2 = transformToTopologyData(areas2, []);
- expect(data2.links.some((x) => x.source === ROOT_IN_ID && x.target === 'point:a1')).toBe(true);
- expect(data2.links.some((x) => x.source === 'point:e1' && x.target === ROOT_OUT_ID)).toBe(true);
- });
- });
- describe('computeLayout 列式布局(固定间距)', () => {
- const centerY = 350; // 700 / 2
- const laneX = (idx: number) => LAYOUT.margin + LAYOUT.colGap * idx;
- test('列序:总进 → lv0..lv6 → 总回,x 依次递增固定 colGap;单节点列垂直居中', () => {
- const layout = computeLayout(laneTopo(), 1200, 700);
- const pos = layout.positions;
- expect(pos[ROOT_IN_ID].x).toBe(laneX(0));
- // 列序:总进(0) → lv0(1) → lv1(2) → lv2(3) → lv3(4) → lv4(5) → lv5(6) → lv6(7) → 未分级(8) → 总回(9)
- expect(pos['point:a1'].x).toBe(laneX(2));
- expect(pos['point:b1'].x).toBe(laneX(3));
- expect(pos['point:c1'].x).toBe(laneX(4));
- expect(pos['point:d1'].x).toBe(laneX(5));
- expect(pos['point:e1'].x).toBe(laneX(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('新增层级 lv0(进风井)/lv6(回风井) 列序:位于 lv1 之前、lv5 之后', () => {
- const areas = [area('a1', 1), area('z0', 0), area('e1', 5), area('z6', 6)];
- const data = transformToTopologyData(areas, []);
- const pos = computeLayout(data, 1200, 700).positions;
- expect(pos['point:z0'].x).toBe(laneX(1)); // lv0 在 lv1 前
- expect(pos['point:a1'].x).toBe(laneX(2));
- expect(pos['point:e1'].x).toBe(laneX(6));
- expect(pos['point:z6'].x).toBe(laneX(7)); // lv6 在 lv5 后
- expect(pos[ROOT_OUT_ID].x).toBe(laneX(9));
- });
- test('同层级同列,行距 = 该层级 rowGap(固定),围绕中心对称', () => {
- const data = laneTopo();
- data.nodes.push({ id: 'point:b2', name: 'b2', category: 1, level: 2, airVolume: 85 });
- const pos = computeLayout(data, 1200, 700).positions;
- expect(pos['point:b2'].x).toBe(pos['point:b1'].x);
- const expected = LAYOUT.rowGap[2] ?? LAYOUT.rowGapDefault;
- expect(Math.abs(pos['point:b2'].y - pos['point:b1'].y)).toBe(expected);
- expect((pos['point:b1'].y + pos['point:b2'].y) / 2).toBeCloseTo(centerY, 5);
- });
- test('colGap 固定生效', () => {
- const orig = LAYOUT.colGap;
- try {
- LAYOUT.colGap = 250;
- const pos = computeLayout(laneTopo(), 1200, 700).positions;
- // 相邻层级列间距 = colGap(a1 为 lv1,与总进之间隔 lv0 空列)
- expect(pos['point:b1'].x - pos['point:a1'].x).toBe(250);
- expect(pos['point:c1'].x - pos['point:b1'].x).toBe(250);
- } finally {
- LAYOUT.colGap = orig;
- }
- });
- test('rowGap 按 level 分别配置生效:只影响对应层级列', () => {
- const orig2 = LAYOUT.rowGap[2];
- try {
- LAYOUT.rowGap[2] = 300;
- const data = laneTopo();
- data.nodes.push({ id: 'point:b2', name: 'b2', category: 1, level: 2, airVolume: 85 });
- data.nodes.push({ id: 'point:c2', name: 'c2', category: 1, level: 3, airVolume: 75 });
- const pos = computeLayout(data, 1200, 700).positions;
- // lv2 列使用本次配置值 300
- expect(Math.abs(pos['point:b2'].y - pos['point:b1'].y)).toBe(300);
- // lv3 列仍使用其自身配置值(未被本次修改影响)
- const gap3 = LAYOUT.rowGap[3] ?? LAYOUT.rowGapDefault;
- expect(Math.abs(pos['point:c2'].y - pos['point:c1'].y)).toBe(gap3);
- // 两列分别围绕 centerY 对称
- expect((pos['point:b1'].y + pos['point:b2'].y) / 2).toBeCloseTo(centerY, 5);
- expect((pos['point:c1'].y + pos['point:c2'].y) / 2).toBeCloseTo(centerY, 5);
- } finally {
- LAYOUT.rowGap[2] = orig2;
- }
- });
- test('多条进风-用风关系数据:全部点位有合法坐标、无 NaN、不越界', () => {
- const areas = [area('a1', 1, 500), area('a2', 1, 400), area('c1', 3, 300), area('c2', 3, 200), area('e1', 5, 100), area('e2', 5, 90)];
- const relations = buildRelationArray(areas, [rel('r1', 'a1', 'c1'), rel('r2', 'a2', 'c2'), rel('r3', 'c1', 'e1'), rel('r4', 'c2', 'e2')]);
- const data = transformToTopologyData(areas, relations);
- const layout = computeLayout(data, 1200, 700);
- for (const a of areas) {
- const pos = layout.positions[`point:${a.id}`];
- expect(pos).toBeDefined();
- expect(Number.isFinite(pos.x)).toBe(true);
- expect(Number.isFinite(pos.y)).toBe(true);
- expect(pos.y).toBeGreaterThanOrEqual(0);
- expect(pos.y).toBeLessThan(700);
- }
- // 两个进风点同列(lv1)、两个用风点同列(lv3)、两个回风点同列(lv5)
- expect(layout.positions['point:a1'].x).toBe(layout.positions['point:a2'].x);
- expect(layout.positions['point:c1'].x).toBe(layout.positions['point:c2'].x);
- expect(layout.positions['point:e1'].x).toBe(layout.positions['point:e2'].x);
- // 各 2 节点列按各自层级行距围绕 centerY 对称(首节点 = centerY − 行距/2)
- const gap1 = LAYOUT.rowGap[1] ?? LAYOUT.rowGapDefault;
- const gap3 = LAYOUT.rowGap[3] ?? LAYOUT.rowGapDefault;
- const gap5 = LAYOUT.rowGap[5] ?? LAYOUT.rowGapDefault;
- expect(layout.positions['point:a1'].y).toBeCloseTo(centerY - gap1 / 2, 5);
- expect(layout.positions['point:c1'].y).toBeCloseTo(centerY - gap3 / 2, 5);
- expect(layout.positions['point:e1'].y).toBeCloseTo(centerY - gap5 / 2, 5);
- expect((layout.positions['point:a1'].y + layout.positions['point:a2'].y) / 2).toBeCloseTo(centerY, 5);
- });
- test('不同节点数列独立垂直居中:1/2/4 节点列各自围绕 centerY 对称,单节点列在 centerY', () => {
- // 列分布:总进(1) lv1(1) lv2(2) lv3(4) lv4(2) lv5(1) 总回(1)
- const areas = [
- area('a1', 1, 500),
- area('b1', 2, 300),
- area('b2', 2, 200),
- area('c1', 3, 400),
- area('c2', 3, 300),
- area('c3', 3, 200),
- area('c4', 3, 100),
- area('d1', 4, 250),
- area('d2', 4, 150),
- area('e1', 5, 100),
- ];
- const data = transformToTopologyData(areas, []);
- const layout = computeLayout(data, 1200, 700);
- const pos = layout.positions;
- // 单节点列(总进、lv1、lv5、总回)垂直居中
- expect(pos[ROOT_IN_ID].y).toBe(centerY);
- expect(pos['point:a1'].y).toBe(centerY);
- expect(pos['point:e1'].y).toBe(centerY);
- expect(pos[ROOT_OUT_ID].y).toBe(centerY);
- // 2 节点列(lv2/lv4)围绕 centerY 对称,中点 = centerY,行距取各自层级配置
- const gap2 = LAYOUT.rowGap[2] ?? LAYOUT.rowGapDefault;
- const gap3 = LAYOUT.rowGap[3] ?? LAYOUT.rowGapDefault;
- const gap4 = LAYOUT.rowGap[4] ?? LAYOUT.rowGapDefault;
- expect((pos['point:b1'].y + pos['point:b2'].y) / 2).toBeCloseTo(centerY, 5);
- expect((pos['point:d1'].y + pos['point:d2'].y) / 2).toBeCloseTo(centerY, 5);
- expect(Math.abs(pos['point:b1'].y - pos['point:b2'].y)).toBe(gap2);
- expect(Math.abs(pos['point:d1'].y - pos['point:d2'].y)).toBe(gap4);
- // 4 节点列(lv3)围绕 centerY 对称,中点 = centerY,跨度 = 3×该层级行距
- expect((pos['point:c1'].y + pos['point:c4'].y) / 2).toBeCloseTo(centerY, 5);
- expect(Math.abs(pos['point:c1'].y - pos['point:c4'].y)).toBe(3 * gap3);
- // 中间列跨度大于两端列(菱形轮廓)
- expect(3 * gap3).toBeGreaterThan(gap2);
- expect(gap2).toBeGreaterThan(0);
- });
- test('空数据(仅总进/总回地面节点)可布局且不抛错', () => {
- const layout = computeLayout(
- {
- nodes: [
- { id: ROOT_IN_ID, name: '地面', category: 0 },
- { id: ROOT_OUT_ID, name: '地面', category: 0 },
- ],
- links: [],
- },
- 1200,
- 700
- );
- expect(layout.positions[ROOT_IN_ID]).toBeDefined();
- expect(layout.positions[ROOT_OUT_ID]).toBeDefined();
- expect(layout.positions[ROOT_IN_ID].y).toBe(centerY);
- expect(layout.positions[ROOT_OUT_ID].y).toBe(centerY);
- });
- test('布局保存坐标覆盖:有效坐标直接使用;null/空串/0/NaN 等无效坐标回退列布局(不挤到原点)', () => {
- const areas = [
- area('a1', 1, 100, { topologyX: 888, topologyY: 666 }),
- area('b1', 2, 90, { topologyX: '999.5', topologyY: '333' }), // 字符串数字同样生效
- area('c1', 3, 80, { topologyX: 'abc', topologyY: NaN }), // 非数字 → 回退
- area('d1', 4, 70), // 无坐标 → 回退
- area('e1', 5, 60, { topologyX: 0, topologyY: null }), // 后端默认 0/null → 回退
- area('f1', 0, 50, { topologyX: '', topologyY: '0' }), // 空串/'0' → 回退(lv0 进风井列)
- ];
- const data = transformToTopologyData(areas, []);
- const pos = computeLayout(data, 1200, 700).positions;
- // 有效保存坐标直接使用
- expect(pos['point:a1']).toEqual({ x: 888, y: 666 });
- expect(pos['point:b1']).toEqual({ x: 999.5, y: 333 });
- // c1 非法坐标回退列布局(lv3 列,垂直居中),而非覆盖到 (0,0)
- expect(pos['point:c1'].x).toBe(laneX(4)); // 总进(0) lv0(1) lv1(2) lv2(3) lv3(4)
- expect(pos['point:c1'].y).toBe(centerY);
- // 无坐标节点正常列布局
- expect(pos['point:d1'].x).toBe(laneX(5)); // lv4
- // 后端默认 0/null/''/0 字符串:回退列布局,绝不挤到原点
- expect(pos['point:e1'].x).toBe(laneX(6)); // lv5
- expect(pos['point:e1'].y).toBe(centerY);
- expect(pos['point:f1'].x).toBe(laneX(1)); // lv0 进风井列
- expect(pos['point:f1'].y).toBe(centerY);
- expect(pos['point:e1'].x).not.toBe(0);
- expect(pos['point:f1'].y).not.toBe(0);
- });
- });
|