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-actions">
|
||||
<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">
|
||||
<CheckOutlined />
|
||||
<span>保存</span>
|
||||
@@ -50,14 +58,14 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { getTeleport } from '@antv/x6-vue-shape';
|
||||
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 { safePreventDefault, safeStopPropagation } from '@/utils/event';
|
||||
import Header from '../header.vue';
|
||||
|
||||
import type { Scenario } 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 { createGraphScenarioElement, createGraphTaskElementFromScenario } from './utils';
|
||||
@@ -66,6 +74,8 @@ import PlatformCard from './platform-card.vue';
|
||||
import NodesCard from './nodes-card.vue';
|
||||
import { findOneScenarioById, saveScenario } from './api';
|
||||
import { resolveConnectionRelation } from './relation';
|
||||
import { generateRandomCommunicationData } from './random-data-generator';
|
||||
import { convertRecordsToGraphContainer, type CommunicationRecord } from './data-converter';
|
||||
|
||||
const TeleportContainer = defineComponent(getTeleport());
|
||||
|
||||
@@ -81,6 +91,8 @@ export default defineComponent({
|
||||
CheckCircleOutlined,
|
||||
CheckOutlined,
|
||||
RollbackOutlined,
|
||||
ThunderboltOutlined,
|
||||
DatabaseOutlined,
|
||||
TeleportContainer,
|
||||
},
|
||||
setup() {
|
||||
@@ -229,6 +241,8 @@ export default defineComponent({
|
||||
graph: nodeGraph,
|
||||
relations: []
|
||||
};
|
||||
console.log(currentScenario.value);
|
||||
|
||||
currentScenarioEditing.value = true;
|
||||
createElements();
|
||||
};
|
||||
@@ -261,6 +275,7 @@ export default defineComponent({
|
||||
}, 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(() => {
|
||||
init();
|
||||
@@ -444,6 +602,8 @@ export default defineComponent({
|
||||
handleDrop,
|
||||
isDraggingOver,
|
||||
handleSave,
|
||||
handleGenerateRandom,
|
||||
handleLoadFromBackend,
|
||||
handleUpdateElement,
|
||||
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