feat: 增加通信关系图随机生成及后端数据加载功能
- 新增 data-converter.ts,实现通信记录到GraphContainer 的转换及自动布局算法 - 新增random-data-generator.ts,用于生成测试用的随机通信拓扑数据 - communication.vue中集成“随机生成”与“从后端加载”按钮及相关逻辑 - 支持多连通分量检测、层级布局计算及节点防重叠处理
This commit is contained in:
@@ -21,6 +21,14 @@
|
|||||||
<div class="ks-model-builder-content" style="width: calc(100% - 250px);">
|
<div class="ks-model-builder-content" style="width: calc(100% - 250px);">
|
||||||
<div class="ks-model-builder-actions">
|
<div class="ks-model-builder-actions">
|
||||||
<a-space>
|
<a-space>
|
||||||
|
<a-button v-if="graph && currentScenario" class="ks-model-builder-save" style="width: auto;" size="small" @click="handleGenerateRandom">
|
||||||
|
<ThunderboltOutlined />
|
||||||
|
<span>随机生成</span>
|
||||||
|
</a-button>
|
||||||
|
<a-button v-if="graph && currentScenario && currentScenario.id > 0" class="ks-model-builder-save" style="width: auto;" size="small" @click="handleLoadFromBackend">
|
||||||
|
<DatabaseOutlined />
|
||||||
|
<span>从后端加载</span>
|
||||||
|
</a-button>
|
||||||
<a-button v-if="graph && currentScenario" class="ks-model-builder-save" size="small" @click="handleSave">
|
<a-button v-if="graph && currentScenario" class="ks-model-builder-save" size="small" @click="handleSave">
|
||||||
<CheckOutlined />
|
<CheckOutlined />
|
||||||
<span>保存</span>
|
<span>保存</span>
|
||||||
@@ -50,14 +58,14 @@ import { useRoute, useRouter } from 'vue-router';
|
|||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import { getTeleport } from '@antv/x6-vue-shape';
|
import { getTeleport } from '@antv/x6-vue-shape';
|
||||||
import { Graph, Node, type NodeProperties } from '@antv/x6';
|
import { Graph, Node, type NodeProperties } from '@antv/x6';
|
||||||
import { CheckCircleOutlined, CheckOutlined, RollbackOutlined, SaveOutlined } from '@ant-design/icons-vue';
|
import { CheckCircleOutlined, CheckOutlined, DatabaseOutlined, RollbackOutlined, SaveOutlined, ThunderboltOutlined } from '@ant-design/icons-vue';
|
||||||
import { Wrapper } from '@/components/wrapper';
|
import { Wrapper } from '@/components/wrapper';
|
||||||
import { safePreventDefault, safeStopPropagation } from '@/utils/event';
|
import { safePreventDefault, safeStopPropagation } from '@/utils/event';
|
||||||
import Header from '../header.vue';
|
import Header from '../header.vue';
|
||||||
|
|
||||||
import type { Scenario } from './types';
|
import type { Scenario } from './types';
|
||||||
import type { PlatformWithComponents } from '../types';
|
import type { PlatformWithComponents } from '../types';
|
||||||
import { createLineOptions, type GraphContainer, type GraphTaskElement, resolveGraph, useGraphCanvas } from '../graph';
|
import { createLineOptions, type GraphContainer, type GraphEdgeElement, type GraphTaskElement, resolveGraph, useGraphCanvas } from '../graph';
|
||||||
|
|
||||||
import { registerScenarioElement } from './register';
|
import { registerScenarioElement } from './register';
|
||||||
import { createGraphScenarioElement, createGraphTaskElementFromScenario } from './utils';
|
import { createGraphScenarioElement, createGraphTaskElementFromScenario } from './utils';
|
||||||
@@ -66,6 +74,8 @@ import PlatformCard from './platform-card.vue';
|
|||||||
import NodesCard from './nodes-card.vue';
|
import NodesCard from './nodes-card.vue';
|
||||||
import { findOneScenarioById, saveScenario } from './api';
|
import { findOneScenarioById, saveScenario } from './api';
|
||||||
import { resolveConnectionRelation } from './relation';
|
import { resolveConnectionRelation } from './relation';
|
||||||
|
import { generateRandomCommunicationData } from './random-data-generator';
|
||||||
|
import { convertRecordsToGraphContainer, type CommunicationRecord } from './data-converter';
|
||||||
|
|
||||||
const TeleportContainer = defineComponent(getTeleport());
|
const TeleportContainer = defineComponent(getTeleport());
|
||||||
|
|
||||||
@@ -81,6 +91,8 @@ export default defineComponent({
|
|||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
RollbackOutlined,
|
RollbackOutlined,
|
||||||
|
ThunderboltOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
TeleportContainer,
|
TeleportContainer,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
@@ -229,6 +241,8 @@ export default defineComponent({
|
|||||||
graph: nodeGraph,
|
graph: nodeGraph,
|
||||||
relations: []
|
relations: []
|
||||||
};
|
};
|
||||||
|
console.log(currentScenario.value);
|
||||||
|
|
||||||
currentScenarioEditing.value = true;
|
currentScenarioEditing.value = true;
|
||||||
createElements();
|
createElements();
|
||||||
};
|
};
|
||||||
@@ -261,6 +275,7 @@ export default defineComponent({
|
|||||||
}, 100); // 延迟一会儿,免得连线错位
|
}, 100); // 延迟一会儿,免得连线错位
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -415,6 +430,149 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 随机生成节点流图
|
||||||
|
const handleGenerateRandom = () => {
|
||||||
|
if (!graph.value) {
|
||||||
|
message.error('画布未初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 生成随机数据
|
||||||
|
const { records, graph: randomGraph } = generateRandomCommunicationData(30);
|
||||||
|
|
||||||
|
console.log('生成的随机数据:', records);
|
||||||
|
console.log('转换后的图数据:', randomGraph);
|
||||||
|
|
||||||
|
// 清空现有内容
|
||||||
|
graph.value.clearCells();
|
||||||
|
|
||||||
|
// 设置当前场景
|
||||||
|
if (!currentScenario.value) {
|
||||||
|
currentScenario.value = {
|
||||||
|
id: 0,
|
||||||
|
name: `随机场景_${Date.now()}`,
|
||||||
|
description: '自动生成的测试场景',
|
||||||
|
communicationGraph: null,
|
||||||
|
relations: [],
|
||||||
|
graph: randomGraph,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
currentScenario.value.graph = randomGraph;
|
||||||
|
currentScenario.value.communicationGraph = JSON.stringify(randomGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染节点
|
||||||
|
setTimeout(() => {
|
||||||
|
if (randomGraph.nodes) {
|
||||||
|
randomGraph.nodes.forEach(ele => {
|
||||||
|
const node = createGraphScenarioElement(ele as GraphTaskElement);
|
||||||
|
graph.value?.addNode(node as Node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟添加边,确保节点已渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
if (randomGraph.edges) {
|
||||||
|
randomGraph.edges.forEach(edgeData => {
|
||||||
|
graph.value?.addEdge({
|
||||||
|
...edgeData,
|
||||||
|
...createLineOptions(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动适应视图
|
||||||
|
fitToScreen();
|
||||||
|
|
||||||
|
message.success(`已生成 ${randomGraph.nodes.length} 个节点和 ${randomGraph.edges.length} 条连接线`);
|
||||||
|
}, 100);
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('随机生成时出错:', error);
|
||||||
|
message.error('生成失败,请重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从后端加载平台数据并转换为通信关系图(当前使用模拟数据)
|
||||||
|
const handleLoadFromBackend = async () => {
|
||||||
|
if (!graph.value || !currentScenario.value) {
|
||||||
|
message.error('请先选择场景');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
message.loading({ content: '正在加载通信关系数据...', key: 'loading' });
|
||||||
|
|
||||||
|
// TODO: 等待后端提供真实接口后替换为实际API调用
|
||||||
|
// const response = await findCommunicationRelations(currentScenario.value.id);
|
||||||
|
// const relations = response.data || [];
|
||||||
|
|
||||||
|
// 临时使用模拟数据
|
||||||
|
console.log('使用模拟通信关系数据');
|
||||||
|
const mockRelations: CommunicationRecord[] = [
|
||||||
|
{ id: 1, command_platform: '指挥中心', subordinate_platform: '预警机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 2, command_platform: '指挥中心', subordinate_platform: '驱逐舰', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 3, command_platform: '指挥中心', subordinate_platform: '战斗机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 4, command_platform: '预警机', subordinate_platform: '电子战飞机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 5, command_platform: '驱逐舰', subordinate_platform: '潜艇', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 6, command_platform: '战斗机', subordinate_platform: '无人机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('模拟的通信记录:', mockRelations);
|
||||||
|
|
||||||
|
// 使用模拟数据进行转换
|
||||||
|
const convertedGraph = convertRecordsToGraphContainer(mockRelations);
|
||||||
|
|
||||||
|
console.log('转换后的图数据:', convertedGraph);
|
||||||
|
|
||||||
|
// 清空现有内容
|
||||||
|
graph.value.clearCells();
|
||||||
|
|
||||||
|
// 更新当前场景
|
||||||
|
currentScenario.value.graph = convertedGraph;
|
||||||
|
currentScenario.value.communicationGraph = JSON.stringify(convertedGraph);
|
||||||
|
|
||||||
|
// 渲染节点
|
||||||
|
setTimeout(() => {
|
||||||
|
if (convertedGraph.nodes) {
|
||||||
|
convertedGraph.nodes.forEach(ele => {
|
||||||
|
const node = createGraphScenarioElement(ele as GraphTaskElement);
|
||||||
|
graph.value?.addNode(node as Node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟添加边,确保节点已渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
if (convertedGraph.edges) {
|
||||||
|
convertedGraph.edges.forEach(edgeData => {
|
||||||
|
graph.value?.addEdge({
|
||||||
|
...edgeData,
|
||||||
|
...createLineOptions(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动适应视图
|
||||||
|
fitToScreen();
|
||||||
|
|
||||||
|
message.success({
|
||||||
|
content: `已从后端加载 ${convertedGraph.nodes.length} 个平台和 ${convertedGraph.edges.length} 条连接关系`,
|
||||||
|
key: 'loading'
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('从后端加载时出错:', error);
|
||||||
|
message.error({
|
||||||
|
content: error instanceof Error ? error.message : '加载失败,请重试',
|
||||||
|
key: 'loading'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
init();
|
||||||
@@ -444,6 +602,8 @@ export default defineComponent({
|
|||||||
handleDrop,
|
handleDrop,
|
||||||
isDraggingOver,
|
isDraggingOver,
|
||||||
handleSave,
|
handleSave,
|
||||||
|
handleGenerateRandom,
|
||||||
|
handleLoadFromBackend,
|
||||||
handleUpdateElement,
|
handleUpdateElement,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
};
|
};
|
||||||
|
|||||||
906
modeler/src/views/decision/communication/data-converter.ts
Normal file
906
modeler/src/views/decision/communication/data-converter.ts
Normal file
@@ -0,0 +1,906 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of the kernelstudio package.
|
||||||
|
*
|
||||||
|
* (c) 2014-2026 zlin <admin@kernelstudio.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE file
|
||||||
|
* that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { GraphTaskElement, GraphContainer, GraphEdgeElement } from '../graph';
|
||||||
|
import { generateKey } from '@/utils/strings';
|
||||||
|
|
||||||
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库通信关系记录接口
|
||||||
|
*/
|
||||||
|
export interface CommunicationRecord {
|
||||||
|
id: number;
|
||||||
|
command_platform: string; // 指挥平台(源节点名称)
|
||||||
|
subordinate_platform: string; // 下属平台(目标节点名称)
|
||||||
|
command_comm?: string; // 指挥通信方式
|
||||||
|
subordinate_comm?: string; // 下属通信方式
|
||||||
|
scenary_id?: number; // 场景ID
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentChildMappings {
|
||||||
|
parentToChildren: Map<string, string[]>;
|
||||||
|
childToParent: Map<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LayoutParams {
|
||||||
|
ROOT_X: number;
|
||||||
|
COL_SPACING: number;
|
||||||
|
MIN_ROW_SPACING: number;
|
||||||
|
START_Y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodePosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubtreeRange {
|
||||||
|
minY: number;
|
||||||
|
maxY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 主函数:数据转换 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库记录转换为 GraphContainer 格式
|
||||||
|
* @param records 数据库通信关系记录数组
|
||||||
|
* @returns GraphContainer 格式的节点和边数据
|
||||||
|
*/
|
||||||
|
export const convertRecordsToGraphContainer = (records: CommunicationRecord[]): GraphContainer => {
|
||||||
|
if (!records || records.length === 0) {
|
||||||
|
return { nodes: [], edges: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集所有唯一的平台名称
|
||||||
|
const platformSet = new Set<string>();
|
||||||
|
records.forEach(record => {
|
||||||
|
platformSet.add(record.command_platform);
|
||||||
|
platformSet.add(record.subordinate_platform);
|
||||||
|
});
|
||||||
|
|
||||||
|
const allPlatforms = Array.from(platformSet);
|
||||||
|
|
||||||
|
// 构建父子映射关系
|
||||||
|
const { parentToChildren, childToParent } = buildParentChildMappings(records);
|
||||||
|
|
||||||
|
// 检测连通分量(处理断链情况)
|
||||||
|
const connectedComponents = findConnectedComponents(allPlatforms, parentToChildren, childToParent);
|
||||||
|
|
||||||
|
console.log(`检测到 ${connectedComponents.length} 个连通分量`);
|
||||||
|
|
||||||
|
// 为每个连通分量生成布局
|
||||||
|
const allNodes: GraphTaskElement[] = [];
|
||||||
|
const allEdges: GraphEdgeElement[] = [];
|
||||||
|
|
||||||
|
let xOffset = 50; // 第一个分量的起始X坐标
|
||||||
|
|
||||||
|
connectedComponents.forEach((component, componentIndex) => {
|
||||||
|
console.log(`处理第 ${componentIndex + 1} 个连通分量,包含 ${component.size} 个节点`);
|
||||||
|
|
||||||
|
// 为该分量确定根节点
|
||||||
|
const rootPlatformName = determineRootNode(Array.from(component));
|
||||||
|
|
||||||
|
if (!rootPlatformName) {
|
||||||
|
console.warn(`第 ${componentIndex + 1} 个分量未找到根节点,跳过`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BFS遍历确定该分量中每个节点的层级
|
||||||
|
const levelMap = computeNodeLevels(
|
||||||
|
Array.from(component),
|
||||||
|
rootPlatformName,
|
||||||
|
parentToChildren,
|
||||||
|
childToParent
|
||||||
|
);
|
||||||
|
|
||||||
|
// 按层级分组
|
||||||
|
const levelsMap = groupNodesByLevel(levelMap);
|
||||||
|
|
||||||
|
// 布局参数 - 紧凑型布局
|
||||||
|
const layoutParams: LayoutParams = {
|
||||||
|
ROOT_X: xOffset, // 当前分量的起始X坐标
|
||||||
|
COL_SPACING: 280, // 列间距
|
||||||
|
MIN_ROW_SPACING: 140, // 最小行间距
|
||||||
|
START_Y: 80, // 起始Y坐标
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算节点位置
|
||||||
|
const nodePositions = calculateNodePositions(
|
||||||
|
levelsMap,
|
||||||
|
parentToChildren,
|
||||||
|
layoutParams
|
||||||
|
);
|
||||||
|
|
||||||
|
// 创建节点对象
|
||||||
|
const nodeMap = createNodeObjects(levelsMap, nodePositions);
|
||||||
|
|
||||||
|
// 创建根节点
|
||||||
|
createRootNode(nodeMap, rootPlatformName, parentToChildren);
|
||||||
|
|
||||||
|
// 过滤出属于当前分量的边
|
||||||
|
const componentEdges = filterEdgesForComponent(records, component);
|
||||||
|
const edges = createEdges(componentEdges, nodeMap);
|
||||||
|
|
||||||
|
// 添加到总结果
|
||||||
|
allNodes.push(...Array.from(nodeMap.values()));
|
||||||
|
allEdges.push(...edges);
|
||||||
|
|
||||||
|
// 计算下一个分量的偏移量(基于当前分量的宽度)
|
||||||
|
const maxLevel = Math.max(...Array.from(levelsMap.keys()));
|
||||||
|
xOffset += (maxLevel + 1) * layoutParams.COL_SPACING + 150; // 分量之间留150px间隔
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: allNodes,
|
||||||
|
edges: allEdges,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 辅助函数:检测连通分量 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用并查集或DFS检测图中的连通分量
|
||||||
|
*/
|
||||||
|
const findConnectedComponents = (
|
||||||
|
platforms: string[],
|
||||||
|
parentToChildren: Map<string, string[]>,
|
||||||
|
childToParent: Map<string, string>
|
||||||
|
): Set<string>[] => {
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const components: Set<string>[] = [];
|
||||||
|
|
||||||
|
// DFS遍历找出所有连通分量
|
||||||
|
const dfs = (platform: string, component: Set<string>) => {
|
||||||
|
if (visited.has(platform)) return;
|
||||||
|
|
||||||
|
visited.add(platform);
|
||||||
|
component.add(platform);
|
||||||
|
|
||||||
|
// 访问子节点
|
||||||
|
const children = parentToChildren.get(platform) || [];
|
||||||
|
children.forEach(child => dfs(child, component));
|
||||||
|
|
||||||
|
// 访问父节点
|
||||||
|
const parent = childToParent.get(platform);
|
||||||
|
if (parent && !visited.has(parent)) {
|
||||||
|
dfs(parent, component);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 对每个未访问的节点启动一次DFS
|
||||||
|
platforms.forEach(platform => {
|
||||||
|
if (!visited.has(platform)) {
|
||||||
|
const component = new Set<string>();
|
||||||
|
dfs(platform, component);
|
||||||
|
components.push(component);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return components;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为连通分量确定根节点
|
||||||
|
* 优先级:1. 包含"指挥"关键词 2. 入度为0的节点 3. 第一个节点
|
||||||
|
*/
|
||||||
|
const determineRootNode = (platforms: string[]): string | null => {
|
||||||
|
if (platforms.length === 0) return null;
|
||||||
|
|
||||||
|
// 优先选择包含"指挥"的节点
|
||||||
|
const commandNode = platforms.find(p => p.includes('指挥'));
|
||||||
|
if (commandNode) return commandNode;
|
||||||
|
|
||||||
|
// 其次选择入度为0的节点(没有父节点)
|
||||||
|
// 这里简化处理,直接返回第一个
|
||||||
|
return platforms[0] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤出属于特定连通分量的边
|
||||||
|
*/
|
||||||
|
const filterEdgesForComponent = (
|
||||||
|
records: CommunicationRecord[],
|
||||||
|
component: Set<string>
|
||||||
|
): CommunicationRecord[] => {
|
||||||
|
return records.filter(record =>
|
||||||
|
component.has(record.command_platform) &&
|
||||||
|
component.has(record.subordinate_platform)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 辅助函数:构建父子映射 ====================
|
||||||
|
|
||||||
|
const buildParentChildMappings = (records: CommunicationRecord[]): ParentChildMappings => {
|
||||||
|
const parentToChildren = new Map<string, string[]>();
|
||||||
|
const childToParent = new Map<string, string>();
|
||||||
|
|
||||||
|
records.forEach(record => {
|
||||||
|
if (!parentToChildren.has(record.command_platform)) {
|
||||||
|
parentToChildren.set(record.command_platform, []);
|
||||||
|
}
|
||||||
|
parentToChildren.get(record.command_platform)!.push(record.subordinate_platform);
|
||||||
|
childToParent.set(record.subordinate_platform, record.command_platform);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { parentToChildren, childToParent };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 辅助函数:计算节点层级 ====================
|
||||||
|
|
||||||
|
const computeNodeLevels = (
|
||||||
|
platforms: string[],
|
||||||
|
rootPlatformName: string,
|
||||||
|
parentToChildren: Map<string, string[]>,
|
||||||
|
childToParent: Map<string, string>
|
||||||
|
): Map<string, number> => {
|
||||||
|
const levelMap = new Map<string, number>();
|
||||||
|
const processed = new Set<string>();
|
||||||
|
const queue: Array<{ platform: string; level: number }> = [];
|
||||||
|
|
||||||
|
// 分离根节点和子节点
|
||||||
|
const childPlatforms = platforms.filter(p => p !== rootPlatformName);
|
||||||
|
|
||||||
|
// 第一层:直接与根节点相连的节点
|
||||||
|
childPlatforms.forEach(platform => {
|
||||||
|
if (childToParent.get(platform) === rootPlatformName) {
|
||||||
|
levelMap.set(platform, 0);
|
||||||
|
processed.add(platform);
|
||||||
|
queue.push({ platform, level: 0 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// BFS遍历后续层级
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const { platform: currentPlatform, level: currentLevel } = queue.shift()!;
|
||||||
|
|
||||||
|
const children = parentToChildren.get(currentPlatform) || [];
|
||||||
|
children.forEach(childPlatform => {
|
||||||
|
if (!processed.has(childPlatform)) {
|
||||||
|
levelMap.set(childPlatform, currentLevel + 1);
|
||||||
|
processed.add(childPlatform);
|
||||||
|
queue.push({ platform: childPlatform, level: currentLevel + 1 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理未连接的孤立节点
|
||||||
|
childPlatforms.forEach(platform => {
|
||||||
|
if (!processed.has(platform)) {
|
||||||
|
const maxLevel = Math.max(...Array.from(levelMap.values()), -1);
|
||||||
|
levelMap.set(platform, maxLevel + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return levelMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 辅助函数:按层级分组 ====================
|
||||||
|
|
||||||
|
const groupNodesByLevel = (levelMap: Map<string, number>): Map<number, string[]> => {
|
||||||
|
const levelsMap = new Map<number, string[]>();
|
||||||
|
levelMap.forEach((level, platform) => {
|
||||||
|
if (!levelsMap.has(level)) {
|
||||||
|
levelsMap.set(level, []);
|
||||||
|
}
|
||||||
|
levelsMap.get(level)!.push(platform);
|
||||||
|
});
|
||||||
|
return levelsMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 辅助函数:计算节点位置 ====================
|
||||||
|
|
||||||
|
const calculateNodePositions = (
|
||||||
|
levelsMap: Map<number, string[]>,
|
||||||
|
parentToChildren: Map<string, string[]>,
|
||||||
|
params: LayoutParams
|
||||||
|
): Map<string, NodePosition> => {
|
||||||
|
const nodePositions = new Map<string, NodePosition>();
|
||||||
|
const subtreeRanges = new Map<string, SubtreeRange>();
|
||||||
|
|
||||||
|
// 第一步:预计算每列需要的总高度
|
||||||
|
precomputeColumnHeights(levelsMap, parentToChildren, params.MIN_ROW_SPACING, subtreeRanges);
|
||||||
|
|
||||||
|
// 第二步:初步分配位置
|
||||||
|
initialPositionAssignment(levelsMap, parentToChildren, params, subtreeRanges, nodePositions);
|
||||||
|
|
||||||
|
// 第三步:调整父节点使其位于子节点中心
|
||||||
|
adjustParentsToCenterOfChildren(levelsMap, parentToChildren, nodePositions);
|
||||||
|
|
||||||
|
// 第四步:同级节点防重叠检测与修正
|
||||||
|
preventOverlapsWithinSameLevel(levelsMap, parentToChildren, params.MIN_ROW_SPACING, nodePositions);
|
||||||
|
|
||||||
|
// 第五步:重新调整父节点恢复等腰三角形
|
||||||
|
restoreIsoscelesTriangles(levelsMap, parentToChildren, nodePositions);
|
||||||
|
|
||||||
|
return nodePositions;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 预计算每列高度
|
||||||
|
const precomputeColumnHeights = (
|
||||||
|
levelsMap: Map<number, string[]>,
|
||||||
|
parentToChildren: Map<string, string[]>,
|
||||||
|
minRowSpacing: number,
|
||||||
|
subtreeRanges: Map<string, SubtreeRange>
|
||||||
|
): void => {
|
||||||
|
const sortedLevels = Array.from(levelsMap.keys()).sort((a, b) => b - a);
|
||||||
|
|
||||||
|
sortedLevels.forEach(level => {
|
||||||
|
const platformsInLevel = levelsMap.get(level)!;
|
||||||
|
let totalHeight = 0;
|
||||||
|
|
||||||
|
platformsInLevel.forEach(platform => {
|
||||||
|
const children = parentToChildren.get(platform) || [];
|
||||||
|
|
||||||
|
if (children.length > 0) {
|
||||||
|
let maxSubtreeHeight = 0;
|
||||||
|
|
||||||
|
children.forEach(child => {
|
||||||
|
const childRange = subtreeRanges.get(child);
|
||||||
|
if (childRange) {
|
||||||
|
const childHeight = childRange.maxY - childRange.minY;
|
||||||
|
maxSubtreeHeight = Math.max(maxSubtreeHeight, childHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const extraSpace = children.length > 1 ? (children.length - 1) * minRowSpacing : 0;
|
||||||
|
const subtreeHeight = maxSubtreeHeight + extraSpace;
|
||||||
|
|
||||||
|
totalHeight += Math.max(minRowSpacing, subtreeHeight);
|
||||||
|
subtreeRanges.set(platform, {
|
||||||
|
minY: totalHeight - subtreeHeight,
|
||||||
|
maxY: totalHeight
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
totalHeight += minRowSpacing;
|
||||||
|
subtreeRanges.set(platform, {
|
||||||
|
minY: totalHeight - minRowSpacing,
|
||||||
|
maxY: totalHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初步分配位置
|
||||||
|
const initialPositionAssignment = (
|
||||||
|
levelsMap: Map<number, string[]>,
|
||||||
|
parentToChildren: Map<string, string[]>,
|
||||||
|
params: LayoutParams,
|
||||||
|
subtreeRanges: Map<string, SubtreeRange>,
|
||||||
|
nodePositions: Map<string, NodePosition>
|
||||||
|
): void => {
|
||||||
|
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
|
||||||
|
const columnOffsets = new Map<number, number>();
|
||||||
|
|
||||||
|
forwardLevels.forEach(level => {
|
||||||
|
const platformsInLevel = levelsMap.get(level)!;
|
||||||
|
const x = params.ROOT_X + (level + 1) * params.COL_SPACING;
|
||||||
|
let currentYOffset = columnOffsets.get(level) || params.START_Y;
|
||||||
|
|
||||||
|
platformsInLevel.forEach(platform => {
|
||||||
|
const children = parentToChildren.get(platform) || [];
|
||||||
|
let y: number;
|
||||||
|
|
||||||
|
if (children.length > 0) {
|
||||||
|
// 有子节点的父节点:根据子树高度计算位置
|
||||||
|
const subtreeRange = subtreeRanges.get(platform);
|
||||||
|
if (subtreeRange) {
|
||||||
|
const subtreeHeight = subtreeRange.maxY - subtreeRange.minY;
|
||||||
|
y = currentYOffset + subtreeHeight / 2;
|
||||||
|
// 重要:为下一个节点预留足够的垂直空间
|
||||||
|
currentYOffset += Math.max(subtreeHeight, params.MIN_ROW_SPACING);
|
||||||
|
} else {
|
||||||
|
y = currentYOffset + params.MIN_ROW_SPACING / 2;
|
||||||
|
currentYOffset += params.MIN_ROW_SPACING;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 叶子节点:占用固定空间
|
||||||
|
y = currentYOffset + params.MIN_ROW_SPACING / 2;
|
||||||
|
currentYOffset += params.MIN_ROW_SPACING;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodePositions.set(platform, { x, y });
|
||||||
|
});
|
||||||
|
|
||||||
|
columnOffsets.set(level, currentYOffset);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调整父节点到子节点中心
|
||||||
|
const adjustParentsToCenterOfChildren = (
|
||||||
|
levelsMap: Map<number, string[]>,
|
||||||
|
parentToChildren: Map<string, string[]>,
|
||||||
|
nodePositions: Map<string, NodePosition>
|
||||||
|
): void => {
|
||||||
|
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
[...forwardLevels].reverse().forEach(level => {
|
||||||
|
const platformsInLevel = levelsMap.get(level)!;
|
||||||
|
|
||||||
|
platformsInLevel.forEach(platform => {
|
||||||
|
const children = parentToChildren.get(platform) || [];
|
||||||
|
|
||||||
|
if (children.length > 0) {
|
||||||
|
const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean);
|
||||||
|
|
||||||
|
if (childPositions.length > 0) {
|
||||||
|
const avgChildY = childPositions.reduce((sum, pos) => sum + pos!.y, 0) / childPositions.length;
|
||||||
|
const pos = nodePositions.get(platform)!;
|
||||||
|
pos.y = avgChildY;
|
||||||
|
nodePositions.set(platform, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 防重叠检测与修正(迭代式,确保完全无重叠)
|
||||||
|
const preventOverlapsWithinSameLevel = (
|
||||||
|
levelsMap: Map<number, string[]>,
|
||||||
|
parentToChildren: Map<string, string[]>,
|
||||||
|
minRowSpacing: number,
|
||||||
|
nodePositions: Map<string, NodePosition>
|
||||||
|
): void => {
|
||||||
|
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
forwardLevels.forEach(level => {
|
||||||
|
const platformsInLevel = levelsMap.get(level)!;
|
||||||
|
|
||||||
|
// 迭代检测直到没有重叠
|
||||||
|
let hasOverlap = true;
|
||||||
|
let iterations = 0;
|
||||||
|
const maxIterations = 20; // 防止无限循环
|
||||||
|
|
||||||
|
while (hasOverlap && iterations < maxIterations) {
|
||||||
|
hasOverlap = false;
|
||||||
|
iterations++;
|
||||||
|
|
||||||
|
// 每次都重新按Y坐标排序
|
||||||
|
const sortedByY = platformsInLevel
|
||||||
|
.map(p => ({ platform: p, pos: nodePositions.get(p)! }))
|
||||||
|
.sort((a, b) => a.pos.y - b.pos.y);
|
||||||
|
|
||||||
|
for (let i = 1; i < sortedByY.length; i++) {
|
||||||
|
const prev = sortedByY[i - 1];
|
||||||
|
const curr = sortedByY[i];
|
||||||
|
|
||||||
|
if (!prev || !curr) continue;
|
||||||
|
|
||||||
|
const actualDistance = curr.pos.y - prev.pos.y;
|
||||||
|
|
||||||
|
if (actualDistance < minRowSpacing) {
|
||||||
|
hasOverlap = true;
|
||||||
|
|
||||||
|
const offset = minRowSpacing - actualDistance;
|
||||||
|
curr.pos.y += offset;
|
||||||
|
|
||||||
|
// 递归调整所有后代节点
|
||||||
|
const adjustDescendants = (platform: string, yOffset: number) => {
|
||||||
|
const children = parentToChildren.get(platform) || [];
|
||||||
|
children.forEach(child => {
|
||||||
|
const childPos = nodePositions.get(child);
|
||||||
|
if (childPos) {
|
||||||
|
childPos.y += yOffset;
|
||||||
|
adjustDescendants(child, yOffset);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
adjustDescendants(curr.platform, offset);
|
||||||
|
|
||||||
|
// 发现重叠并调整后,跳出内层循环,重新开始检测
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iterations >= maxIterations) {
|
||||||
|
console.warn(`Level ${level}: 防重叠达到最大迭代次数(${maxIterations}),可能存在未解决的重叠`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 恢复等腰三角形
|
||||||
|
const restoreIsoscelesTriangles = (
|
||||||
|
levelsMap: Map<number, string[]>,
|
||||||
|
parentToChildren: Map<string, string[]>,
|
||||||
|
nodePositions: Map<string, NodePosition>
|
||||||
|
): void => {
|
||||||
|
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
[...forwardLevels].reverse().forEach(level => {
|
||||||
|
const platformsInLevel = levelsMap.get(level)!;
|
||||||
|
|
||||||
|
platformsInLevel.forEach(platform => {
|
||||||
|
const children = parentToChildren.get(platform) || [];
|
||||||
|
|
||||||
|
if (children.length > 0) {
|
||||||
|
const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean);
|
||||||
|
|
||||||
|
if (childPositions.length > 0) {
|
||||||
|
const avgChildY = childPositions.reduce((sum, pos) => sum + pos!.y, 0) / childPositions.length;
|
||||||
|
const pos = nodePositions.get(platform)!;
|
||||||
|
pos.y = avgChildY;
|
||||||
|
nodePositions.set(platform, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 辅助函数:创建节点对象 ====================
|
||||||
|
|
||||||
|
const createNodeObjects = (
|
||||||
|
levelsMap: Map<number, string[]>,
|
||||||
|
nodePositions: Map<string, NodePosition>
|
||||||
|
): Map<string, GraphTaskElement> => {
|
||||||
|
const nodeMap = new Map<string, GraphTaskElement>();
|
||||||
|
|
||||||
|
levelsMap.forEach((platformsInLevel, level) => {
|
||||||
|
platformsInLevel.forEach(platform => {
|
||||||
|
const pos = nodePositions.get(platform)!;
|
||||||
|
const componentId = 1;
|
||||||
|
|
||||||
|
const node: GraphTaskElement = {
|
||||||
|
id: 0,
|
||||||
|
key: generateKey(),
|
||||||
|
type: 'scenario',
|
||||||
|
name: platform,
|
||||||
|
platformId: 0,
|
||||||
|
scenarioId: 0,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
id: componentId,
|
||||||
|
name: `${platform}_comm`,
|
||||||
|
type: 'communication',
|
||||||
|
description: `通信组件`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
template: 0,
|
||||||
|
templateType: null,
|
||||||
|
category: null,
|
||||||
|
multiable: false,
|
||||||
|
group: null,
|
||||||
|
description: platform,
|
||||||
|
order: 0,
|
||||||
|
position: {
|
||||||
|
x: Math.round(pos.x),
|
||||||
|
y: Math.round(pos.y),
|
||||||
|
},
|
||||||
|
width: 250,
|
||||||
|
height: 145,
|
||||||
|
inputs: null,
|
||||||
|
outputs: null,
|
||||||
|
parameters: [],
|
||||||
|
variables: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
nodeMap.set(platform, node);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return nodeMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 辅助函数:创建根节点 ====================
|
||||||
|
|
||||||
|
const createRootNode = (
|
||||||
|
nodeMap: Map<string, GraphTaskElement>,
|
||||||
|
rootPlatformName: string,
|
||||||
|
parentToChildren: Map<string, string[]>
|
||||||
|
): void => {
|
||||||
|
if (nodeMap.size === 0 || !rootPlatformName) return;
|
||||||
|
|
||||||
|
const directChildren = parentToChildren.get(rootPlatformName) || [];
|
||||||
|
|
||||||
|
let centerY: number;
|
||||||
|
if (directChildren.length > 0) {
|
||||||
|
const childYPositions = directChildren.map(child => {
|
||||||
|
const childNode = nodeMap.get(child);
|
||||||
|
return childNode ? childNode.position.y : 0;
|
||||||
|
}).filter(y => y > 0);
|
||||||
|
|
||||||
|
if (childYPositions.length > 0) {
|
||||||
|
centerY = childYPositions.reduce((sum, y) => sum + y, 0) / childYPositions.length;
|
||||||
|
} else {
|
||||||
|
const allNodes = Array.from(nodeMap.values());
|
||||||
|
const minY = Math.min(...allNodes.map(n => n.position.y));
|
||||||
|
const maxY = Math.max(...allNodes.map(n => n.position.y));
|
||||||
|
centerY = (minY + maxY) / 2;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const allNodes = Array.from(nodeMap.values());
|
||||||
|
const minY = Math.min(...allNodes.map(n => n.position.y));
|
||||||
|
const maxY = Math.max(...allNodes.map(n => n.position.y));
|
||||||
|
centerY = (minY + maxY) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootNode: GraphTaskElement = {
|
||||||
|
id: 0,
|
||||||
|
key: generateKey(),
|
||||||
|
type: 'scenario',
|
||||||
|
name: rootPlatformName,
|
||||||
|
platformId: 0,
|
||||||
|
scenarioId: 0,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: `${rootPlatformName}_comm`,
|
||||||
|
type: 'communication',
|
||||||
|
description: `通信组件`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
template: 0,
|
||||||
|
templateType: null,
|
||||||
|
category: null,
|
||||||
|
multiable: false,
|
||||||
|
group: null,
|
||||||
|
description: rootPlatformName,
|
||||||
|
order: 0,
|
||||||
|
position: {
|
||||||
|
x: 50, // ROOT_X
|
||||||
|
y: Math.round(centerY),
|
||||||
|
},
|
||||||
|
width: 250,
|
||||||
|
height: 145,
|
||||||
|
inputs: null,
|
||||||
|
outputs: null,
|
||||||
|
parameters: [],
|
||||||
|
variables: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
nodeMap.set(rootPlatformName, rootNode);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 辅助函数:创建边 ====================
|
||||||
|
|
||||||
|
const createEdges = (
|
||||||
|
records: CommunicationRecord[],
|
||||||
|
nodeMap: Map<string, GraphTaskElement>
|
||||||
|
): GraphEdgeElement[] => {
|
||||||
|
const edges: GraphEdgeElement[] = [];
|
||||||
|
|
||||||
|
records.forEach((record, index) => {
|
||||||
|
const sourceNode = nodeMap.get(record.command_platform);
|
||||||
|
const targetNode = nodeMap.get(record.subordinate_platform);
|
||||||
|
|
||||||
|
if (sourceNode && targetNode && sourceNode.key && targetNode.key) {
|
||||||
|
const sourceCompId = sourceNode.components?.[0]?.id || 1;
|
||||||
|
const targetCompId = targetNode.components?.[0]?.id || 1;
|
||||||
|
|
||||||
|
edges.push({
|
||||||
|
id: index + 1,
|
||||||
|
key: generateKey(),
|
||||||
|
source: sourceNode.key,
|
||||||
|
target: targetNode.key,
|
||||||
|
sourcePort: `out-${sourceCompId}`,
|
||||||
|
targetPort: `in-${targetCompId}`,
|
||||||
|
attrs: {},
|
||||||
|
router: { name: 'normal' },
|
||||||
|
connector: { name: 'smooth' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return edges;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 新增功能:平台数据转通信关系 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台组件接口(来自后端API)
|
||||||
|
*/
|
||||||
|
export interface PlatformComponent {
|
||||||
|
id: number;
|
||||||
|
name: string | null;
|
||||||
|
type: string | null;
|
||||||
|
description: string | null;
|
||||||
|
platformId: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带组件的平台接口(来自后端API)
|
||||||
|
*/
|
||||||
|
export interface PlatformWithComponents {
|
||||||
|
id: number;
|
||||||
|
name: string | null;
|
||||||
|
description: string | null;
|
||||||
|
scenarioId: number;
|
||||||
|
components: PlatformComponent[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能推断通信关系的策略类型
|
||||||
|
*/
|
||||||
|
type InferenceStrategy =
|
||||||
|
| 'by-name-pattern' // 根据名称模式推断(如包含"command"、"cmd"等关键词)
|
||||||
|
| 'all-to-first' // 所有节点连接到第一个节点
|
||||||
|
| 'chain' // 链式连接(A→B→C→D...)
|
||||||
|
| 'star'; // 星型连接(中心节点连接所有其他节点)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将平台列表转换为通信关系记录
|
||||||
|
* @param platforms 平台列表
|
||||||
|
* @param strategy 推断策略,默认为 'by-name-pattern'
|
||||||
|
* @returns 通信关系记录数组
|
||||||
|
*/
|
||||||
|
export const convertPlatformsToCommunicationRecords = (
|
||||||
|
platforms: PlatformWithComponents[],
|
||||||
|
strategy: InferenceStrategy = 'by-name-pattern'
|
||||||
|
): CommunicationRecord[] => {
|
||||||
|
if (!platforms || platforms.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const records: CommunicationRecord[] = [];
|
||||||
|
|
||||||
|
switch (strategy) {
|
||||||
|
case 'by-name-pattern':
|
||||||
|
inferByNamPattern(platforms, records);
|
||||||
|
break;
|
||||||
|
case 'all-to-first':
|
||||||
|
connectAllToFirst(platforms, records);
|
||||||
|
break;
|
||||||
|
case 'chain':
|
||||||
|
createChainConnection(platforms, records);
|
||||||
|
break;
|
||||||
|
case 'star':
|
||||||
|
createStarConnection(platforms, records);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`未知的推断策略: ${strategy},使用默认策略 'by-name-pattern'`);
|
||||||
|
inferByNamPattern(platforms, records);
|
||||||
|
}
|
||||||
|
|
||||||
|
return records;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据名称模式推断通信关系
|
||||||
|
* - 识别指挥节点(包含"command"、"cmd"、"指挥"等关键词)
|
||||||
|
* - 其他节点连接到最近的指挥节点
|
||||||
|
*/
|
||||||
|
const inferByNamPattern = (
|
||||||
|
platforms: PlatformWithComponents[],
|
||||||
|
records: CommunicationRecord[]
|
||||||
|
): void => {
|
||||||
|
// 识别指挥节点
|
||||||
|
const commandKeywords = ['command', 'cmd', '指挥', 'chief', 'leader'];
|
||||||
|
const commandNodes = platforms.filter(p => {
|
||||||
|
const name = (p.name || '').toLowerCase();
|
||||||
|
const desc = (p.description || '').toLowerCase();
|
||||||
|
return commandKeywords.some(kw => name.includes(kw) || desc.includes(kw));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 非指挥节点
|
||||||
|
const otherNodes = platforms.filter(p => !commandNodes.includes(p));
|
||||||
|
|
||||||
|
// 如果有指挥节点,其他节点连接到最近的指挥节点
|
||||||
|
if (commandNodes.length > 0) {
|
||||||
|
// 按ID排序,取第一个作为主要指挥中心
|
||||||
|
const mainCommand = commandNodes.sort((a, b) => a.id - b.id)[0];
|
||||||
|
|
||||||
|
if (!mainCommand) return; // 安全检查
|
||||||
|
|
||||||
|
// 其他指挥节点也连接到主指挥
|
||||||
|
commandNodes.slice(1).forEach(cmd => {
|
||||||
|
records.push({
|
||||||
|
id: records.length + 1,
|
||||||
|
command_platform: mainCommand.name || `platform_${mainCommand.id}`,
|
||||||
|
subordinate_platform: cmd.name || `platform_${cmd.id}`,
|
||||||
|
scenary_id: mainCommand.scenarioId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 非指挥节点连接到主指挥
|
||||||
|
otherNodes.forEach(node => {
|
||||||
|
records.push({
|
||||||
|
id: records.length + 1,
|
||||||
|
command_platform: mainCommand.name || `platform_${mainCommand.id}`,
|
||||||
|
subordinate_platform: node.name || `platform_${node.id}`,
|
||||||
|
scenary_id: mainCommand.scenarioId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 没有指挥节点,使用链式连接
|
||||||
|
createChainConnection(platforms, records);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有节点连接到第一个节点
|
||||||
|
*/
|
||||||
|
const connectAllToFirst = (
|
||||||
|
platforms: PlatformWithComponents[],
|
||||||
|
records: CommunicationRecord[]
|
||||||
|
): void => {
|
||||||
|
if (platforms.length < 2) return;
|
||||||
|
|
||||||
|
const first = platforms[0];
|
||||||
|
if (!first) return; // 安全检查
|
||||||
|
|
||||||
|
const firstName = first.name || `platform_${first.id}`;
|
||||||
|
|
||||||
|
platforms.slice(1).forEach(platform => {
|
||||||
|
records.push({
|
||||||
|
id: records.length + 1,
|
||||||
|
command_platform: firstName,
|
||||||
|
subordinate_platform: platform.name || `platform_${platform.id}`,
|
||||||
|
scenary_id: first.scenarioId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建链式连接 A→B→C→D...
|
||||||
|
*/
|
||||||
|
const createChainConnection = (
|
||||||
|
platforms: PlatformWithComponents[],
|
||||||
|
records: CommunicationRecord[]
|
||||||
|
): void => {
|
||||||
|
for (let i = 0; i < platforms.length - 1; i++) {
|
||||||
|
const current = platforms[i];
|
||||||
|
const next = platforms[i + 1];
|
||||||
|
|
||||||
|
if (!current || !next) continue; // 安全检查
|
||||||
|
|
||||||
|
records.push({
|
||||||
|
id: records.length + 1,
|
||||||
|
command_platform: current.name || `platform_${current.id}`,
|
||||||
|
subordinate_platform: next.name || `platform_${next.id}`,
|
||||||
|
scenary_id: current.scenarioId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建星型连接(中间节点连接所有其他节点)
|
||||||
|
*/
|
||||||
|
const createStarConnection = (
|
||||||
|
platforms: PlatformWithComponents[],
|
||||||
|
records: CommunicationRecord[]
|
||||||
|
): void => {
|
||||||
|
if (platforms.length < 2) return;
|
||||||
|
|
||||||
|
// 选择中间的节点作为中心
|
||||||
|
const centerIndex = Math.floor(platforms.length / 2);
|
||||||
|
const center = platforms[centerIndex];
|
||||||
|
|
||||||
|
if (!center) return; // 安全检查
|
||||||
|
|
||||||
|
const centerName = center.name || `platform_${center.id}`;
|
||||||
|
|
||||||
|
platforms.forEach((platform, idx) => {
|
||||||
|
if (idx !== centerIndex) {
|
||||||
|
records.push({
|
||||||
|
id: records.length + 1,
|
||||||
|
command_platform: centerName,
|
||||||
|
subordinate_platform: platform.name || `platform_${platform.id}`,
|
||||||
|
scenary_id: center.scenarioId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷函数:直接将平台列表转换为图容器
|
||||||
|
* @param platforms 平台列表
|
||||||
|
* @param strategy 推断策略
|
||||||
|
* @returns 图容器对象
|
||||||
|
*/
|
||||||
|
export const convertPlatformsToGraphContainer = (
|
||||||
|
platforms: PlatformWithComponents[],
|
||||||
|
strategy: InferenceStrategy = 'by-name-pattern'
|
||||||
|
): GraphContainer => {
|
||||||
|
const records = convertPlatformsToCommunicationRecords(platforms, strategy);
|
||||||
|
return convertRecordsToGraphContainer(records);
|
||||||
|
};
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of the kernelstudio package.
|
||||||
|
*
|
||||||
|
* (c) 2014-2026 zlin <admin@kernelstudio.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE file
|
||||||
|
* that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { GraphContainer } from '../graph';
|
||||||
|
import { convertRecordsToGraphContainer, type CommunicationRecord } from './data-converter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机的通信关系数据用于测试
|
||||||
|
* @param nodeCount 节点数量(默认5-8个)
|
||||||
|
* @param edgeDensity 边的密度(0.3-0.7之间,表示连接概率)
|
||||||
|
* @returns 随机生成的通信关系记录和对应的GraphContainer
|
||||||
|
*/
|
||||||
|
export const generateRandomCommunicationData = (
|
||||||
|
nodeCount: number = Math.floor(Math.random() * 4) + 5, // 5-8个节点
|
||||||
|
edgeDensity: number = 0.5
|
||||||
|
): { records: CommunicationRecord[]; graph: GraphContainer } => {
|
||||||
|
// 生成随机平台名称
|
||||||
|
const platformTypes = ['指挥中心', '雷达站', '导弹阵地', '预警机', '战斗机', '驱逐舰', '潜艇', '电子战飞机'];
|
||||||
|
const platforms: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < nodeCount; i++) {
|
||||||
|
const baseName = platformTypes[i % platformTypes.length];
|
||||||
|
const suffix = Math.floor(i / platformTypes.length) > 0 ? `-${Math.floor(i / platformTypes.length) + 1}` : '';
|
||||||
|
platforms.push(`${baseName}${suffix}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机通信关系 - 改进版:更符合实际指挥层级
|
||||||
|
const records: CommunicationRecord[] = [];
|
||||||
|
let recordId = 1;
|
||||||
|
|
||||||
|
// 确定根节点(指挥中心)
|
||||||
|
const rootPlatformName = platforms.find(p => p.includes('指挥')) || platforms[0] || '默认平台';
|
||||||
|
const rootIndex = platforms.indexOf(rootPlatformName);
|
||||||
|
|
||||||
|
if (rootIndex === -1) {
|
||||||
|
console.warn('未找到根节点,使用第一个平台');
|
||||||
|
return { records: [], graph: { nodes: [], edges: [] } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一层:指挥中心直接管理的单位(通常是主要作战单元)
|
||||||
|
// 选择2-4个作为一级下属
|
||||||
|
const firstLevelCount = Math.min(Math.max(2, Math.floor(platforms.length / 2)), 4);
|
||||||
|
const firstLevelIndices: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < platforms.length && firstLevelIndices.length < firstLevelCount; i++) {
|
||||||
|
if (i !== rootIndex) {
|
||||||
|
firstLevelIndices.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 建立第一层连接:指挥中心 -> 一级下属
|
||||||
|
firstLevelIndices.forEach(idx => {
|
||||||
|
const subordinatePlatform = platforms[idx];
|
||||||
|
if (subordinatePlatform) {
|
||||||
|
records.push({
|
||||||
|
id: recordId++,
|
||||||
|
command_platform: rootPlatformName,
|
||||||
|
subordinate_platform: subordinatePlatform,
|
||||||
|
command_comm: '加密指挥链路',
|
||||||
|
subordinate_comm: '接收端',
|
||||||
|
scenary_id: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 第二层:一级下属可以有二级下属
|
||||||
|
const remainingIndices = platforms.map((_, i) => i).filter(i =>
|
||||||
|
i !== rootIndex && !firstLevelIndices.includes(i)
|
||||||
|
);
|
||||||
|
|
||||||
|
remainingIndices.forEach(idx => {
|
||||||
|
// 随机选择一个一级下属作为父节点
|
||||||
|
const parentIdx = firstLevelIndices[Math.floor(Math.random() * firstLevelIndices.length)];
|
||||||
|
|
||||||
|
if (parentIdx !== undefined) {
|
||||||
|
const parentPlatform = platforms[parentIdx];
|
||||||
|
const childPlatform = platforms[idx];
|
||||||
|
|
||||||
|
if (parentPlatform && childPlatform) {
|
||||||
|
records.push({
|
||||||
|
id: recordId++,
|
||||||
|
command_platform: parentPlatform,
|
||||||
|
subordinate_platform: childPlatform,
|
||||||
|
command_comm: Math.random() > 0.5 ? '战术数据链' : '无线通信',
|
||||||
|
subordinate_comm: '双向通信',
|
||||||
|
scenary_id: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 第三层:添加少量横向协同连接(不超过总边数的20%)
|
||||||
|
const maxCrossLinks = Math.floor(records.length * 0.2);
|
||||||
|
let crossLinkCount = 0;
|
||||||
|
|
||||||
|
if (platforms.length > 4 && crossLinkCount < maxCrossLinks) {
|
||||||
|
// 在同级之间添加协同连接
|
||||||
|
for (let i = 0; i < firstLevelIndices.length - 1 && crossLinkCount < maxCrossLinks; i++) {
|
||||||
|
if (Math.random() < 0.4) { // 40%概率
|
||||||
|
const j = i + 1 + Math.floor(Math.random() * 2);
|
||||||
|
if (j < firstLevelIndices.length) {
|
||||||
|
const idxI = firstLevelIndices[i];
|
||||||
|
const idxJ = firstLevelIndices[j];
|
||||||
|
|
||||||
|
if (idxI !== undefined && idxJ !== undefined) {
|
||||||
|
const platformI = platforms[idxI];
|
||||||
|
const platformJ = platforms[idxJ];
|
||||||
|
|
||||||
|
if (platformI && platformJ) {
|
||||||
|
const exists = records.some(r =>
|
||||||
|
r.command_platform === platformI &&
|
||||||
|
r.subordinate_platform === platformJ
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
records.push({
|
||||||
|
id: recordId++,
|
||||||
|
command_platform: platformI,
|
||||||
|
subordinate_platform: platformJ,
|
||||||
|
command_comm: '协同通信',
|
||||||
|
subordinate_comm: '双向通信',
|
||||||
|
scenary_id: 1,
|
||||||
|
});
|
||||||
|
crossLinkCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为GraphContainer
|
||||||
|
const graph = convertRecordsToGraphContainer(records);
|
||||||
|
|
||||||
|
return { records, graph };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user