diff --git a/modeler/src/views/decision/communication/communication.vue b/modeler/src/views/decision/communication/communication.vue index 36f0909..b5b6549 100644 --- a/modeler/src/views/decision/communication/communication.vue +++ b/modeler/src/views/decision/communication/communication.vue @@ -21,6 +21,14 @@
+ + + 随机生成 + + + + 从后端加载 + 保存 @@ -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, }; diff --git a/modeler/src/views/decision/communication/data-converter.ts b/modeler/src/views/decision/communication/data-converter.ts new file mode 100644 index 0000000..e5b55d8 --- /dev/null +++ b/modeler/src/views/decision/communication/data-converter.ts @@ -0,0 +1,906 @@ +/* + * This file is part of the kernelstudio package. + * + * (c) 2014-2026 zlin + * + * 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; + childToParent: Map; +} + +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(); + 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, + childToParent: Map +): Set[] => { + const visited = new Set(); + const components: Set[] = []; + + // DFS遍历找出所有连通分量 + const dfs = (platform: string, component: Set) => { + 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(); + 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 +): CommunicationRecord[] => { + return records.filter(record => + component.has(record.command_platform) && + component.has(record.subordinate_platform) + ); +}; + +// ==================== 辅助函数:构建父子映射 ==================== + +const buildParentChildMappings = (records: CommunicationRecord[]): ParentChildMappings => { + const parentToChildren = new Map(); + const childToParent = new Map(); + + 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, + childToParent: Map +): Map => { + const levelMap = new Map(); + const processed = new Set(); + 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): Map => { + const levelsMap = new Map(); + levelMap.forEach((level, platform) => { + if (!levelsMap.has(level)) { + levelsMap.set(level, []); + } + levelsMap.get(level)!.push(platform); + }); + return levelsMap; +}; + +// ==================== 辅助函数:计算节点位置 ==================== + +const calculateNodePositions = ( + levelsMap: Map, + parentToChildren: Map, + params: LayoutParams +): Map => { + const nodePositions = new Map(); + const subtreeRanges = new Map(); + + // 第一步:预计算每列需要的总高度 + 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, + parentToChildren: Map, + minRowSpacing: number, + subtreeRanges: Map +): 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, + parentToChildren: Map, + params: LayoutParams, + subtreeRanges: Map, + nodePositions: Map +): void => { + const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b); + const columnOffsets = new Map(); + + 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, + parentToChildren: Map, + nodePositions: Map +): 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, + parentToChildren: Map, + minRowSpacing: number, + nodePositions: Map +): 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, + parentToChildren: Map, + nodePositions: Map +): 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, + nodePositions: Map +): Map => { + const nodeMap = new Map(); + + 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, + rootPlatformName: string, + parentToChildren: Map +): 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 +): 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); +}; diff --git a/modeler/src/views/decision/communication/random-data-generator.ts b/modeler/src/views/decision/communication/random-data-generator.ts new file mode 100644 index 0000000..9d81e11 --- /dev/null +++ b/modeler/src/views/decision/communication/random-data-generator.ts @@ -0,0 +1,143 @@ +/* + * This file is part of the kernelstudio package. + * + * (c) 2014-2026 zlin + * + * 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 }; +};