添加行为决策树管理的场景数据到节点数据的自动转换逻辑
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { HttpRequestClient } from '@/utils/request';
|
import { HttpRequestClient } from '@/utils/request';
|
||||||
import type { Scenario, ScenarioDetailsResponse, ScenarioPageableResponse, ScenarioRequest } from './types';
|
import type { Scenario, ScenarioDetailsResponse, ScenarioPageableResponse, ScenarioRequest, CommunicationRelationsResponse } from './types';
|
||||||
import type { PlatformWithComponentsResponse } from '../types';
|
import type { PlatformWithComponentsResponse } from '../types';
|
||||||
import type { BasicResponse } from '@/types';
|
import type { BasicResponse } from '@/types';
|
||||||
|
|
||||||
@@ -32,6 +32,15 @@ export const findPlatformWithComponents = (id: number): Promise<PlatformWithComp
|
|||||||
return req.get<PlatformWithComponentsResponse>(`/system/firerule/platforms/${id}`);
|
return req.get<PlatformWithComponentsResponse>(`/system/firerule/platforms/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取场景的所有通信关系
|
||||||
|
* @param id 场景ID
|
||||||
|
* @returns 通信关系列表
|
||||||
|
*/
|
||||||
|
export const findRelations = (id: number): Promise<CommunicationRelationsResponse> => {
|
||||||
|
return req.get<CommunicationRelationsResponse>(`/system/scene/getAllRelation/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
export const saveScenario = (scenario: Scenario): Promise<BasicResponse> => {
|
export const saveScenario = (scenario: Scenario): Promise<BasicResponse> => {
|
||||||
return req.postJson<BasicResponse>(`/system/scene/saveSceneConfig`,scenario);
|
return req.postJson<BasicResponse>(`/system/scene/saveSceneConfig`,scenario);
|
||||||
};
|
};
|
||||||
@@ -72,7 +72,7 @@ import { createGraphScenarioElement, createGraphTaskElementFromScenario } from '
|
|||||||
|
|
||||||
import PlatformCard from './platform-card.vue';
|
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, findRelations } from './api';
|
||||||
import { resolveConnectionRelation } from './relation';
|
import { resolveConnectionRelation } from './relation';
|
||||||
import { generateRandomCommunicationData } from './random-data-generator';
|
import { generateRandomCommunicationData } from './random-data-generator';
|
||||||
import { convertRecordsToGraphContainer, type CommunicationRecord } from './data-converter';
|
import { convertRecordsToGraphContainer, type CommunicationRecord } from './data-converter';
|
||||||
@@ -223,27 +223,77 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = (scenario: Scenario) => {
|
const handleSelect = async (scenario: Scenario) => {
|
||||||
let nodeGraph: GraphContainer | null = null;
|
let nodeGraph: GraphContainer | null = null;
|
||||||
try {
|
try {
|
||||||
nodeGraph = JSON.parse(scenario.communicationGraph as unknown as string) as unknown as GraphContainer;
|
nodeGraph = JSON.parse(scenario.communicationGraph as unknown as string) as unknown as GraphContainer;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('parse error,cause:', e);
|
console.error('parse error,cause:', e);
|
||||||
}
|
}
|
||||||
if (!nodeGraph) {
|
|
||||||
nodeGraph = {
|
// 设置当前场景
|
||||||
nodes: [],
|
|
||||||
edges: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
currentScenario.value = {
|
currentScenario.value = {
|
||||||
...scenario,
|
...scenario,
|
||||||
graph: nodeGraph,
|
graph: nodeGraph || { nodes: [], edges: [] },
|
||||||
relations: []
|
relations: []
|
||||||
};
|
};
|
||||||
console.log(currentScenario.value);
|
console.log('选中场景:', currentScenario.value);
|
||||||
|
|
||||||
currentScenarioEditing.value = true;
|
currentScenarioEditing.value = true;
|
||||||
|
|
||||||
|
// 如果场景有ID且没有已保存的图数据,尝试从后端加载通信关系
|
||||||
|
if (scenario.id > 0 && !nodeGraph) {
|
||||||
|
try {
|
||||||
|
message.loading({ content: '正在加载通信关系...', key: 'loading-relations' });
|
||||||
|
const response = await findRelations(scenario.id);
|
||||||
|
|
||||||
|
console.log('API完整响应:', response);
|
||||||
|
|
||||||
|
// 解析API响应(支持多种格式)
|
||||||
|
let relations: any[] = [];
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
relations = response.data;
|
||||||
|
} else if (response.data && Array.isArray((response.data as any).data)) {
|
||||||
|
relations = (response.data as any).data;
|
||||||
|
} else if (response.data && Array.isArray((response.data as any).rows)) {
|
||||||
|
relations = (response.data as any).rows;
|
||||||
|
} else if (response.data && Array.isArray((response.data as any).list)) {
|
||||||
|
relations = (response.data as any).list;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('解析后的通信关系数量:', relations.length);
|
||||||
|
|
||||||
|
if (relations.length > 0) {
|
||||||
|
// 字段名标准化(驼峰转下划线)
|
||||||
|
const normalizedRelations = relations.map((item: any) => ({
|
||||||
|
id: item.id,
|
||||||
|
command_platform: item.commandPlatform || item.command_platform,
|
||||||
|
subordinate_platform: item.subordinatePlatform || item.subordinate_platform,
|
||||||
|
command_comm: item.commandComm || item.command_comm,
|
||||||
|
subordinate_comm: item.subordinateComm || item.subordinate_comm,
|
||||||
|
scenary_id: item.scenaryId || item.scenary_id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('标准化后的第一条记录:', normalizedRelations[0]);
|
||||||
|
|
||||||
|
// 转换为图数据
|
||||||
|
const convertedGraph = convertRecordsToGraphContainer(normalizedRelations);
|
||||||
|
console.log('转换后的图数据:', convertedGraph);
|
||||||
|
|
||||||
|
// 更新当前场景的图数据
|
||||||
|
currentScenario.value.graph = convertedGraph;
|
||||||
|
currentScenario.value.communicationGraph = JSON.stringify(convertedGraph);
|
||||||
|
|
||||||
|
message.success({ content: `成功加载 ${normalizedRelations.length} 条通信关系`, key: 'loading-relations' });
|
||||||
|
} else {
|
||||||
|
message.warning({ content: '该场景暂无通信关系数据', key: 'loading-relations' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('从后端加载通信关系失败:', error);
|
||||||
|
message.error({ content: '加载通信关系失败,请手动点击"从后端加载"', key: 'loading-relations' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createElements();
|
createElements();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -505,25 +555,69 @@ export default defineComponent({
|
|||||||
try {
|
try {
|
||||||
message.loading({ content: '正在加载通信关系数据...', key: 'loading' });
|
message.loading({ content: '正在加载通信关系数据...', key: 'loading' });
|
||||||
|
|
||||||
// TODO: 等待后端提供真实接口后替换为实际API调用
|
// 调用真实API获取通信关系
|
||||||
// const response = await findCommunicationRelations(currentScenario.value.id);
|
console.log(`正在从后端加载场景 ${currentScenario.value.id} 的通信关系...`);
|
||||||
// const relations = response.data || [];
|
const response = await findRelations(currentScenario.value.id);
|
||||||
|
|
||||||
// 临时使用模拟数据
|
console.log('API完整响应:', response);
|
||||||
console.log('使用模拟通信关系数据');
|
console.log('response.data类型:', typeof response.data, Array.isArray(response.data) ? 'Array' : 'Object');
|
||||||
const mockRelations: CommunicationRecord[] = [
|
|
||||||
{ id: 1, command_platform: '指挥中心', subordinate_platform: '预警机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
|
// API返回的是 CommunicationRelationRecord[],与 CommunicationRecord 结构兼容
|
||||||
{ 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 },
|
let relations: any[] = [];
|
||||||
{ id: 4, command_platform: '预警机', subordinate_platform: '电子战飞机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
|
if (Array.isArray(response.data)) {
|
||||||
{ id: 5, command_platform: '驱逐舰', subordinate_platform: '潜艇', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
|
relations = response.data;
|
||||||
{ id: 6, command_platform: '战斗机', subordinate_platform: '无人机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
|
} else if (response.data && Array.isArray((response.data as any).data)) {
|
||||||
];
|
relations = (response.data as any).data;
|
||||||
|
} else if (response.data && Array.isArray((response.data as any).rows)) {
|
||||||
|
relations = (response.data as any).rows;
|
||||||
|
} else if (response.data && Array.isArray((response.data as any).list)) {
|
||||||
|
relations = (response.data as any).list;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('解析后的通信关系数量:', relations.length);
|
||||||
|
if (relations.length > 0) {
|
||||||
|
console.log('第一条记录:', JSON.stringify(relations[0], null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端返回的是驼峰命名,需要转换为下划线命名以匹配前端类型
|
||||||
|
const normalizedRelations = relations.map((item: any) => ({
|
||||||
|
id: item.id,
|
||||||
|
command_platform: item.commandPlatform || item.command_platform,
|
||||||
|
subordinate_platform: item.subordinatePlatform || item.subordinate_platform,
|
||||||
|
command_comm: item.commandComm || item.command_comm,
|
||||||
|
subordinate_comm: item.subordinateComm || item.subordinate_comm,
|
||||||
|
scenary_id: item.scenaryId || item.scenary_id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('标准化后的第一条记录:', normalizedRelations[0]);
|
||||||
|
|
||||||
|
if (normalizedRelations.length === 0) {
|
||||||
|
console.warn('API未返回任何通信关系数据,使用模拟数据作为fallback');
|
||||||
|
// Fallback到模拟数据(保留以便测试)
|
||||||
|
relations.push(
|
||||||
|
{ id: 6, command_platform: 'chief', subordinate_platform: 'task1_commander', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 7, command_platform: 'chief', subordinate_platform: 'task2_commander', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 8, command_platform: 'chief', subordinate_platform: 'task3_commander', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 9, command_platform: 'task1_commander', subordinate_platform: 'platform1', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 10, command_platform: 'task1_commander', subordinate_platform: 'platform3', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 11, command_platform: 'task1_commander', subordinate_platform: 'platform4', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 12, command_platform: 'task1_commander', subordinate_platform: 'platform5', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 13, command_platform: 'task1_commander', subordinate_platform: 'platform6', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 14, command_platform: 'task2_commander', subordinate_platform: 'platform3', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 15, command_platform: 'task2_commander', subordinate_platform: 'platform5', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 16, command_platform: 'task2_commander', subordinate_platform: 'platform4', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 17, command_platform: 'task3_commander', subordinate_platform: 'platform6', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 18, command_platform: 'task3_commander', subordinate_platform: 'platform6', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 19, command_platform: 'task3_commander', subordinate_platform: 'platform7', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 20, command_platform: 'task3_commander', subordinate_platform: 'platform8', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('模拟的通信记录:', mockRelations);
|
console.log('最终使用的通信记录:', normalizedRelations);
|
||||||
|
|
||||||
// 使用模拟数据进行转换
|
// 使用数据进行转换
|
||||||
const convertedGraph = convertRecordsToGraphContainer(mockRelations);
|
const convertedGraph = convertRecordsToGraphContainer(normalizedRelations);
|
||||||
|
|
||||||
console.log('转换后的图数据:', convertedGraph);
|
console.log('转换后的图数据:', convertedGraph);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,22 @@ import { generateKey } from '@/utils/strings';
|
|||||||
|
|
||||||
// ==================== 类型定义 ====================
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 布局方向类型
|
||||||
|
*/
|
||||||
|
export type LayoutDirection = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 布局配置接口
|
||||||
|
*/
|
||||||
|
export interface LayoutConfig {
|
||||||
|
direction: LayoutDirection;
|
||||||
|
rootPosition?: number; // 根节点起始位置
|
||||||
|
colSpacing?: number; // 列间距
|
||||||
|
rowSpacing?: number; // 行间距
|
||||||
|
startPosition?: number; // 起始位置
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据库通信关系记录接口
|
* 数据库通信关系记录接口
|
||||||
*/
|
*/
|
||||||
@@ -30,10 +46,11 @@ interface ParentChildMappings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface LayoutParams {
|
interface LayoutParams {
|
||||||
ROOT_X: number;
|
ROOT_POSITION: number; // 根节点起始位置(horizontal时为X, vertical时为Y)
|
||||||
COL_SPACING: number;
|
LEVEL_SPACING: number; // 层级间距(horizontal时为X间距, vertical时为Y间距)
|
||||||
MIN_ROW_SPACING: number;
|
SIBLING_SPACING: number; // 同级间距(horizontal时为Y间距, vertical时为X间距)
|
||||||
START_Y: number;
|
START_POSITION: number; // 起始位置(horizontal时为Y, vertical时为X)
|
||||||
|
direction: LayoutDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NodePosition {
|
interface NodePosition {
|
||||||
@@ -42,31 +59,107 @@ interface NodePosition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SubtreeRange {
|
interface SubtreeRange {
|
||||||
minY: number;
|
min: number;
|
||||||
maxY: number;
|
max: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 坐标映射器接口 - 用于抽象水平和垂直布局的坐标转换
|
||||||
|
*/
|
||||||
|
interface CoordinateMapper {
|
||||||
|
/** 将层级索引映射到主轴坐标 */
|
||||||
|
mapLevelToAxis(level: number): number;
|
||||||
|
/** 将同级索引映射到次轴坐标 */
|
||||||
|
mapSiblingIndexToAxis(index: number, totalInLevel?: number): number;
|
||||||
|
/** 获取节点的主轴坐标 */
|
||||||
|
getPrimaryCoord(pos: NodePosition): number;
|
||||||
|
/** 获取节点的次轴坐标 */
|
||||||
|
getSecondaryCoord(pos: NodePosition): number;
|
||||||
|
/** 设置节点坐标 */
|
||||||
|
setCoords(primary: number, secondary: number): NodePosition;
|
||||||
|
/** 获取子树范围的主轴坐标 */
|
||||||
|
getSubtreePrimaryCoord(range: SubtreeRange): { min: number; max: number };
|
||||||
|
/** 创建子树范围对象 */
|
||||||
|
createSubtreeRange(min: number, max: number): SubtreeRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建水平布局的坐标映射器 (X轴表示层级,Y轴表示同级分布)
|
||||||
|
*/
|
||||||
|
const createHorizontalMapper = (params: LayoutParams): CoordinateMapper => ({
|
||||||
|
mapLevelToAxis: (level) => params.ROOT_POSITION + (level + 1) * params.LEVEL_SPACING,
|
||||||
|
mapSiblingIndexToAxis: (index, totalInLevel) => {
|
||||||
|
// 均匀分布在START_POSITION开始的区域内
|
||||||
|
return params.START_POSITION + index * params.SIBLING_SPACING + params.SIBLING_SPACING / 2;
|
||||||
|
},
|
||||||
|
getPrimaryCoord: (pos) => pos.x,
|
||||||
|
getSecondaryCoord: (pos) => pos.y,
|
||||||
|
setCoords: (primary, secondary) => ({ x: primary, y: secondary }),
|
||||||
|
getSubtreePrimaryCoord: (range) => ({ min: range.min, max: range.max }),
|
||||||
|
createSubtreeRange: (min, max) => ({ min, max }),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建垂直布局的坐标映射器 (Y轴表示层级,X轴表示同级分布)
|
||||||
|
*/
|
||||||
|
const createVerticalMapper = (params: LayoutParams): CoordinateMapper => ({
|
||||||
|
mapLevelToAxis: (level) => params.ROOT_POSITION + (level + 1) * params.LEVEL_SPACING,
|
||||||
|
mapSiblingIndexToAxis: (index, totalInLevel) => {
|
||||||
|
// 均匀分布在START_POSITION开始的区域内
|
||||||
|
return params.START_POSITION + index * params.SIBLING_SPACING + params.SIBLING_SPACING / 2;
|
||||||
|
},
|
||||||
|
getPrimaryCoord: (pos) => pos.y,
|
||||||
|
getSecondaryCoord: (pos) => pos.x,
|
||||||
|
setCoords: (primary, secondary) => ({ x: secondary, y: primary }),
|
||||||
|
getSubtreePrimaryCoord: (range) => ({ min: range.min, max: range.max }),
|
||||||
|
createSubtreeRange: (min, max) => ({ min, max }),
|
||||||
|
});
|
||||||
|
|
||||||
// ==================== 主函数:数据转换 ====================
|
// ==================== 主函数:数据转换 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从数据库记录转换为 GraphContainer 格式
|
* 从数据库记录转换为 GraphContainer 格式
|
||||||
* @param records 数据库通信关系记录数组
|
* @param records 数据库通信关系记录数组
|
||||||
|
* @param config 布局配置(可选),默认为水平布局
|
||||||
* @returns GraphContainer 格式的节点和边数据
|
* @returns GraphContainer 格式的节点和边数据
|
||||||
*/
|
*/
|
||||||
export const convertRecordsToGraphContainer = (records: CommunicationRecord[]): GraphContainer => {
|
export const convertRecordsToGraphContainer = (
|
||||||
|
records: CommunicationRecord[],
|
||||||
|
config?: Partial<LayoutConfig>
|
||||||
|
): GraphContainer => {
|
||||||
if (!records || records.length === 0) {
|
if (!records || records.length === 0) {
|
||||||
return { nodes: [], edges: [] };
|
return { nodes: [], edges: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收集所有唯一的平台名称
|
// 合并默认配置
|
||||||
|
const finalConfig: LayoutConfig = {
|
||||||
|
direction: config?.direction || 'horizontal',
|
||||||
|
rootPosition: typeof config?.rootPosition === 'number' ? config.rootPosition : 50,
|
||||||
|
colSpacing: typeof config?.colSpacing === 'number' ? config.colSpacing : 280,
|
||||||
|
rowSpacing: typeof config?.rowSpacing === 'number' ? config.rowSpacing : 140,
|
||||||
|
startPosition: typeof config?.startPosition === 'number' ? config.startPosition : 80,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 收集所有唯一的平台名称(过滤无效值)
|
||||||
const platformSet = new Set<string>();
|
const platformSet = new Set<string>();
|
||||||
records.forEach(record => {
|
records.forEach(record => {
|
||||||
platformSet.add(record.command_platform);
|
if (record.command_platform && typeof record.command_platform === 'string' && record.command_platform.trim()) {
|
||||||
platformSet.add(record.subordinate_platform);
|
platformSet.add(record.command_platform);
|
||||||
|
}
|
||||||
|
if (record.subordinate_platform && typeof record.subordinate_platform === 'string' && record.subordinate_platform.trim()) {
|
||||||
|
platformSet.add(record.subordinate_platform);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const allPlatforms = Array.from(platformSet);
|
const allPlatforms = Array.from(platformSet);
|
||||||
|
|
||||||
|
console.log(`收集到 ${allPlatforms.length} 个唯一平台:`, allPlatforms);
|
||||||
|
|
||||||
|
if (allPlatforms.length === 0) {
|
||||||
|
console.warn('没有有效的平台数据');
|
||||||
|
return { nodes: [], edges: [] };
|
||||||
|
}
|
||||||
|
|
||||||
// 构建父子映射关系
|
// 构建父子映射关系
|
||||||
const { parentToChildren, childToParent } = buildParentChildMappings(records);
|
const { parentToChildren, childToParent } = buildParentChildMappings(records);
|
||||||
|
|
||||||
@@ -79,13 +172,11 @@ export const convertRecordsToGraphContainer = (records: CommunicationRecord[]):
|
|||||||
const allNodes: GraphTaskElement[] = [];
|
const allNodes: GraphTaskElement[] = [];
|
||||||
const allEdges: GraphEdgeElement[] = [];
|
const allEdges: GraphEdgeElement[] = [];
|
||||||
|
|
||||||
let xOffset = 50; // 第一个分量的起始X坐标
|
let offset: number = finalConfig.rootPosition ?? 50; // 第一个分量的起始位置
|
||||||
|
|
||||||
connectedComponents.forEach((component, componentIndex) => {
|
connectedComponents.forEach((component, componentIndex) => {
|
||||||
console.log(`处理第 ${componentIndex + 1} 个连通分量,包含 ${component.size} 个节点`);
|
// 为该分量确定根节点(选择入度为0的节点)
|
||||||
|
const rootPlatformName = determineRootNode(Array.from(component), childToParent);
|
||||||
// 为该分量确定根节点
|
|
||||||
const rootPlatformName = determineRootNode(Array.from(component));
|
|
||||||
|
|
||||||
if (!rootPlatformName) {
|
if (!rootPlatformName) {
|
||||||
console.warn(`第 ${componentIndex + 1} 个分量未找到根节点,跳过`);
|
console.warn(`第 ${componentIndex + 1} 个分量未找到根节点,跳过`);
|
||||||
@@ -100,29 +191,48 @@ export const convertRecordsToGraphContainer = (records: CommunicationRecord[]):
|
|||||||
childToParent
|
childToParent
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(` 分量 ${componentIndex + 1} 的层级映射:`, Object.fromEntries(levelMap));
|
||||||
|
|
||||||
// 按层级分组
|
// 按层级分组
|
||||||
const levelsMap = groupNodesByLevel(levelMap);
|
const levelsMap = groupNodesByLevel(levelMap);
|
||||||
|
|
||||||
// 布局参数 - 紧凑型布局
|
console.log(` 分量 ${componentIndex + 1} 的层级分组:`, Object.fromEntries(levelsMap));
|
||||||
|
|
||||||
|
// 根据方向创建布局参数
|
||||||
const layoutParams: LayoutParams = {
|
const layoutParams: LayoutParams = {
|
||||||
ROOT_X: xOffset, // 当前分量的起始X坐标
|
ROOT_POSITION: offset ?? 50,
|
||||||
COL_SPACING: 280, // 列间距
|
LEVEL_SPACING: finalConfig.colSpacing ?? 280,
|
||||||
MIN_ROW_SPACING: 140, // 最小行间距
|
SIBLING_SPACING: finalConfig.rowSpacing ?? 140,
|
||||||
START_Y: 80, // 起始Y坐标
|
START_POSITION: finalConfig.startPosition ?? 80,
|
||||||
|
direction: finalConfig.direction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 创建坐标映射器
|
||||||
|
const mapper = finalConfig.direction === 'horizontal'
|
||||||
|
? createHorizontalMapper(layoutParams)
|
||||||
|
: createVerticalMapper(layoutParams);
|
||||||
|
|
||||||
// 计算节点位置
|
// 计算节点位置
|
||||||
const nodePositions = calculateNodePositions(
|
const nodePositions = calculateNodePositions(
|
||||||
levelsMap,
|
levelsMap,
|
||||||
parentToChildren,
|
parentToChildren,
|
||||||
layoutParams
|
childToParent,
|
||||||
|
layoutParams,
|
||||||
|
mapper
|
||||||
);
|
);
|
||||||
|
|
||||||
// 创建节点对象
|
// 创建节点对象
|
||||||
const nodeMap = createNodeObjects(levelsMap, nodePositions);
|
const nodeMap = createNodeObjects(levelsMap, nodePositions);
|
||||||
|
|
||||||
// 创建根节点
|
console.log(` 分量 ${componentIndex + 1} 创建了 ${nodeMap.size} 个节点:`, Array.from(nodeMap.keys()));
|
||||||
createRootNode(nodeMap, rootPlatformName, parentToChildren);
|
|
||||||
|
// 创建根节点(如果不存在则添加)
|
||||||
|
if (!nodeMap.has(rootPlatformName)) {
|
||||||
|
console.warn(` 根节点 "${rootPlatformName}" 未在 levelsMap 中找到,手动创建`);
|
||||||
|
}
|
||||||
|
createRootNode(nodeMap, rootPlatformName, parentToChildren, layoutParams, mapper);
|
||||||
|
|
||||||
|
console.log(` 最终节点数: ${nodeMap.size}`);
|
||||||
|
|
||||||
// 过滤出属于当前分量的边
|
// 过滤出属于当前分量的边
|
||||||
const componentEdges = filterEdgesForComponent(records, component);
|
const componentEdges = filterEdgesForComponent(records, component);
|
||||||
@@ -134,7 +244,7 @@ export const convertRecordsToGraphContainer = (records: CommunicationRecord[]):
|
|||||||
|
|
||||||
// 计算下一个分量的偏移量(基于当前分量的宽度)
|
// 计算下一个分量的偏移量(基于当前分量的宽度)
|
||||||
const maxLevel = Math.max(...Array.from(levelsMap.keys()));
|
const maxLevel = Math.max(...Array.from(levelsMap.keys()));
|
||||||
xOffset += (maxLevel + 1) * layoutParams.COL_SPACING + 150; // 分量之间留150px间隔
|
offset += (maxLevel + 1) * layoutParams.LEVEL_SPACING + 150; // 分量之间留150px间隔
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -153,6 +263,9 @@ const findConnectedComponents = (
|
|||||||
parentToChildren: Map<string, string[]>,
|
parentToChildren: Map<string, string[]>,
|
||||||
childToParent: Map<string, string>
|
childToParent: Map<string, string>
|
||||||
): Set<string>[] => {
|
): Set<string>[] => {
|
||||||
|
// 过滤掉undefined、null和空字符串
|
||||||
|
const validPlatforms = platforms.filter(p => p && typeof p === 'string' && p.trim().length > 0);
|
||||||
|
|
||||||
const visited = new Set<string>();
|
const visited = new Set<string>();
|
||||||
const components: Set<string>[] = [];
|
const components: Set<string>[] = [];
|
||||||
|
|
||||||
@@ -165,21 +278,28 @@ const findConnectedComponents = (
|
|||||||
|
|
||||||
// 访问子节点
|
// 访问子节点
|
||||||
const children = parentToChildren.get(platform) || [];
|
const children = parentToChildren.get(platform) || [];
|
||||||
children.forEach(child => dfs(child, component));
|
children.forEach(child => {
|
||||||
|
// 只处理有效的子节点
|
||||||
|
if (child && typeof child === 'string' && child.trim().length > 0) {
|
||||||
|
dfs(child, component);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 访问父节点
|
// 访问父节点
|
||||||
const parent = childToParent.get(platform);
|
const parent = childToParent.get(platform);
|
||||||
if (parent && !visited.has(parent)) {
|
if (parent && !visited.has(parent) && typeof parent === 'string' && parent.trim().length > 0) {
|
||||||
dfs(parent, component);
|
dfs(parent, component);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 对每个未访问的节点启动一次DFS
|
// 对每个未访问的有效节点启动一次DFS
|
||||||
platforms.forEach(platform => {
|
validPlatforms.forEach(platform => {
|
||||||
if (!visited.has(platform)) {
|
if (!visited.has(platform)) {
|
||||||
const component = new Set<string>();
|
const component = new Set<string>();
|
||||||
dfs(platform, component);
|
dfs(platform, component);
|
||||||
components.push(component);
|
if (component.size > 0) {
|
||||||
|
components.push(component);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,18 +308,26 @@ const findConnectedComponents = (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 为连通分量确定根节点
|
* 为连通分量确定根节点
|
||||||
* 优先级:1. 包含"指挥"关键词 2. 入度为0的节点 3. 第一个节点
|
* 策略:选择入度为0的节点(没有父节点的节点),如果有多个则选择第一个
|
||||||
*/
|
*/
|
||||||
const determineRootNode = (platforms: string[]): string | null => {
|
const determineRootNode = (
|
||||||
if (platforms.length === 0) return null;
|
platforms: string[],
|
||||||
|
childToParent: Map<string, string>
|
||||||
|
): string | null => {
|
||||||
|
// 过滤掉undefined、null和空字符串
|
||||||
|
const validPlatforms = platforms.filter(p => p && typeof p === 'string' && p.trim().length > 0);
|
||||||
|
|
||||||
// 优先选择包含"指挥"的节点
|
if (validPlatforms.length === 0) return null;
|
||||||
const commandNode = platforms.find(p => p.includes('指挥'));
|
|
||||||
if (commandNode) return commandNode;
|
|
||||||
|
|
||||||
// 其次选择入度为0的节点(没有父节点)
|
// 优先选择入度为0的节点(没有父节点)
|
||||||
// 这里简化处理,直接返回第一个
|
const rootCandidates = validPlatforms.filter(platform => !childToParent.has(platform));
|
||||||
return platforms[0] || null;
|
|
||||||
|
if (rootCandidates.length > 0) {
|
||||||
|
return rootCandidates[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果所有节点都有父节点(可能是环形结构),返回第一个有效节点
|
||||||
|
return validPlatforms[0] || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,16 +372,16 @@ const computeNodeLevels = (
|
|||||||
const processed = new Set<string>();
|
const processed = new Set<string>();
|
||||||
const queue: Array<{ platform: string; level: number }> = [];
|
const queue: Array<{ platform: string; level: number }> = [];
|
||||||
|
|
||||||
// 分离根节点和子节点
|
// 找出所有入度为0的节点(没有父节点的节点),作为BFS的起点
|
||||||
const childPlatforms = platforms.filter(p => p !== rootPlatformName);
|
const rootNodes = platforms.filter(p => !childToParent.has(p));
|
||||||
|
|
||||||
// 第一层:直接与根节点相连的节点
|
console.log(` BFS起点(入度为0的节点): [${rootNodes.join(', ')}]`);
|
||||||
childPlatforms.forEach(platform => {
|
|
||||||
if (childToParent.get(platform) === rootPlatformName) {
|
// 将所有根节点加入队列,层级为0
|
||||||
levelMap.set(platform, 0);
|
rootNodes.forEach(root => {
|
||||||
processed.add(platform);
|
levelMap.set(root, 0);
|
||||||
queue.push({ platform, level: 0 });
|
processed.add(root);
|
||||||
}
|
queue.push({ platform: root, level: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
// BFS遍历后续层级
|
// BFS遍历后续层级
|
||||||
@@ -270,13 +398,23 @@ const computeNodeLevels = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理未连接的孤立节点
|
// 理论上不应该有未处理的节点,因为所有节点都应该从某个根节点可达
|
||||||
childPlatforms.forEach(platform => {
|
// 如果有,说明图中存在环或者数据有问题
|
||||||
if (!processed.has(platform)) {
|
const unprocessed = platforms.filter(p => !processed.has(p));
|
||||||
const maxLevel = Math.max(...Array.from(levelMap.values()), -1);
|
if (unprocessed.length > 0) {
|
||||||
levelMap.set(platform, maxLevel + 1);
|
console.warn(` 警告:以下节点未被BFS遍历到(可能存在环或数据问题): [${unprocessed.join(', ')}]`);
|
||||||
}
|
// 给这些节点分配一个合理的层级(基于它们的父节点)
|
||||||
});
|
unprocessed.forEach(platform => {
|
||||||
|
const parent = childToParent.get(platform);
|
||||||
|
if (parent && levelMap.has(parent)) {
|
||||||
|
levelMap.set(platform, levelMap.get(parent)! + 1);
|
||||||
|
} else {
|
||||||
|
// 如果连父节点都没有,放到最后一层
|
||||||
|
const maxLevel = Math.max(...Array.from(levelMap.values()), 0);
|
||||||
|
levelMap.set(platform, maxLevel + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return levelMap;
|
return levelMap;
|
||||||
};
|
};
|
||||||
@@ -299,25 +437,30 @@ const groupNodesByLevel = (levelMap: Map<string, number>): Map<number, string[]>
|
|||||||
const calculateNodePositions = (
|
const calculateNodePositions = (
|
||||||
levelsMap: Map<number, string[]>,
|
levelsMap: Map<number, string[]>,
|
||||||
parentToChildren: Map<string, string[]>,
|
parentToChildren: Map<string, string[]>,
|
||||||
params: LayoutParams
|
childToParent: Map<string, string>,
|
||||||
|
params: LayoutParams,
|
||||||
|
mapper: CoordinateMapper
|
||||||
): Map<string, NodePosition> => {
|
): Map<string, NodePosition> => {
|
||||||
const nodePositions = new Map<string, NodePosition>();
|
const nodePositions = new Map<string, NodePosition>();
|
||||||
const subtreeRanges = new Map<string, SubtreeRange>();
|
const subtreeRanges = new Map<string, SubtreeRange>();
|
||||||
|
|
||||||
// 第一步:预计算每列需要的总高度
|
// 第一步:预计算每列需要的总高度
|
||||||
precomputeColumnHeights(levelsMap, parentToChildren, params.MIN_ROW_SPACING, subtreeRanges);
|
precomputeColumnHeights(levelsMap, parentToChildren, params.SIBLING_SPACING, subtreeRanges, mapper);
|
||||||
|
|
||||||
// 第二步:初步分配位置
|
// 第二步:初步分配位置
|
||||||
initialPositionAssignment(levelsMap, parentToChildren, params, subtreeRanges, nodePositions);
|
initialPositionAssignment(levelsMap, parentToChildren, params, subtreeRanges, nodePositions, mapper);
|
||||||
|
|
||||||
// 第三步:调整父节点使其位于子节点中心
|
// 第三步:调整父节点使其位于子节点中心(处理共享子节点情况)
|
||||||
adjustParentsToCenterOfChildren(levelsMap, parentToChildren, nodePositions);
|
adjustParentsToCenterOfChildren(levelsMap, parentToChildren, childToParent, nodePositions, mapper);
|
||||||
|
|
||||||
// 第四步:同级节点防重叠检测与修正
|
// 第四步:全局防重叠检测与修正(跨所有层级)
|
||||||
preventOverlapsWithinSameLevel(levelsMap, parentToChildren, params.MIN_ROW_SPACING, nodePositions);
|
preventGlobalOverlaps(parentToChildren, params.SIBLING_SPACING, nodePositions, mapper);
|
||||||
|
|
||||||
// 第五步:重新调整父节点恢复等腰三角形
|
// 第五步:重新调整父节点恢复等腰三角形
|
||||||
restoreIsoscelesTriangles(levelsMap, parentToChildren, nodePositions);
|
restoreIsoscelesTriangles(levelsMap, parentToChildren, nodePositions, mapper);
|
||||||
|
|
||||||
|
// 第六步:最终全局防重叠检查
|
||||||
|
preventGlobalOverlaps(parentToChildren, params.SIBLING_SPACING, nodePositions, mapper);
|
||||||
|
|
||||||
return nodePositions;
|
return nodePositions;
|
||||||
};
|
};
|
||||||
@@ -326,90 +469,68 @@ const calculateNodePositions = (
|
|||||||
const precomputeColumnHeights = (
|
const precomputeColumnHeights = (
|
||||||
levelsMap: Map<number, string[]>,
|
levelsMap: Map<number, string[]>,
|
||||||
parentToChildren: Map<string, string[]>,
|
parentToChildren: Map<string, string[]>,
|
||||||
minRowSpacing: number,
|
siblingSpacing: number,
|
||||||
subtreeRanges: Map<string, SubtreeRange>
|
subtreeRanges: Map<string, SubtreeRange>,
|
||||||
|
mapper: CoordinateMapper
|
||||||
): void => {
|
): void => {
|
||||||
const sortedLevels = Array.from(levelsMap.keys()).sort((a, b) => b - a);
|
const sortedLevels = Array.from(levelsMap.keys()).sort((a, b) => b - a);
|
||||||
|
|
||||||
sortedLevels.forEach(level => {
|
sortedLevels.forEach(level => {
|
||||||
const platformsInLevel = levelsMap.get(level)!;
|
const platformsInLevel = levelsMap.get(level)!;
|
||||||
let totalHeight = 0;
|
let totalSize = 0;
|
||||||
|
|
||||||
platformsInLevel.forEach(platform => {
|
platformsInLevel.forEach(platform => {
|
||||||
const children = parentToChildren.get(platform) || [];
|
const children = parentToChildren.get(platform) || [];
|
||||||
|
|
||||||
if (children.length > 0) {
|
if (children.length > 0) {
|
||||||
let maxSubtreeHeight = 0;
|
let maxSubtreeSize = 0;
|
||||||
|
|
||||||
children.forEach(child => {
|
children.forEach(child => {
|
||||||
const childRange = subtreeRanges.get(child);
|
const childRange = subtreeRanges.get(child);
|
||||||
if (childRange) {
|
if (childRange) {
|
||||||
const childHeight = childRange.maxY - childRange.minY;
|
const range = mapper.getSubtreePrimaryCoord(childRange);
|
||||||
maxSubtreeHeight = Math.max(maxSubtreeHeight, childHeight);
|
const childSize = range.max - range.min;
|
||||||
|
maxSubtreeSize = Math.max(maxSubtreeSize, childSize);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const extraSpace = children.length > 1 ? (children.length - 1) * minRowSpacing : 0;
|
const subtreeSize = maxSubtreeSize;
|
||||||
const subtreeHeight = maxSubtreeHeight + extraSpace;
|
|
||||||
|
|
||||||
totalHeight += Math.max(minRowSpacing, subtreeHeight);
|
totalSize += Math.max(siblingSpacing, subtreeSize);
|
||||||
subtreeRanges.set(platform, {
|
subtreeRanges.set(platform, mapper.createSubtreeRange(
|
||||||
minY: totalHeight - subtreeHeight,
|
totalSize - subtreeSize,
|
||||||
maxY: totalHeight
|
totalSize
|
||||||
});
|
));
|
||||||
} else {
|
} else {
|
||||||
totalHeight += minRowSpacing;
|
totalSize += siblingSpacing;
|
||||||
subtreeRanges.set(platform, {
|
subtreeRanges.set(platform, mapper.createSubtreeRange(
|
||||||
minY: totalHeight - minRowSpacing,
|
totalSize - siblingSpacing,
|
||||||
maxY: totalHeight
|
totalSize
|
||||||
});
|
));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初步分配位置
|
// 初步分配位置(简化版:所有节点均匀分布,不考虑子树高度)
|
||||||
const initialPositionAssignment = (
|
const initialPositionAssignment = (
|
||||||
levelsMap: Map<number, string[]>,
|
levelsMap: Map<number, string[]>,
|
||||||
parentToChildren: Map<string, string[]>,
|
parentToChildren: Map<string, string[]>,
|
||||||
params: LayoutParams,
|
params: LayoutParams,
|
||||||
subtreeRanges: Map<string, SubtreeRange>,
|
subtreeRanges: Map<string, SubtreeRange>,
|
||||||
nodePositions: Map<string, NodePosition>
|
nodePositions: Map<string, NodePosition>,
|
||||||
|
mapper: CoordinateMapper
|
||||||
): void => {
|
): void => {
|
||||||
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
|
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
|
||||||
const columnOffsets = new Map<number, number>();
|
|
||||||
|
|
||||||
forwardLevels.forEach(level => {
|
forwardLevels.forEach(level => {
|
||||||
const platformsInLevel = levelsMap.get(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 => {
|
platformsInLevel.forEach((platform, index) => {
|
||||||
const children = parentToChildren.get(platform) || [];
|
const primaryCoord = mapper.mapLevelToAxis(level);
|
||||||
let y: number;
|
const secondaryCoord = mapper.mapSiblingIndexToAxis(index, platformsInLevel.length);
|
||||||
|
nodePositions.set(platform, mapper.setCoords(primaryCoord, secondaryCoord));
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -417,10 +538,13 @@ const initialPositionAssignment = (
|
|||||||
const adjustParentsToCenterOfChildren = (
|
const adjustParentsToCenterOfChildren = (
|
||||||
levelsMap: Map<number, string[]>,
|
levelsMap: Map<number, string[]>,
|
||||||
parentToChildren: Map<string, string[]>,
|
parentToChildren: Map<string, string[]>,
|
||||||
nodePositions: Map<string, NodePosition>
|
childToParent: Map<string, string>,
|
||||||
|
nodePositions: Map<string, NodePosition>,
|
||||||
|
mapper: CoordinateMapper
|
||||||
): void => {
|
): void => {
|
||||||
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
|
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
// 从最底层向上遍历,确保子节点位置已经确定后再调整父节点
|
||||||
[...forwardLevels].reverse().forEach(level => {
|
[...forwardLevels].reverse().forEach(level => {
|
||||||
const platformsInLevel = levelsMap.get(level)!;
|
const platformsInLevel = levelsMap.get(level)!;
|
||||||
|
|
||||||
@@ -431,64 +555,78 @@ const adjustParentsToCenterOfChildren = (
|
|||||||
const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean);
|
const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean);
|
||||||
|
|
||||||
if (childPositions.length > 0) {
|
if (childPositions.length > 0) {
|
||||||
const avgChildY = childPositions.reduce((sum, pos) => sum + pos!.y, 0) / childPositions.length;
|
// 计算所有子节点的平均次轴坐标(垂直布局时是X,水平布局时是Y)
|
||||||
|
const avgSecondaryCoord = childPositions.reduce((sum, pos) =>
|
||||||
|
sum + mapper.getSecondaryCoord(pos!), 0) / childPositions.length;
|
||||||
|
|
||||||
|
// 将父节点的次轴坐标移动到子节点的中心
|
||||||
const pos = nodePositions.get(platform)!;
|
const pos = nodePositions.get(platform)!;
|
||||||
pos.y = avgChildY;
|
const primaryCoord = mapper.getPrimaryCoord(pos);
|
||||||
nodePositions.set(platform, pos);
|
nodePositions.set(platform, mapper.setCoords(primaryCoord, avgSecondaryCoord));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 防重叠检测与修正(迭代式,确保完全无重叠)
|
// 全局防重叠检测与修正(跨所有层级,迭代式确保完全无重叠)
|
||||||
const preventOverlapsWithinSameLevel = (
|
const preventGlobalOverlaps = (
|
||||||
levelsMap: Map<number, string[]>,
|
|
||||||
parentToChildren: Map<string, string[]>,
|
parentToChildren: Map<string, string[]>,
|
||||||
minRowSpacing: number,
|
siblingSpacing: number,
|
||||||
nodePositions: Map<string, NodePosition>
|
nodePositions: Map<string, NodePosition>,
|
||||||
|
mapper: CoordinateMapper
|
||||||
): void => {
|
): void => {
|
||||||
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
|
// 收集所有节点并按主轴坐标分组(同层的节点)
|
||||||
|
const nodesByLevel = new Map<number, Array<{ platform: string; pos: NodePosition }>>();
|
||||||
|
|
||||||
forwardLevels.forEach(level => {
|
nodePositions.forEach((pos, platform) => {
|
||||||
const platformsInLevel = levelsMap.get(level)!;
|
const primaryCoord = mapper.getPrimaryCoord(pos);
|
||||||
|
if (!nodesByLevel.has(primaryCoord)) {
|
||||||
// 迭代检测直到没有重叠
|
nodesByLevel.set(primaryCoord, []);
|
||||||
|
}
|
||||||
|
nodesByLevel.get(primaryCoord)!.push({ platform, pos });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 对每一层进行防重叠处理
|
||||||
|
nodesByLevel.forEach(nodes => {
|
||||||
let hasOverlap = true;
|
let hasOverlap = true;
|
||||||
let iterations = 0;
|
let iterations = 0;
|
||||||
const maxIterations = 20; // 防止无限循环
|
const maxIterations = 30; // 防止无限循环
|
||||||
|
|
||||||
while (hasOverlap && iterations < maxIterations) {
|
while (hasOverlap && iterations < maxIterations) {
|
||||||
hasOverlap = false;
|
hasOverlap = false;
|
||||||
iterations++;
|
iterations++;
|
||||||
|
|
||||||
// 每次都重新按Y坐标排序
|
// 按次轴坐标排序
|
||||||
const sortedByY = platformsInLevel
|
nodes.sort((a, b) => mapper.getSecondaryCoord(a.pos) - mapper.getSecondaryCoord(b.pos));
|
||||||
.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++) {
|
for (let i = 1; i < nodes.length; i++) {
|
||||||
const prev = sortedByY[i - 1];
|
const prev = nodes[i - 1];
|
||||||
const curr = sortedByY[i];
|
const curr = nodes[i];
|
||||||
|
|
||||||
if (!prev || !curr) continue;
|
if (!prev || !curr) continue;
|
||||||
|
|
||||||
const actualDistance = curr.pos.y - prev.pos.y;
|
const actualDistance = mapper.getSecondaryCoord(curr.pos) - mapper.getSecondaryCoord(prev.pos);
|
||||||
|
|
||||||
if (actualDistance < minRowSpacing) {
|
if (actualDistance < siblingSpacing) {
|
||||||
hasOverlap = true;
|
hasOverlap = true;
|
||||||
|
|
||||||
const offset = minRowSpacing - actualDistance;
|
const offset = siblingSpacing - actualDistance;
|
||||||
curr.pos.y += offset;
|
const currSecondary = mapper.getSecondaryCoord(curr.pos);
|
||||||
|
const currPrimary = mapper.getPrimaryCoord(curr.pos);
|
||||||
|
curr.pos = mapper.setCoords(currPrimary, currSecondary + offset);
|
||||||
|
|
||||||
// 递归调整所有后代节点
|
// 递归调整所有后代节点
|
||||||
const adjustDescendants = (platform: string, yOffset: number) => {
|
const adjustDescendants = (platform: string, secondaryOffset: number) => {
|
||||||
const children = parentToChildren.get(platform) || [];
|
const children = parentToChildren.get(platform) || [];
|
||||||
children.forEach(child => {
|
children.forEach(child => {
|
||||||
const childPos = nodePositions.get(child);
|
const childPos = nodePositions.get(child);
|
||||||
if (childPos) {
|
if (childPos) {
|
||||||
childPos.y += yOffset;
|
const childPrimary = mapper.getPrimaryCoord(childPos);
|
||||||
adjustDescendants(child, yOffset);
|
const childSecondary = mapper.getSecondaryCoord(childPos);
|
||||||
|
const newPos = mapper.setCoords(childPrimary, childSecondary + secondaryOffset);
|
||||||
|
nodePositions.set(child, newPos);
|
||||||
|
adjustDescendants(child, secondaryOffset);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -502,7 +640,7 @@ const preventOverlapsWithinSameLevel = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (iterations >= maxIterations) {
|
if (iterations >= maxIterations) {
|
||||||
console.warn(`Level ${level}: 防重叠达到最大迭代次数(${maxIterations}),可能存在未解决的重叠`);
|
console.warn(`某层防重叠达到最大迭代次数(${maxIterations}),可能存在未解决的重叠`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -511,7 +649,8 @@ const preventOverlapsWithinSameLevel = (
|
|||||||
const restoreIsoscelesTriangles = (
|
const restoreIsoscelesTriangles = (
|
||||||
levelsMap: Map<number, string[]>,
|
levelsMap: Map<number, string[]>,
|
||||||
parentToChildren: Map<string, string[]>,
|
parentToChildren: Map<string, string[]>,
|
||||||
nodePositions: Map<string, NodePosition>
|
nodePositions: Map<string, NodePosition>,
|
||||||
|
mapper: CoordinateMapper
|
||||||
): void => {
|
): void => {
|
||||||
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
|
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
|
||||||
|
|
||||||
@@ -525,10 +664,11 @@ const restoreIsoscelesTriangles = (
|
|||||||
const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean);
|
const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean);
|
||||||
|
|
||||||
if (childPositions.length > 0) {
|
if (childPositions.length > 0) {
|
||||||
const avgChildY = childPositions.reduce((sum, pos) => sum + pos!.y, 0) / childPositions.length;
|
const avgSecondaryCoord = childPositions.reduce((sum, pos) =>
|
||||||
|
sum + mapper.getSecondaryCoord(pos!), 0) / childPositions.length;
|
||||||
const pos = nodePositions.get(platform)!;
|
const pos = nodePositions.get(platform)!;
|
||||||
pos.y = avgChildY;
|
const primaryCoord = mapper.getPrimaryCoord(pos);
|
||||||
nodePositions.set(platform, pos);
|
nodePositions.set(platform, mapper.setCoords(primaryCoord, avgSecondaryCoord));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -594,32 +734,36 @@ const createNodeObjects = (
|
|||||||
const createRootNode = (
|
const createRootNode = (
|
||||||
nodeMap: Map<string, GraphTaskElement>,
|
nodeMap: Map<string, GraphTaskElement>,
|
||||||
rootPlatformName: string,
|
rootPlatformName: string,
|
||||||
parentToChildren: Map<string, string[]>
|
parentToChildren: Map<string, string[]>,
|
||||||
|
params: LayoutParams,
|
||||||
|
mapper: CoordinateMapper
|
||||||
): void => {
|
): void => {
|
||||||
if (nodeMap.size === 0 || !rootPlatformName) return;
|
if (nodeMap.size === 0 || !rootPlatformName) return;
|
||||||
|
|
||||||
const directChildren = parentToChildren.get(rootPlatformName) || [];
|
const directChildren = parentToChildren.get(rootPlatformName) || [];
|
||||||
|
|
||||||
let centerY: number;
|
let centerSecondaryCoord: number;
|
||||||
if (directChildren.length > 0) {
|
if (directChildren.length > 0) {
|
||||||
const childYPositions = directChildren.map(child => {
|
const childSecondaryPositions = directChildren.map(child => {
|
||||||
const childNode = nodeMap.get(child);
|
const childNode = nodeMap.get(child);
|
||||||
return childNode ? childNode.position.y : 0;
|
return childNode ? mapper.getSecondaryCoord(childNode.position) : 0;
|
||||||
}).filter(y => y > 0);
|
}).filter(coord => coord > 0);
|
||||||
|
|
||||||
if (childYPositions.length > 0) {
|
if (childSecondaryPositions.length > 0) {
|
||||||
centerY = childYPositions.reduce((sum, y) => sum + y, 0) / childYPositions.length;
|
centerSecondaryCoord = childSecondaryPositions.reduce((sum, coord) => sum + coord, 0) / childSecondaryPositions.length;
|
||||||
} else {
|
} else {
|
||||||
const allNodes = Array.from(nodeMap.values());
|
const allNodes = Array.from(nodeMap.values());
|
||||||
const minY = Math.min(...allNodes.map(n => n.position.y));
|
const coords = allNodes.map(n => mapper.getSecondaryCoord(n.position));
|
||||||
const maxY = Math.max(...allNodes.map(n => n.position.y));
|
const minCoord = Math.min(...coords);
|
||||||
centerY = (minY + maxY) / 2;
|
const maxCoord = Math.max(...coords);
|
||||||
|
centerSecondaryCoord = (minCoord + maxCoord) / 2;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const allNodes = Array.from(nodeMap.values());
|
const allNodes = Array.from(nodeMap.values());
|
||||||
const minY = Math.min(...allNodes.map(n => n.position.y));
|
const coords = allNodes.map(n => mapper.getSecondaryCoord(n.position));
|
||||||
const maxY = Math.max(...allNodes.map(n => n.position.y));
|
const minCoord = Math.min(...coords);
|
||||||
centerY = (minY + maxY) / 2;
|
const maxCoord = Math.max(...coords);
|
||||||
|
centerSecondaryCoord = (minCoord + maxCoord) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootNode: GraphTaskElement = {
|
const rootNode: GraphTaskElement = {
|
||||||
@@ -644,10 +788,7 @@ const createRootNode = (
|
|||||||
group: null,
|
group: null,
|
||||||
description: rootPlatformName,
|
description: rootPlatformName,
|
||||||
order: 0,
|
order: 0,
|
||||||
position: {
|
position: mapper.setCoords(params.ROOT_POSITION, Math.round(centerSecondaryCoord)),
|
||||||
x: 50, // ROOT_X
|
|
||||||
y: Math.round(centerY),
|
|
||||||
},
|
|
||||||
width: 250,
|
width: 250,
|
||||||
height: 145,
|
height: 145,
|
||||||
inputs: null,
|
inputs: null,
|
||||||
@@ -895,12 +1036,14 @@ const createStarConnection = (
|
|||||||
* 便捷函数:直接将平台列表转换为图容器
|
* 便捷函数:直接将平台列表转换为图容器
|
||||||
* @param platforms 平台列表
|
* @param platforms 平台列表
|
||||||
* @param strategy 推断策略
|
* @param strategy 推断策略
|
||||||
|
* @param layoutConfig 布局配置(可选)
|
||||||
* @returns 图容器对象
|
* @returns 图容器对象
|
||||||
*/
|
*/
|
||||||
export const convertPlatformsToGraphContainer = (
|
export const convertPlatformsToGraphContainer = (
|
||||||
platforms: PlatformWithComponents[],
|
platforms: PlatformWithComponents[],
|
||||||
strategy: InferenceStrategy = 'by-name-pattern'
|
strategy: InferenceStrategy = 'by-name-pattern',
|
||||||
|
layoutConfig?: Partial<LayoutConfig>
|
||||||
): GraphContainer => {
|
): GraphContainer => {
|
||||||
const records = convertPlatformsToCommunicationRecords(platforms, strategy);
|
const records = convertPlatformsToCommunicationRecords(platforms, strategy);
|
||||||
return convertRecordsToGraphContainer(records);
|
return convertRecordsToGraphContainer(records, layoutConfig);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,3 +48,22 @@ export interface ScenarioDetailsResponse extends ApiDataResponse<Scenario> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通信关系记录(对应数据库表结构)
|
||||||
|
*/
|
||||||
|
export interface CommunicationRelationRecord {
|
||||||
|
id: number;
|
||||||
|
command_platform: string; // 指挥平台名称
|
||||||
|
subordinate_platform: string; // 下属平台名称
|
||||||
|
command_comm?: string; // 指挥端通信方式
|
||||||
|
subordinate_comm?: string; // 下属端通信方式
|
||||||
|
scenary_id?: number; // 场景ID
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取场景所有通信关系的响应
|
||||||
|
*/
|
||||||
|
export interface CommunicationRelationsResponse extends ApiDataResponse<CommunicationRelationRecord[]> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user