diff --git a/AGENTS.md b/AGENTS.md index e8ddaaf..bb7ad76 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -309,20 +309,89 @@ Visibility="{Binding HasItems, Converter={StaticResource BoolToVisibilityConvert ``` **项目中已定义的资源(AnimationControlView.xaml)**: + | 资源名 | 类型 | 说明 | |--------|------|------| | `BoolToVisibilityConverter` | BoolToVisibilityConverter | 布尔转可见性,支持Inverse参数 | -**检查步骤**: -1. 添加XAML代码时,检查所有`{StaticResource xxx}`引用 -2. 确认资源在文件顶部``或父级资源字典中已定义 -3. 不确定时,在项目中搜索该资源名确认存在 -4. 避免复制其他项目/文件的代码直接使用(资源可能不同) +**XAML开发工作流程(必须遵守)**: -**常见错误模式**: -- `InverseBoolToVisibilityConverter` → 改用 `BoolToVisibilityConverter` + `ConverterParameter=Inverse` -- `VisibilityConverter` → 改用 `BoolToVisibilityConverter` -- 自定义Style名称拼写错误 → 检查Resources中的定义 +1. **每添加一个资源引用,立即确认其存在** + + ``` + ❌ 错误做法: + - 复制其他文件的XAML代码 + - 一次性写大量XAML再测试 + + ✅ 正确做法: + - 每写一行 {StaticResource xxx},立即检查 xxx 是否已定义 + - 使用 Ctrl+F 搜索 xxx 确认在当前文件或合并字典中存在 + ``` + +2. **资源定义位置优先级**: + - 本文件 `` 或 `` 内定义 + - 引用的外部资源字典(如 `NavisworksStyles.xaml`) + - 必须在 XAML 顶部检查 `MergedDictionaries` 是否正确合并 + +3. **新增XAML文件时必做检查清单**: + + ``` + □ 所有 {StaticResource xxx} 引用都有定义 + □ 所有 xmlns:local 命名空间映射正确 + □ 所有 ValueConverter 在 xaml.cs 中已实现 + □ 合并的资源字典路径正确(pack://application:,,,/...) + □ 编译通过后立即运行测试,验证窗口能正常打开 + ``` + +4. **常见错误模式与修正**: + + | 错误写法 | 问题 | 正确写法 | + |---------|------|---------| + | `InverseBoolToVisibilityConverter` | 未定义 | `BoolToVisibilityConverter` + `ConverterParameter=Inverse` | + | `VisibilityConverter` | 未定义 | `BoolToVisibilityConverter` | + | `BooleanToVisibilityConverter` (键名) | 引用时使用 `BoolToVisibilityConverter` | 键名和引用保持一致 | + | 拼写错误的Style名 | 找不到资源 | 检查资源字典中的精确定义 | + +5. **调试XAML资源错误**: + - 错误信息:`无法在资源字典中找到名为 xxx 的资源` + - 定位方法:从堆栈跟踪找到最后加载的XAML元素 + - 快速修复:临时注释掉报错元素,添加简化版本测试 + - 预防措施:小步增量开发,每步验证窗口可正常显示 + +#### AI助手强制执行机制(防止再次犯错) + +**写XAML前,必须大声说出以下承诺:** +> "我承诺:本次XAML开发将严格遵守渐进式验证原则,每添加一个资源引用立即验证,绝不一次性写大量代码。" + +**编写过程中的强制检查点:** + +| 步骤 | 强制动作 | 验证命令/方法 | +|-----|---------|--------------| +| 1. 写完XAML结构 | 列出所有 `{StaticResource xxx}` | `grep -n "StaticResource" File.xaml` | +| 2. 检查每个引用 | 在文件中搜索资源定义 | 确认 `x:Key="xxx"` 存在 | +| 3. 检查Converter | 确认 xaml.cs 中有实现 | `grep "class.*Converter.*:" File.xaml.cs` | +| 4. 首次编译 | 必须通过 | `compile.bat` | +| 5. 运行时验证 | 窗口能正常打开无异常 | 实际运行测试 | + +**如果违反规则的后果:** + +- 必须立即停止当前工作 +- 回滚到上一个可运行的版本 +- 重新开始,严格按照检查点执行 + +**自检话术(每次保存前自问):** + +``` +□ 我是否复制了其他文件的XAML代码而没有检查资源? +□ 我是否一次性写了超过50行XAML才测试? +□ 我能否确定每个 {StaticResource} 都在当前文件或合并字典中定义? +□ 如果现在运行,窗口会崩溃吗? +``` + +**历史错误记录(警示):** + +- 2025-02-14: PathAnalysisDialog.xaml 使用了 `BoolToVisibilityConverter` 但定义的是 `BooleanToVisibilityConverter`,导致窗口崩溃 +- 根本原因:没有逐条执行检查清单,过度自信 #### UI色彩规范 - Material Design @@ -347,6 +416,7 @@ Visibility="{Binding HasItems, Converter={StaticResource BoolToVisibilityConvert | 通道预览 | 绿色 | Color.Green | **使用规范**: + 1. 新增UI元素时优先使用上述Material色系 2. 如需新颜色,参考 [Material Design Color Palette](https://material.io/resources/color/) 3. 使用 `Color.FromByteRGB(r, g, b)` 定义颜色(Navisworks API) diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj deleted file mode 100644 index 39d5249..0000000 --- a/NavisworksTransport.UnitTests.csproj +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - Debug - AnyCPU - {2B5F1A8D-3CEB-4154-8761-F568AD9393FF} - Library - Properties - NavisworksTransport.UnitTests - NavisworksTransport.UnitTests - v4.8 - 512 - true - true - - - - true - full - false - bin\x64\Debug\ - DEBUG;TRACE - prompt - 4 - x64 - - - - pdbonly - true - bin\x64\Release\ - TRACE - prompt - 4 - x64 - - - - - - ..\..\..\..\Program Files\Autodesk\Navisworks Manage 2026\Autodesk.Navisworks.Api.dll - False - - - - - - - - - - - - - - - - - - - - - - - - - packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - True - - - packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll - True - - - - - - - bin\x64\Debug\NavisworksTransportPlugin.dll - True - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/NavisworksTransportPlugin.csproj b/NavisworksTransportPlugin.csproj index 1efdb79..c0f08a9 100644 --- a/NavisworksTransportPlugin.csproj +++ b/NavisworksTransportPlugin.csproj @@ -114,7 +114,10 @@ + + + diff --git a/UnitTests/Collections/ThreadSafeObservableCollectionBasicTests.cs b/UnitTests/Collections/ThreadSafeObservableCollectionBasicTests.cs deleted file mode 100644 index f580fdd..0000000 --- a/UnitTests/Collections/ThreadSafeObservableCollectionBasicTests.cs +++ /dev/null @@ -1,284 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace NavisworksTransport.UnitTests -{ - /// - /// ThreadSafeObservableCollection基础功能测试 - /// 只包含可以在控制台环境下稳定运行的纯数据操作测试,不涉及事件 - /// - [TestClass] - public class ThreadSafeObservableCollectionBasicTests - { - private ThreadSafeObservableCollection _collection; - - [TestInitialize] - public void Setup() - { - _collection = new ThreadSafeObservableCollection(); - } - - [TestCleanup] - public void Cleanup() - { - _collection = null; - } - - #region 构造函数测试 - - [TestMethod] - public void Constructor_ShouldCreateEmptyCollection() - { - // Arrange & Act - var collection = new ThreadSafeObservableCollection(); - - // Assert - Assert.AreEqual(0, collection.Count, "新创建的集合应该为空"); - } - - [TestMethod] - public void Constructor_WithInitialItems_ShouldContainAllItems() - { - // Arrange - var initialItems = new[] { "A", "B", "C" }; - - // Act - var collection = new ThreadSafeObservableCollection(initialItems); - - // Assert - Assert.AreEqual(3, collection.Count, "集合应该包含所有初始元素"); - var snapshot = collection.ToSnapshot(); - CollectionAssert.AreEqual(initialItems, snapshot, "集合内容应该与初始元素相同"); - } - - #endregion - - #region 基础数据操作测试 - - [TestMethod] - public void Add_ShouldIncreaseCount() - { - // Arrange - var item = "TestItem"; - - // Act - _collection.Add(item); - - // Assert - Assert.AreEqual(1, _collection.Count, "添加元素后集合计数应该增加"); - Assert.IsTrue(_collection.Contains(item), "集合应该包含添加的元素"); - } - - [TestMethod] - public void Remove_ShouldDecreaseCount() - { - // Arrange - var item = "TestItem"; - _collection.Add(item); - - // Act - var result = _collection.Remove(item); - - // Assert - Assert.IsTrue(result, "移除已存在的元素应该返回true"); - Assert.AreEqual(0, _collection.Count, "移除元素后集合计数应该减少"); - Assert.IsFalse(_collection.Contains(item), "集合不应该包含已移除的元素"); - } - - [TestMethod] - public void Insert_ShouldInsertAtCorrectPosition() - { - // Arrange - _collection.Add("A"); - _collection.Add("C"); - - // Act - _collection.Insert(1, "B"); - - // Assert - Assert.AreEqual(3, _collection.Count, "插入元素后集合计数应该增加"); - Assert.AreEqual("B", _collection[1], "元素应该被插入到正确位置"); - var snapshot = _collection.ToSnapshot(); - CollectionAssert.AreEqual(new[] { "A", "B", "C" }, snapshot, "集合顺序应该正确"); - } - - [TestMethod] - public void Clear_ShouldRemoveAllItems() - { - // Arrange - _collection.Add("A"); - _collection.Add("B"); - _collection.Add("C"); - - // Act - _collection.Clear(); - - // Assert - Assert.AreEqual(0, _collection.Count, "清空后集合应该为空"); - } - - #endregion - - #region 批量操作测试 - - [TestMethod] - public void AddRange_ShouldAddAllItems() - { - // Arrange - var items = new[] { "A", "B", "C" }; - - // Act - _collection.AddRange(items); - - // Assert - Assert.AreEqual(3, _collection.Count, "AddRange应该添加所有元素"); - var snapshot = _collection.ToSnapshot(); - CollectionAssert.AreEqual(items, snapshot, "集合内容应该与添加的元素相同"); - } - - [TestMethod] - public void AddRange_WithEmptyCollection_ShouldNotChangeCount() - { - // Arrange - var emptyItems = new string[0]; - - // Act - _collection.AddRange(emptyItems); - - // Assert - Assert.AreEqual(0, _collection.Count, "添加空集合不应该改变计数"); - } - - [TestMethod] - public void RemoveRange_ShouldRemoveMatchingItems() - { - // Arrange - _collection.AddRange(new[] { "A", "B", "C", "D" }); - var itemsToRemove = new[] { "B", "D" }; - - // Act - var removedCount = _collection.RemoveRange(itemsToRemove); - - // Assert - Assert.AreEqual(2, removedCount, "应该移除2个元素"); - Assert.AreEqual(2, _collection.Count, "移除后集合应该包含2个元素"); - var snapshot = _collection.ToSnapshot(); - CollectionAssert.AreEqual(new[] { "A", "C" }, snapshot, "剩余元素应该正确"); - } - - [TestMethod] - public void ClearAndAddRange_ShouldReplaceAllItems() - { - // Arrange - _collection.AddRange(new[] { "Old1", "Old2" }); - var newItems = new[] { "New1", "New2", "New3" }; - - // Act - _collection.ClearAndAddRange(newItems); - - // Assert - Assert.AreEqual(3, _collection.Count, "替换后集合应该包含新元素的数量"); - var snapshot = _collection.ToSnapshot(); - CollectionAssert.AreEqual(newItems, snapshot, "集合内容应该是新元素"); - } - - #endregion - - #region 查询操作测试 - - [TestMethod] - public void FirstOrDefault_ShouldReturnCorrectItem() - { - // Arrange - _collection.AddRange(new[] { "Apple", "Banana", "Cherry" }); - - // Act - var result = _collection.FirstOrDefault(item => item.StartsWith("B")); - - // Assert - Assert.AreEqual("Banana", result, "FirstOrDefault应该返回第一个匹配的元素"); - } - - [TestMethod] - public void Count_WithPredicate_ShouldReturnCorrectCount() - { - // Arrange - _collection.AddRange(new[] { "Apple", "Banana", "Apricot", "Cherry" }); - - // Act - var count = _collection.Count(item => item.StartsWith("A")); - - // Assert - Assert.AreEqual(2, count, "Count应该返回满足条件的元素数量"); - } - - [TestMethod] - public void Any_WithPredicate_ShouldReturnCorrectResult() - { - // Arrange - _collection.AddRange(new[] { "Apple", "Banana", "Cherry" }); - - // Act - var hasZ = _collection.Any(item => item.StartsWith("Z")); - var hasA = _collection.Any(item => item.StartsWith("A")); - - // Assert - Assert.IsFalse(hasZ, "不存在以Z开头的元素时Any应该返回false"); - Assert.IsTrue(hasA, "存在以A开头的元素时Any应该返回true"); - } - - [TestMethod] - public void ToSnapshot_ShouldReturnCurrentState() - { - // Arrange - _collection.AddRange(new[] { "A", "B", "C" }); - - // Act - var snapshot1 = _collection.ToSnapshot(); - _collection.Add("D"); - var snapshot2 = _collection.ToSnapshot(); - - // Assert - CollectionAssert.AreEqual(new[] { "A", "B", "C" }, snapshot1, "第一个快照应该不包含后添加的元素"); - CollectionAssert.AreEqual(new[] { "A", "B", "C", "D" }, snapshot2, "第二个快照应该包含后添加的元素"); - } - - #endregion - - #region 异常处理测试 - - [TestMethod] - public void Add_WithNull_ShouldThrowArgumentNullException() - { - // Act & Assert - Assert.ThrowsException(() => - { - _collection.Add(null); - }, "添加null元素应该抛出ArgumentNullException"); - } - - [TestMethod] - public void AddRange_WithNull_ShouldThrowArgumentNullException() - { - // Act & Assert - Assert.ThrowsException(() => - { - _collection.AddRange(null); - }, "AddRange传入null应该抛出ArgumentNullException"); - } - - [TestMethod] - public void IndexAccess_WithInvalidIndex_ShouldThrowArgumentOutOfRangeException() - { - // Arrange - _collection.Add("OnlyItem"); - - // Act & Assert - Assert.ThrowsException(() => - { - var item = _collection[5]; // 无效索引 - }, "访问无效索引应该抛出ArgumentOutOfRangeException"); - } - - #endregion - } -} \ No newline at end of file diff --git a/UnitTests/Commands/CommandBaseTests.cs b/UnitTests/Commands/CommandBaseTests.cs deleted file mode 100644 index d0010c8..0000000 --- a/UnitTests/Commands/CommandBaseTests.cs +++ /dev/null @@ -1,428 +0,0 @@ -namespace NavisworksTransport.UnitTests.Commands -{ - /// - /// CommandBase抽象类的纯逻辑测试 - /// 通过创建测试实现类来测试基础功能,不依赖Navisworks环境 - /// - [TestClass] - public class CommandBaseTests - { - private TestCommand _testCommand; - - [TestInitialize] - public void SetUp() - { - _testCommand = new TestCommand(); - } - - [TestCleanup] - public void TearDown() - { - _testCommand?.Cancel(); - } - - #region 构造函数测试 - - [TestMethod] - public void Constructor_Default_SetsCorrectDefaults() - { - // Arrange & Act - var command = new TestCommand(); - - // Assert - Assert.IsNotNull(command.CommandId, "CommandId不应该为空"); - Assert.IsTrue(Guid.TryParse(command.CommandId, out _), "CommandId应该是有效的GUID"); - Assert.AreEqual("TestCommand", command.DisplayName, "DisplayName应该默认为类名"); - Assert.AreEqual("路径规划命令", command.Description, "Description应该有默认值"); - Assert.AreEqual(CommandExecutionStatus.NotStarted, command.Status, "初始状态应该为NotStarted"); - Assert.AreEqual(0, command.Progress, "初始进度应该为0"); - } - - [TestMethod] - public void Constructor_WithParameters_SetsCorrectValues() - { - // Arrange - string expectedCommandId = "test-command-123"; - string expectedDisplayName = "测试命令"; - string expectedDescription = "这是一个测试命令"; - - // Act - var command = new TestCommand(expectedCommandId, expectedDisplayName, expectedDescription); - - // Assert - Assert.AreEqual(expectedCommandId, command.CommandId, "CommandId应该设置为指定值"); - Assert.AreEqual(expectedDisplayName, command.DisplayName, "DisplayName应该设置为指定值"); - Assert.AreEqual(expectedDescription, command.Description, "Description应该设置为指定值"); - } - - [TestMethod] - public void Constructor_WithNullParameters_SetsDefaults() - { - // Arrange & Act - var command = new TestCommand(null, null, null); - - // Assert - Assert.IsNotNull(command.CommandId, "CommandId不应该为空"); - Assert.IsTrue(Guid.TryParse(command.CommandId, out _), "CommandId应该是有效的GUID"); - Assert.AreEqual("TestCommand", command.DisplayName, "DisplayName应该默认为类名"); - Assert.AreEqual("路径规划命令", command.Description, "Description应该有默认值"); - } - - #endregion - - #region 状态管理测试 - - [TestMethod] - public void Status_MultipleThreadsAccess_ThreadSafe() - { - // Arrange - var command = new TestCommand(); - var exceptions = new List(); - var tasks = new List(); - - // Act - 多线程并发访问Status - for (int i = 0; i < 10; i++) - { - tasks.Add(Task.Run(() => - { - try - { - for (int j = 0; j < 100; j++) - { - var status = command.Status; // 读取状态 - } - } - catch (Exception ex) - { - lock (exceptions) - { - exceptions.Add(ex); - } - } - })); - } - - Task.WaitAll(tasks.ToArray()); - - // Assert - Assert.AreEqual(0, exceptions.Count, "多线程访问Status不应该抛出异常"); - } - - [TestMethod] - public void Progress_MultipleThreadsAccess_ThreadSafe() - { - // Arrange - var command = new TestCommand(); - var exceptions = new List(); - var tasks = new List(); - - // Act - 多线程并发访问Progress - for (int i = 0; i < 10; i++) - { - tasks.Add(Task.Run(() => - { - try - { - for (int j = 0; j < 100; j++) - { - var progress = command.Progress; // 读取进度 - } - } - catch (Exception ex) - { - lock (exceptions) - { - exceptions.Add(ex); - } - } - })); - } - - Task.WaitAll(tasks.ToArray()); - - // Assert - Assert.AreEqual(0, exceptions.Count, "多线程访问Progress不应该抛出异常"); - } - - #endregion - - #region 事件测试 - - [TestMethod] - public void StatusChanged_EventRaised_WhenStatusChanges() - { - // Arrange - var command = new TestCommand(); - CommandStatusChangedEventArgs receivedEventArgs = null; - command.StatusChanged += (sender, args) => receivedEventArgs = args; - - // Act - command.SimulateStatusChangeEvent(CommandExecutionStatus.NotStarted, CommandExecutionStatus.Executing); - - // Assert - Assert.IsNotNull(receivedEventArgs, "StatusChanged事件应该被触发"); - Assert.AreEqual(CommandExecutionStatus.NotStarted, receivedEventArgs.OldStatus, "旧状态应该正确"); - Assert.AreEqual(CommandExecutionStatus.Executing, receivedEventArgs.NewStatus, "新状态应该正确"); - } - - [TestMethod] - public void ProgressChanged_EventRaised_WhenProgressChanges() - { - // Arrange - var command = new TestCommand(); - CommandProgressChangedEventArgs receivedEventArgs = null; - command.ProgressChanged += (sender, args) => receivedEventArgs = args; - - // Act - command.SimulateProgressChange(50, "进度50%"); - - // Assert - Assert.IsNotNull(receivedEventArgs, "ProgressChanged事件应该被触发"); - Assert.AreEqual(50, receivedEventArgs.Progress, "进度值应该正确"); - Assert.AreEqual("进度50%", receivedEventArgs.StatusMessage, "状态消息应该正确"); - } - - #endregion - - #region 执行测试 - - [TestMethod] - public async Task ExecuteAsync_Success_ReturnsSuccessResult() - { - // Arrange - var command = new TestCommand(); - command.SetShouldSucceed(true); - - // Act - var result = await command.ExecuteAsync(); - - // Assert - Assert.IsTrue(result.IsSuccess, "执行应该成功"); - Assert.AreEqual(CommandExecutionStatus.Completed, command.Status, "状态应该为Completed"); - Assert.AreEqual(100, command.Progress, "进度应该为100"); - } - - [TestMethod] - public async Task ExecuteAsync_Failure_ReturnsFailureResult() - { - // Arrange - var command = new TestCommand(); - command.SetShouldSucceed(false); - - // Act - var result = await command.ExecuteAsync(); - - // Assert - Assert.IsFalse(result.IsSuccess, "执行应该失败"); - Assert.AreEqual(CommandExecutionStatus.Failed, command.Status, "状态应该为Failed"); - } - - [TestMethod] - public async Task ExecuteAsync_ValidationFailure_ReturnsValidationFailure() - { - // Arrange - var command = new TestCommand(); - command.SetValidationShouldFail(true); - - // Act - var result = await command.ExecuteAsync(); - - // Assert - Assert.IsFalse(result.IsSuccess, "执行应该失败"); - Assert.AreEqual(CommandExecutionStatus.Failed, command.Status, "状态应该为Failed"); - Assert.IsTrue(result.ErrorMessage.Contains("验证失败"), "错误消息应该包含验证失败信息"); - } - - [TestMethod] - public async Task ExecuteAsync_CancellationRequested_ReturnsCancelledResult() - { - // Arrange - var command = new TestCommand(); - command.SetExecutionDelay(TimeSpan.FromSeconds(2)); // 设置较长的执行时间 - var cts = new CancellationTokenSource(); - - // Act - var executeTask = command.ExecuteAsync(cts.Token); - cts.CancelAfter(100); // 100ms后取消 - var result = await executeTask; - - // Assert - Assert.IsFalse(result.IsSuccess, "执行应该失败"); - Assert.AreEqual(CommandExecutionStatus.Cancelled, command.Status, "状态应该为Cancelled"); - Assert.IsTrue(result.ErrorMessage.Contains("取消"), "错误消息应该包含取消信息"); - } - - [TestMethod] - public async Task ExecuteAsync_AlreadyExecuting_ReturnsFailure() - { - // Arrange - var command = new TestCommand(); - command.SetExecutionDelay(TimeSpan.FromSeconds(1)); - - // Act - var task1 = command.ExecuteAsync(); - await Task.Delay(50); // 确保第一个任务开始执行 - var result2 = await command.ExecuteAsync(); // 尝试重复执行 - - // Assert - Assert.IsFalse(result2.IsSuccess, "重复执行应该失败"); - Assert.IsTrue(result2.ErrorMessage.Contains("正在执行中"), "错误消息应该指示命令正在执行"); - - // 等待第一个任务完成 - await task1; - } - - [TestMethod] - public void Cancel_WhileExecuting_CancelsExecution() - { - // Arrange - var command = new TestCommand(); - command.SetExecutionDelay(TimeSpan.FromSeconds(2)); - - // Act - var executeTask = command.ExecuteAsync(); - command.Cancel(); // 取消执行 - - // Assert - 由于是异步操作,我们只验证Cancel方法不抛出异常 - Assert.IsTrue(true, "Cancel方法应该能够正常调用"); - } - - #endregion - - #region CanExecute测试 - - [TestMethod] - public void CanExecute_DefaultImplementation_ReturnsSuccess() - { - // Arrange - var command = new TestCommand(); - - // Act - var result = command.CanExecute(); - - // Assert - Assert.IsTrue(result.IsSuccess, "默认的CanExecute应该返回成功"); - } - - [TestMethod] - public void CanExecute_WhileExecuting_ReturnsFailure() - { - // Arrange - var command = new TestCommand(); - command.SetExecutionDelay(TimeSpan.FromSeconds(1)); - - // Act - var executeTask = command.ExecuteAsync(); - var canExecuteResult = command.CanExecute(); - - // Assert - Assert.IsFalse(canExecuteResult.IsSuccess, "执行中的命令CanExecute应该返回失败"); - - // 清理 - command.Cancel(); - } - - #endregion - - #region 性能测试 - - [TestMethod] - public async Task ExecuteAsync_TracksElapsedTime() - { - // Arrange - var command = new TestCommand(); - command.SetExecutionDelay(TimeSpan.FromMilliseconds(100)); - - // Act - var result = await command.ExecuteAsync(); - - // Assert - Assert.IsTrue(result.ElapsedMilliseconds >= 90, "执行时间应该被正确记录"); // 允许一些时间误差 - Assert.IsTrue(result.ElapsedMilliseconds < 500, "执行时间不应该过长"); - } - - #endregion - } - - #region 测试用Command实现 - - /// - /// 用于测试的Command实现类 - /// - internal class TestCommand : CommandBase - { - private bool _shouldSucceed = true; - private bool _validationShouldFail = false; - private TimeSpan _executionDelay = TimeSpan.Zero; - - public TestCommand() : base() { } - - public TestCommand(string commandId, string displayName, string description) - : base(commandId, displayName, description) { } - - public void SetShouldSucceed(bool shouldSucceed) - { - _shouldSucceed = shouldSucceed; - } - - public void SetValidationShouldFail(bool shouldFail) - { - _validationShouldFail = shouldFail; - } - - public void SetExecutionDelay(TimeSpan delay) - { - _executionDelay = delay; - } - - // 暴露受保护的方法用于测试 - public void SimulateProgressChange(int progress, string message) - { - UpdateProgress(progress, message); - } - - public void SimulateStatusChangeEvent(CommandExecutionStatus oldStatus, CommandExecutionStatus newStatus) - { - OnStatusChanged(oldStatus, newStatus, "测试状态变化"); - } - - protected override async Task ValidateAsync(CancellationToken cancellationToken) - { - if (_validationShouldFail) - { - return PathPlanningResult.ValidationFailure("测试验证失败"); - } - return PathPlanningResult.Success("验证成功"); - } - - protected override async Task ExecuteInternalAsync(CancellationToken cancellationToken) - { - // 模拟执行延迟 - if (_executionDelay > TimeSpan.Zero) - { - await Task.Delay(_executionDelay, cancellationToken); - } - - // 模拟进度更新 - for (int i = 10; i <= 90; i += 20) - { - if (cancellationToken.IsCancellationRequested) - break; - - UpdateProgress(i, $"执行进度 {i}%"); - await Task.Delay(10, cancellationToken); // 短暂延迟以模拟工作 - } - - if (_shouldSucceed) - { - return PathPlanningResult.Success("测试执行成功"); - } - else - { - return PathPlanningResult.Failure("测试执行失败"); - } - } - } - - #endregion -} \ No newline at end of file diff --git a/UnitTests/Core/PathCurveEngineStandaloneTests.cs b/UnitTests/Core/PathCurveEngineStandaloneTests.cs deleted file mode 100644 index df0219a..0000000 --- a/UnitTests/Core/PathCurveEngineStandaloneTests.cs +++ /dev/null @@ -1,565 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace NavisworksTransport.UnitTests.Core -{ - #region 简单的几何类型(不依赖 Navisworks API) - - /// - /// 简单的 3D 点结构体(用于测试) - /// - public struct TestPoint3D : IEquatable - { - public double X { get; set; } - public double Y { get; set; } - public double Z { get; set; } - - public TestPoint3D(double x, double y, double z) - { - X = x; - Y = y; - Z = z; - } - - public static TestPoint3D operator +(TestPoint3D p, TestVector3D v) - { - return new TestPoint3D(p.X + v.X, p.Y + v.Y, p.Z + v.Z); - } - - public static TestPoint3D operator -(TestPoint3D p1, TestPoint3D p2) - { - return new TestPoint3D(p1.X - p2.X, p1.Y - p2.Y, p1.Z - p2.Z); - } - - public static TestPoint3D operator *(TestPoint3D p, double scalar) - { - return new TestPoint3D(p.X * scalar, p.Y * scalar, p.Z * scalar); - } - - public double Length - { - get { return Math.Sqrt(X * X + Y * Y + Z * Z); } - } - - public bool Equals(TestPoint3D other) - { - return Math.Abs(X - other.X) < 1e-10 && - Math.Abs(Y - other.Y) < 1e-10 && - Math.Abs(Z - other.Z) < 1e-10; - } - - public override bool Equals(object obj) - { - return obj is TestPoint3D && Equals((TestPoint3D)obj); - } - - public override int GetHashCode() - { - return X.GetHashCode() ^ Y.GetHashCode() ^ Z.GetHashCode(); - } - - public static bool operator ==(TestPoint3D p1, TestPoint3D p2) - { - return p1.Equals(p2); - } - - public static bool operator !=(TestPoint3D p1, TestPoint3D p2) - { - return !p1.Equals(p2); - } - } - - /// - /// 简单的 3D 向量结构体(用于测试) - /// - public struct TestVector3D - { - public double X { get; set; } - public double Y { get; set; } - public double Z { get; set; } - - public TestVector3D(double x, double y, double z) - { - X = x; - Y = y; - Z = z; - } - - public TestVector3D(TestPoint3D point) - { - X = point.X; - Y = point.Y; - Z = point.Z; - } - - public static TestVector3D operator +(TestVector3D v1, TestVector3D v2) - { - return new TestVector3D(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z); - } - - public static TestVector3D operator -(TestVector3D v1, TestVector3D v2) - { - return new TestVector3D(v1.X - v2.X, v1.Y - v2.Y, v1.Z - v2.Z); - } - - public static TestVector3D operator *(TestVector3D v, double scalar) - { - return new TestVector3D(v.X * scalar, v.Y * scalar, v.Z * scalar); - } - - public static TestVector3D operator *(double scalar, TestVector3D v) - { - return v * scalar; - } - - public double Length - { - get { return Math.Sqrt(X * X + Y * Y + Z * Z); } - } - - public TestVector3D Normalize() - { - double len = Length; - if (len < 1e-10) - return new TestVector3D(0, 0, 0); - return new TestVector3D(X / len, Y / len, Z / len); - } - - public static double DotProduct(TestVector3D v1, TestVector3D v2) - { - return v1.X * v2.X + v1.Y * v2.Y + v1.Z * v2.Z; - } - - public static TestVector3D CrossProduct(TestVector3D v1, TestVector3D v2) - { - return new TestVector3D( - v1.Y * v2.Z - v1.Z * v2.Y, - v1.Z * v2.X - v1.X * v2.Z, - v1.X * v2.Y - v1.Y * v2.X - ); - } - } - - /// - /// 测试用圆弧轨迹数据 - /// - public class TestArcTrajectory - { - public TestPoint3D Ts { get; set; } // 进入切点 - public TestPoint3D Te { get; set; } // 退出切点 - public TestPoint3D ArcCenter { get; set; } // 圆心 - public double RequestedRadius { get; set; } // 请求半径 - public double ActualRadius { get; set; } // 实际半径 - public double DeflectionAngle { get; set; } // 偏转角(弧度) - public double ArcLength { get; set; } // 圆弧长度 - } - - #endregion - - /// - /// PathCurveEngine 独立测试版本(不依赖 Navisworks API) - /// 用于测试核心算法逻辑的正确性 - /// - public static class TestPathCurveEngine - { - private static double Clamp(double value, double min, double max) - { - if (value < min) return min; - if (value > max) return max; - return value; - } - - /// - /// 计算圆弧切点和轨迹参数 - /// - public static TestArcTrajectory CalculateFillet( - TestPoint3D pPrev, - TestPoint3D pCurr, - TestPoint3D pNext, - double turnRadius) - { - // 1. 计算单位向量 - TestVector3D v1 = new TestVector3D(pPrev - pCurr).Normalize(); - TestVector3D v2 = new TestVector3D(pNext - pCurr).Normalize(); - - // 2. 计算夹角 α - double cosAlpha = TestVector3D.DotProduct(v1, v2); - double angleRad = Math.Acos(Clamp(cosAlpha, -1.0, 1.0)); - - // 如果几乎共线,返回无效轨迹 - if (angleRad < 0.01 || angleRad > Math.PI - 0.01) - { - return new TestArcTrajectory - { - Ts = pCurr, - Te = pCurr, - ArcCenter = pCurr, - RequestedRadius = turnRadius, - ActualRadius = 0, - DeflectionAngle = angleRad, - ArcLength = 0 - }; - } - - // 3. 计算切线长 Lt = R / tan(α/2) - double Lt = turnRadius / Math.Tan(angleRad / 2.0); - - // 4. 安全截断检查(限制为边长的45%) - double seg1Length = (pPrev - pCurr).Length; - double seg2Length = (pNext - pCurr).Length; - double maxAllowedLt = Math.Min(seg1Length, seg2Length) * 0.45; - - double actualRadius = turnRadius; - if (Lt > maxAllowedLt) - { - Lt = maxAllowedLt; - actualRadius = Lt * Math.Tan(angleRad / 2.0); - } - - // 5. 计算切点 - TestPoint3D ts = pCurr + v1 * Lt; - TestPoint3D te = pCurr + v2 * Lt; - - // 6. 计算圆心(使用角平分线) - TestVector3D bisector = (v1 + v2).Normalize(); - double distToCenter = actualRadius / Math.Sin(angleRad / 2.0); - TestPoint3D arcCenter = pCurr + bisector * distToCenter; - - // 7. 计算圆弧长度 - double arcLength = actualRadius * angleRad; - - return new TestArcTrajectory - { - Ts = ts, - Te = te, - ArcCenter = arcCenter, - RequestedRadius = turnRadius, - ActualRadius = actualRadius, - DeflectionAngle = angleRad, - ArcLength = arcLength - }; - } - - /// - /// 采样圆弧为离散点序列 - /// - public static List SampleArc(TestArcTrajectory trajectory, double samplingStep) - { - var points = new List(); - - if (trajectory.ActualRadius < 1e-10 || trajectory.ArcLength < 1e-10) - { - points.Add(trajectory.Ts); - points.Add(trajectory.Te); - return points; - } - - // 计算采样点数量 - int numSamples = Math.Max(2, (int)Math.Ceiling(trajectory.ArcLength / samplingStep)); - - // 计算起始和结束角度 - TestVector3D vStart = new TestVector3D(trajectory.Ts - trajectory.ArcCenter).Normalize(); - TestVector3D vEnd = new TestVector3D(trajectory.Te - trajectory.ArcCenter).Normalize(); - - // 计算旋转轴(使用叉积) - TestVector3D axis = TestVector3D.CrossProduct(vStart, vEnd).Normalize(); - - // 生成采样点 - for (int i = 0; i <= numSamples; i++) - { - double t = (double)i / numSamples; - double angle = t * trajectory.DeflectionAngle; - TestPoint3D p = RotatePointAroundAxis(trajectory.Ts, trajectory.ArcCenter, axis, angle); - points.Add(p); - } - - // 确保最后一个点是退出切点 - points[points.Count - 1] = trajectory.Te; - - return points; - } - - /// - /// 绕轴旋转点(罗德里格斯旋转公式) - /// - private static TestPoint3D RotatePointAroundAxis( - TestPoint3D point, - TestPoint3D center, - TestVector3D axis, - double angle) - { - TestVector3D v = new TestVector3D(point - center); - TestVector3D kxv = TestVector3D.CrossProduct(axis, v); - double kdv = TestVector3D.DotProduct(axis, v); - - TestVector3D vRot = v * Math.Cos(angle) + kxv * Math.Sin(angle) + axis * kdv * (1 - Math.Cos(angle)); - return center + vRot; - } - } - - /// - /// PathCurveEngine 核心算法独立测试 - /// 测试路径曲线化算法的正确性(不依赖 Navisworks API) - /// - [TestClass] - public class PathCurveEngineStandaloneTests - { - #region CalculateFillet 测试 - - [TestMethod] - public void Standalone_CalculateFillet_RightAngleTurn_ReturnsValidArc() - { - // Arrange - 创建90度直角转弯 - var pPrev = new TestPoint3D(0, 10, 0); // 上方点 - var pCurr = new TestPoint3D(0, 0, 0); // 转弯点 - var pNext = new TestPoint3D(10, 0, 0); // 右方点 - double turnRadius = 2.0; - - // Act - var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.AreEqual(turnRadius, trajectory.RequestedRadius, 0.01, "请求半径应该正确"); - Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); - Assert.IsTrue(trajectory.DeflectionAngle > 0, "偏转角应该大于0"); - Assert.IsTrue(trajectory.ArcLength > 0, "圆弧长度应该大于0"); - - // 验证切点位置 - Assert.IsTrue(trajectory.Ts.Y > 0, "进入切点应该在转弯点上方"); - Assert.IsTrue(Math.Abs(trajectory.Ts.X) < 0.01, "进入切点X坐标应该接近0"); - Assert.IsTrue(trajectory.Te.X > 0, "退出切点应该在转弯点右侧"); - Assert.IsTrue(Math.Abs(trajectory.Te.Y) < 0.01, "退出切点Y坐标应该接近0"); - } - - [TestMethod] - public void Standalone_CalculateFillet_AcuteAngleTurn_ReturnsValidArc() - { - // Arrange - 创建锐角转弯(约60度) - var pPrev = new TestPoint3D(0, 10, 0); - var pCurr = new TestPoint3D(0, 0, 0); - var pNext = new TestPoint3D(8.66, 5, 0); // 60度方向 - double turnRadius = 2.0; - - // Act - var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); - Assert.IsTrue(trajectory.DeflectionAngle > 0.5 && trajectory.DeflectionAngle < 1.5, "偏转角应该在60度左右"); - } - - [TestMethod] - public void Standalone_CalculateFillet_ObtuseAngleTurn_ReturnsValidArc() - { - // Arrange - 创建钝角转弯(约120度) - var pPrev = new TestPoint3D(0, 10, 0); - var pCurr = new TestPoint3D(0, 0, 0); - var pNext = new TestPoint3D(8.66, -5, 0); // 120度方向 - double turnRadius = 2.0; - - // Act - var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); - Assert.IsTrue(trajectory.DeflectionAngle > 1.5 && trajectory.DeflectionAngle < 2.5, "偏转角应该在120度左右"); - } - - [TestMethod] - public void Standalone_CalculateFillet_CollinearPoints_ReturnsInvalidArc() - { - // Arrange - 创建共线点(几乎直线) - var pPrev = new TestPoint3D(0, 10, 0); - var pCurr = new TestPoint3D(0, 0, 0); - var pNext = new TestPoint3D(0, -10, 0); // 同一直线 - double turnRadius = 2.0; - - // Act - var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.AreEqual(0, trajectory.ActualRadius, 0.01, "实际半径应该为0"); - Assert.AreEqual(0, trajectory.ArcLength, 0.01, "圆弧长度应该为0"); - Assert.AreEqual(pCurr, trajectory.Ts, "切点应该与转弯点相同"); - } - - [TestMethod] - public void Standalone_CalculateFillet_SafetyTruncation_AdjustsRadius() - { - // Arrange - 创建短边场景,需要安全截断 - var pPrev = new TestPoint3D(0, 1, 0); // 短边 - var pCurr = new TestPoint3D(0, 0, 0); - var pNext = new TestPoint3D(1, 0, 0); // 短边 - double turnRadius = 2.0; // 半径大于边长 - - // Act - var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.IsTrue(trajectory.ActualRadius < turnRadius, "实际半径应该小于请求半径"); - Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); - } - - [TestMethod] - public void Standalone_CalculateFillet_3DTurn_ReturnsValidArc() - { - // Arrange - 创建3D空间中的转弯 - var pPrev = new TestPoint3D(0, 10, 0); - var pCurr = new TestPoint3D(0, 0, 0); - var pNext = new TestPoint3D(10, 0, 5); // 有Z轴变化 - double turnRadius = 2.0; - - // Act - var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); - Assert.IsTrue(Math.Abs(trajectory.ArcCenter.Z) > 0.01, "圆心Z坐标应该不为0"); - } - - #endregion - - #region SampleArc 测试 - - [TestMethod] - public void Standalone_SampleArc_WithValidTrajectory_ReturnsSampledPoints() - { - // Arrange - var trajectory = new TestArcTrajectory - { - Ts = new TestPoint3D(0, 2, 0), - Te = new TestPoint3D(2, 0, 0), - ArcCenter = new TestPoint3D(0, 0, 0), - ActualRadius = 2.0, - DeflectionAngle = Math.PI / 2, // 90度 - ArcLength = Math.PI // 半圆周长 - }; - double samplingStep = 0.5; - - // Act - var sampledPoints = TestPathCurveEngine.SampleArc(trajectory, samplingStep); - - // Assert - Assert.IsNotNull(sampledPoints, "采样点列表不应该为null"); - Assert.IsTrue(sampledPoints.Count >= 2, "采样点数量应该至少为2"); - Assert.AreEqual(trajectory.Ts, sampledPoints.First(), "第一个点应该是进入切点"); - Assert.AreEqual(trajectory.Te, sampledPoints.Last(), "最后一个点应该是退出切点"); - } - - [TestMethod] - public void Standalone_SampleArc_SmallArcLength_ReturnsThreePoints() - { - // Arrange - var trajectory = new TestArcTrajectory - { - Ts = new TestPoint3D(0, 0.1, 0), - Te = new TestPoint3D(0.1, 0, 0), - ArcCenter = new TestPoint3D(0, 0, 0), - ActualRadius = 0.1, - DeflectionAngle = 0.1, - ArcLength = 0.01 // 非常小的圆弧 - }; - double samplingStep = 0.05; - - // Act - var sampledPoints = TestPathCurveEngine.SampleArc(trajectory, samplingStep); - - // Assert - Assert.IsNotNull(sampledPoints, "采样点列表不应该为null"); - Assert.AreEqual(3, sampledPoints.Count, "小圆弧应该返回3个点(起点、中间点、终点)"); - } - - [TestMethod] - public void Standalone_SampleArc_SamplingStep0_05_ReturnsCorrectCount() - { - // Arrange - var trajectory = new TestArcTrajectory - { - Ts = new TestPoint3D(0, 2, 0), - Te = new TestPoint3D(2, 0, 0), - ArcCenter = new TestPoint3D(0, 0, 0), - ActualRadius = 2.0, - DeflectionAngle = Math.PI / 2, - ArcLength = Math.PI - }; - double samplingStep = 0.05; - - // Act - var sampledPoints = TestPathCurveEngine.SampleArc(trajectory, samplingStep); - - // Assert - int expectedCount = (int)Math.Ceiling(Math.PI / 0.05) + 1; - Assert.IsTrue(sampledPoints.Count >= expectedCount - 2 && sampledPoints.Count <= expectedCount + 2, - $"采样点数量应该在 {expectedCount - 2} 到 {expectedCount + 2} 之间"); - } - - #endregion - - #region 集成测试 - - [TestMethod] - public void FullWorkflow_RightAngleTurn_CalculatesCorrectTrajectory() - { - // Arrange - 创建一个90度转弯 - var pPrev = new TestPoint3D(0, 10, 0); - var pCurr = new TestPoint3D(0, 0, 0); - var pNext = new TestPoint3D(10, 0, 0); - double turnRadius = 2.0; - - // Act - 计算轨迹 - var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - - // 采样圆弧 - var sampledPoints = TestPathCurveEngine.SampleArc(trajectory, 0.5); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); - - // 验证圆弧长度 - Assert.IsTrue(trajectory.ArcLength > 0 && trajectory.ArcLength < 10, "圆弧长度应该合理"); - - // 验证采样点 - Assert.IsNotNull(sampledPoints, "采样点不应该为null"); - Assert.IsTrue(sampledPoints.Count > 2, "采样点数量应该大于2"); - - // 验证所有采样点到圆心的距离应该接近半径 - foreach (var point in sampledPoints) - { - double dist = (point - trajectory.ArcCenter).Length; - Assert.IsTrue(Math.Abs(dist - trajectory.ActualRadius) < 0.1, - $"采样点到圆心的距离应该接近半径: {dist} vs {trajectory.ActualRadius}"); - } - } - - [TestMethod] - public void FullWorkflow_AcuteAngleTurn_CalculatesCorrectTrajectory() - { - // Arrange - 创建一个60度锐角转弯 - var pPrev = new TestPoint3D(0, 10, 0); - var pCurr = new TestPoint3D(0, 0, 0); - var pNext = new TestPoint3D(8.66, 5, 0); - double turnRadius = 2.0; - - // Act - var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - var sampledPoints = TestPathCurveEngine.SampleArc(trajectory, 0.5); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.IsTrue(trajectory.DeflectionAngle > 0.5 && trajectory.DeflectionAngle < 1.5, - "偏转角应该在60度左右"); - Assert.IsTrue(sampledPoints.Count > 2, "采样点数量应该大于2"); - } - - #endregion - } -} \ No newline at end of file diff --git a/UnitTests/Core/PathCurveEngineTests.cs b/UnitTests/Core/PathCurveEngineTests.cs deleted file mode 100644 index f7e7544..0000000 --- a/UnitTests/Core/PathCurveEngineTests.cs +++ /dev/null @@ -1,518 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Autodesk.Navisworks.Api; - -namespace NavisworksTransport.UnitTests.Core -{ - /// - /// PathCurveEngine 核心算法测试 - /// 测试路径曲线化算法的正确性 - /// - [TestClass] - public class PathCurveEngineTests - { - #region CalculateFillet 测试 - - [TestMethod] - public void CalculateFillet_RightAngleTurn_ReturnsValidArc() - { - // Arrange - 创建90度直角转弯 - var pPrev = new Point3D(0, 10, 0); // 上方点 - var pCurr = new Point3D(0, 0, 0); // 转弯点 - var pNext = new Point3D(10, 0, 0); // 右方点 - double turnRadius = 2.0; - - // Act - var trajectory = PathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.AreEqual(turnRadius, trajectory.RequestedRadius, 0.01, "请求半径应该正确"); - Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); - Assert.IsTrue(trajectory.DeflectionAngle > 0, "偏转角应该大于0"); - Assert.IsTrue(trajectory.ArcLength > 0, "圆弧长度应该大于0"); - - // 验证切点位置 - Assert.IsTrue(trajectory.Ts.Y > 0, "进入切点应该在转弯点上方"); - Assert.IsTrue(trajectory.Ts.X == 0, "进入切点X坐标应该为0"); - Assert.IsTrue(trajectory.Te.X > 0, "退出切点应该在转弯点右侧"); - Assert.IsTrue(trajectory.Te.Y == 0, "退出切点Y坐标应该为0"); - } - - [TestMethod] - public void CalculateFillet_AcuteAngleTurn_ReturnsValidArc() - { - // Arrange - 创建锐角转弯(约60度) - var pPrev = new Point3D(0, 10, 0); - var pCurr = new Point3D(0, 0, 0); - var pNext = new Point3D(8.66, 5, 0); // 60度方向 - double turnRadius = 2.0; - - // Act - var trajectory = PathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); - Assert.IsTrue(trajectory.DeflectionAngle > 0.5 && trajectory.DeflectionAngle < 1.5, "偏转角应该在60度左右"); - } - - [TestMethod] - public void CalculateFillet_ObtuseAngleTurn_ReturnsValidArc() - { - // Arrange - 创建钝角转弯(约120度) - var pPrev = new Point3D(0, 10, 0); - var pCurr = new Point3D(0, 0, 0); - var pNext = new Point3D(8.66, -5, 0); // 120度方向 - double turnRadius = 2.0; - - // Act - var trajectory = PathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); - Assert.IsTrue(trajectory.DeflectionAngle > 1.5 && trajectory.DeflectionAngle < 2.5, "偏转角应该在120度左右"); - } - - [TestMethod] - public void CalculateFillet_CollinearPoints_ReturnsInvalidArc() - { - // Arrange - 创建共线点(几乎直线) - var pPrev = new Point3D(0, 10, 0); - var pCurr = new Point3D(0, 0, 0); - var pNext = new Point3D(0, -10, 0); // 同一直线 - double turnRadius = 2.0; - - // Act - var trajectory = PathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.AreEqual(0, trajectory.ActualRadius, 0.01, "实际半径应该为0"); - Assert.AreEqual(0, trajectory.ArcLength, 0.01, "圆弧长度应该为0"); - Assert.AreEqual(pCurr, trajectory.Ts, "切点应该与转弯点相同"); - } - - [TestMethod] - public void CalculateFillet_SafetyTruncation_AdjustsRadius() - { - // Arrange - 创建短边场景,需要安全截断 - var pPrev = new Point3D(0, 1, 0); // 短边 - var pCurr = new Point3D(0, 0, 0); - var pNext = new Point3D(1, 0, 0); // 短边 - double turnRadius = 2.0; // 半径大于边长 - - // Act - var trajectory = PathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.IsTrue(trajectory.ActualRadius < turnRadius, "实际半径应该小于请求半径"); - Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); - } - - [TestMethod] - public void CalculateFillet_3DTurn_ReturnsValidArc() - { - // Arrange - 创建3D空间中的转弯 - var pPrev = new Point3D(0, 10, 0); - var pCurr = new Point3D(0, 0, 0); - var pNext = new Point3D(10, 0, 5); // 有Z轴变化 - double turnRadius = 2.0; - - // Act - var trajectory = PathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); - - // Assert - Assert.IsNotNull(trajectory, "轨迹不应该为null"); - Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); - Assert.IsTrue(trajectory.ArcCenter.Z != 0, "圆心Z坐标应该不为0"); - } - - #endregion - - #region SampleArc 测试 - - [TestMethod] - public void SampleArc_WithValidTrajectory_ReturnsSampledPoints() - { - // Arrange - var trajectory = new ArcTrajectory - { - Ts = new Point3D(0, 2, 0), - Te = new Point3D(2, 0, 0), - ArcCenter = new Point3D(0, 0, 0), - ActualRadius = 2.0, - DeflectionAngle = Math.PI / 2, // 90度 - ArcLength = Math.PI // 半圆周长 - }; - double samplingStep = 0.5; - - // Act - var sampledPoints = PathCurveEngine.SampleArc(trajectory, samplingStep); - - // Assert - Assert.IsNotNull(sampledPoints, "采样点列表不应该为null"); - Assert.IsTrue(sampledPoints.Count >= 2, "采样点数量应该至少为2"); - Assert.AreEqual(trajectory.Ts, sampledPoints.First(), "第一个点应该是进入切点"); - Assert.AreEqual(trajectory.Te, sampledPoints.Last(), "最后一个点应该是退出切点"); - } - - [TestMethod] - public void SampleArc_SmallArcLength_ReturnsTwoPoints() - { - // Arrange - var trajectory = new ArcTrajectory - { - Ts = new Point3D(0, 0.1, 0), - Te = new Point3D(0.1, 0, 0), - ArcCenter = new Point3D(0, 0, 0), - ActualRadius = 0.1, - DeflectionAngle = 0.1, - ArcLength = 0.01 // 非常小的圆弧 - }; - double samplingStep = 0.05; - - // Act - var sampledPoints = PathCurveEngine.SampleArc(trajectory, samplingStep); - - // Assert - Assert.IsNotNull(sampledPoints, "采样点列表不应该为null"); - Assert.AreEqual(2, sampledPoints.Count, "小圆弧应该返回2个点"); - } - - [TestMethod] - public void SampleArc_SamplingStep0_05_ReturnsCorrectCount() - { - // Arrange - var trajectory = new ArcTrajectory - { - Ts = new Point3D(0, 2, 0), - Te = new Point3D(2, 0, 0), - ArcCenter = new Point3D(0, 0, 0), - ActualRadius = 2.0, - DeflectionAngle = Math.PI / 2, - ArcLength = Math.PI - }; - double samplingStep = 0.05; - - // Act - var sampledPoints = PathCurveEngine.SampleArc(trajectory, samplingStep); - - // Assert - int expectedCount = (int)Math.Ceiling(Math.PI / 0.05) + 1; - Assert.IsTrue(sampledPoints.Count >= expectedCount - 2 && sampledPoints.Count <= expectedCount + 2, - $"采样点数量应该在 {expectedCount - 2} 到 {expectedCount + 2} 之间"); - } - - #endregion - - #region ApplyCurvatureToRoute 测试 - - [TestMethod] - public void ApplyCurvatureToRoute_SimplePath_GeneratesEdges() - { - // Arrange - var route = new PathRoute - { - Id = Guid.NewGuid().ToString(), - Name = "测试路径", - TurnRadius = 2.0 - }; - route.Points = new List - { - new PathPoint(new Point3D(0, 0, 0), "起点", PathPointType.StartPoint), - new PathPoint(new Point3D(10, 0, 0), "点2", PathPointType.WayPoint), - new PathPoint(new Point3D(10, 10, 0), "终点", PathPointType.EndPoint) - }; - route.Points[0].Index = 0; - route.Points[1].Index = 1; - route.Points[2].Index = 2; - double samplingStep = 0.5; - - // Act - PathCurveEngine.ApplyCurvatureToRoute(route, samplingStep); - - // Assert - Assert.IsTrue(route.IsCurved, "路径应该被标记为已曲线化"); - Assert.IsTrue(route.Edges.Count > 0, "应该生成路径边"); - Assert.IsTrue(route.TotalLength > 0, "路径总长度应该大于0"); - - // 验证每个边都有采样点 - foreach (var edge in route.Edges) - { - Assert.IsNotNull(edge.SampledPoints, "每个边都应该有采样点"); - Assert.IsTrue(edge.SampledPoints.Count > 0, "采样点数量应该大于0"); - } - } - - [TestMethod] - public void ApplyCurvatureToRoute_LShapedPath_GeneratesArcEdge() - { - // Arrange - var route = new PathRoute - { - Id = Guid.NewGuid().ToString(), - Name = "L型路径", - TurnRadius = 1.5 - }; - route.Points = new List - { - new PathPoint(new Point3D(0, 10, 0), "起点", PathPointType.StartPoint), - new PathPoint(new Point3D(0, 0, 0), "转弯点", PathPointType.WayPoint), - new PathPoint(new Point3D(10, 0, 0), "终点", PathPointType.EndPoint) - }; - route.Points[0].Index = 0; - route.Points[1].Index = 1; - route.Points[2].Index = 2; - double samplingStep = 0.5; - - // Act - PathCurveEngine.ApplyCurvatureToRoute(route, samplingStep); - - // Assert - Assert.IsTrue(route.IsCurved, "路径应该被标记为已曲线化"); - Assert.AreEqual(2, route.Edges.Count, "L型路径应该生成2条边"); - - // 第一条边是直线段 - Assert.AreEqual(PathSegmentType.Straight, route.Edges[0].SegmentType, "第一条边应该是直线段"); - - // 第二条边包含圆弧 - Assert.AreEqual(PathSegmentType.Arc, route.Edges[1].SegmentType, "第二条边应该是圆弧段"); - Assert.IsNotNull(route.Edges[1].Trajectory, "圆弧边应该有轨迹数据"); - Assert.IsTrue(route.Edges[1].Trajectory.ActualRadius > 0, "实际半径应该大于0"); - } - - [TestMethod] - public void ApplyCurvatureToRoute_ZShapedPath_GeneratesMultipleArcEdges() - { - // Arrange - var route = new PathRoute - { - Id = Guid.NewGuid().ToString(), - Name = "Z型路径", - TurnRadius = 1.5 - }; - route.Points = new List - { - new PathPoint(new Point3D(0, 10, 0), "起点", PathPointType.StartPoint), - new PathPoint(new Point3D(0, 0, 0), "转弯点1", PathPointType.WayPoint), - new PathPoint(new Point3D(10, 0, 0), "转弯点2", PathPointType.WayPoint), - new PathPoint(new Point3D(10, 10, 0), "终点", PathPointType.EndPoint) - }; - route.Points[0].Index = 0; - route.Points[1].Index = 1; - route.Points[2].Index = 2; - route.Points[3].Index = 3; - double samplingStep = 0.5; - - // Act - PathCurveEngine.ApplyCurvatureToRoute(route, samplingStep); - - // Assert - Assert.IsTrue(route.IsCurved, "路径应该被标记为已曲线化"); - Assert.AreEqual(3, route.Edges.Count, "Z型路径应该生成3条边"); - - // 应该有两条圆弧边 - int arcEdgeCount = route.Edges.Count(e => e.SegmentType == PathSegmentType.Arc); - Assert.IsTrue(arcEdgeCount >= 1, "应该至少有一条圆弧边"); - } - - [TestMethod] - public void ApplyCurvatureToRoute_NullRoute_DoesNotThrow() - { - // Arrange - PathRoute route = null; - double samplingStep = 0.5; - - // Act & Assert - try - { - PathCurveEngine.ApplyCurvatureToRoute(route, samplingStep); - Assert.Fail("应该抛出ArgumentNullException"); - } - catch (ArgumentNullException) - { - // Expected - } - } - - [TestMethod] - public void ApplyCurvatureToRoute_LessThanTwoPoints_DoesNotCurve() - { - // Arrange - var route = new PathRoute - { - Id = Guid.NewGuid().ToString(), - Name = "单点路径", - TurnRadius = 2.0 - }; - route.Points = new List - { - new PathPoint(new Point3D(0, 0, 0), "起点", PathPointType.StartPoint) - }; - route.Points[0].Index = 0; - double samplingStep = 0.5; - - // Act - PathCurveEngine.ApplyCurvatureToRoute(route, samplingStep); - - // Assert - Assert.IsFalse(route.IsCurved, "少于2个点的路径不应该被曲线化"); - Assert.AreEqual(0, route.Edges.Count, "不应该生成任何边"); - } - - #endregion - - #region RecalculateRouteLength 测试 - - [TestMethod] - public void RecalculateRouteLength_StraightEdges_CalculatesCorrectly() - { - // Arrange - var route = new PathRoute - { - Id = Guid.NewGuid().ToString(), - Name = "测试路径" - }; - route.Edges = new List - { - new PathEdge - { - Id = Guid.NewGuid().ToString(), - SegmentType = PathSegmentType.Straight, - PhysicalLength = 10.0, - SampledPoints = new List() - }, - new PathEdge - { - Id = Guid.NewGuid().ToString(), - SegmentType = PathSegmentType.Straight, - PhysicalLength = 5.0, - SampledPoints = new List() - } - }; - - // Act - PathCurveEngine.RecalculateRouteLength(route); - - // Assert - Assert.AreEqual(15.0, route.TotalLength, 0.01, "路径总长度应该等于所有边长度之和"); - } - - [TestMethod] - public void RecalculateRouteLength_MixedEdgeTypes_CalculatesCorrectly() - { - // Arrange - var route = new PathRoute - { - Id = Guid.NewGuid().ToString(), - Name = "测试路径" - }; - route.Edges = new List - { - new PathEdge - { - Id = Guid.NewGuid().ToString(), - SegmentType = PathSegmentType.Straight, - PhysicalLength = 10.0, - SampledPoints = new List() - }, - new PathEdge - { - Id = Guid.NewGuid().ToString(), - SegmentType = PathSegmentType.Arc, - PhysicalLength = Math.PI, // 半圆 - SampledPoints = new List() - }, - new PathEdge - { - Id = Guid.NewGuid().ToString(), - SegmentType = PathSegmentType.Straight, - PhysicalLength = 5.0, - SampledPoints = new List() - } - }; - - // Act - PathCurveEngine.RecalculateRouteLength(route); - - // Assert - double expectedLength = 10.0 + Math.PI + 5.0; - Assert.AreEqual(expectedLength, route.TotalLength, 0.01, "路径总长度应该正确计算"); - } - - [TestMethod] - public void RecalculateRouteLength_NullRoute_DoesNotThrow() - { - // Arrange - PathRoute route = null; - - // Act - PathCurveEngine.RecalculateRouteLength(route); - - // Assert - 不应该抛出异常 - Assert.IsTrue(true, "null路径不应该导致异常"); - } - - [TestMethod] - public void RecalculateRouteLength_EmptyEdges_SetsZeroLength() - { - // Arrange - var route = new PathRoute - { - Id = Guid.NewGuid().ToString(), - Name = "空路径", - Edges = new List() - }; - - // Act - PathCurveEngine.RecalculateRouteLength(route); - - // Assert - Assert.AreEqual(0, route.TotalLength, 0.01, "空边的路径长度应该为0"); - } - - #endregion - - #region 集成测试 - - [TestMethod] - public void FullWorkflow_CreateCurvePath_CalculatesCorrectLength() - { - // Arrange - 创建一个包含转弯的路径 - var route = new PathRoute - { - Id = Guid.NewGuid().ToString(), - Name = "完整测试路径", - TurnRadius = 2.0 - }; - route.Points = new List - { - new PathPoint(new Point3D(0, 20, 0), "起点", PathPointType.StartPoint), - new PathPoint(new Point3D(0, 0, 0), "转弯点", PathPointType.WayPoint), - new PathPoint(new Point3D(20, 0, 0), "终点", PathPointType.EndPoint) - }; - route.Points[0].Index = 0; - route.Points[1].Index = 1; - route.Points[2].Index = 2; - double samplingStep = 0.5; - - // Act - 应用曲线化 - PathCurveEngine.ApplyCurvatureToRoute(route, samplingStep); - - // Assert - Assert.IsTrue(route.IsCurved, "路径应该被曲线化"); - Assert.IsTrue(route.TotalLength > 0, "路径总长度应该大于0"); - - // 验证路径总长度小于直线距离(因为圆弧比直线短) - double straightDistance = 20 + 20; // 两条直线段 - Assert.IsTrue(route.TotalLength < straightDistance, "曲线化后的路径长度应该小于直线距离"); - } - - #endregion - } -} \ No newline at end of file diff --git a/UnitTests/Core/UIStateManagerBasicTests.cs b/UnitTests/Core/UIStateManagerBasicTests.cs deleted file mode 100644 index 503a367..0000000 --- a/UnitTests/Core/UIStateManagerBasicTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -namespace NavisworksTransport.UnitTests -{ - /// - /// UIStateManager基础功能测试 - /// 只包含可以在控制台环境下稳定运行的纯逻辑测试 - /// - [TestClass] - public class UIStateManagerBasicTests - { - private UIStateManager _uiStateManager; - - [TestInitialize] - public void Setup() - { - _uiStateManager = UIStateManager.Instance; - } - - [TestCleanup] - public void Cleanup() - { - // 不释放单例UIStateManager - } - - #region 单例模式测试 - - [TestMethod] - public void Instance_ShouldReturnSameInstanceForMultipleCalls() - { - // Arrange & Act - var instance1 = UIStateManager.Instance; - var instance2 = UIStateManager.Instance; - - // Assert - Assert.AreSame(instance1, instance2, "UIStateManager应该返回相同的单例实例"); - } - - [TestMethod] - public void Instance_ShouldNotBeNull() - { - // Arrange & Act - var instance = UIStateManager.Instance; - - // Assert - Assert.IsNotNull(instance, "UIStateManager实例不应该为null"); - } - - #endregion - - #region 基础属性测试 - - [TestMethod] - public void PendingOperationsCount_ShouldBeNonNegative() - { - // Arrange & Act - var count = _uiStateManager.PendingOperationsCount; - - // Assert - Assert.IsTrue(count >= 0, "待处理操作数量应该是非负数"); - } - - #endregion - - #region 队列管理基础测试 - - [TestMethod] - public void QueueUIUpdate_WithNullOperation_ShouldNotThrow() - { - // Act & Assert - // 传入null操作不应该抛出异常,应该被安全忽略 - _uiStateManager.QueueUIUpdate(null); - Assert.IsTrue(true, "传入null的队列操作应该被安全忽略"); - } - - [TestMethod] - public void QueueUIUpdate_WithValidOperation_ShouldAccept() - { - // Arrange - var executed = false; - - // Act - _uiStateManager.QueueUIUpdate(() => executed = true); - - // Assert - // 这里主要验证方法调用不会抛出异常 - Assert.IsTrue(true, "有效的队列操作应该被接受"); - } - - [TestMethod] - public void ClearUpdateQueue_ShouldNotThrow() - { - // Arrange - _uiStateManager.QueueUIUpdate(() => { }); - - // Act & Assert - _uiStateManager.ClearUpdateQueue(); - Assert.IsTrue(true, "清空队列操作应该成功完成"); - } - - #endregion - - #region 优先级测试 - - [TestMethod] - public void QueueUIUpdate_WithPriority_ShouldAcceptOperation() - { - // Act & Assert - _uiStateManager.QueueUIUpdate(() => { }, UIUpdatePriority.High); - _uiStateManager.QueueUIUpdate(() => { }, UIUpdatePriority.Normal); - _uiStateManager.QueueUIUpdate(() => { }, UIUpdatePriority.Low); - _uiStateManager.QueueUIUpdate(() => { }, UIUpdatePriority.Critical); - - Assert.IsTrue(true, "所有优先级的队列操作都应该被接受"); - } - - #endregion - } -} \ No newline at end of file diff --git a/UnitTests/Properties/AssemblyInfo.cs b/UnitTests/Properties/AssemblyInfo.cs deleted file mode 100644 index 0881e2b..0000000 --- a/UnitTests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("NavisworksTransport.UnitTests")] -[assembly: AssemblyDescription("Unit Tests for NavisworksTransport Plugin")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("NavisworksTransport Unit Tests")] -[assembly: AssemblyCopyright("Copyright © 2025")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -[assembly: ComVisible(false)] - -[assembly: Guid("2b5f1a8d-3ceb-4154-8761-f568ad9393ff")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/UnitTests/SimpleTest.cs b/UnitTests/SimpleTest.cs deleted file mode 100644 index 9b86f00..0000000 --- a/UnitTests/SimpleTest.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace NavisworksTransport.UnitTests -{ - [TestClass] - public class SimpleTest - { - [TestMethod] - public void HelloWorld_ShouldPass() - { - // Arrange - string expected = "Hello World"; - - // Act - string actual = "Hello World"; - - // Assert - Assert.AreEqual(expected, actual, "Hello World 测试应该通过"); - } - - [TestMethod] - public void SimpleMath_ShouldReturnCorrectSum() - { - // Arrange - int a = 2; - int b = 3; - int expected = 5; - - // Act - int actual = a + b; - - // Assert - Assert.AreEqual(expected, actual, "2 + 3 应该等于 5"); - } - } -} \ No newline at end of file diff --git a/UnitTests/TestHelpers/TestViewModel.cs b/UnitTests/TestHelpers/TestViewModel.cs deleted file mode 100644 index c2f6042..0000000 --- a/UnitTests/TestHelpers/TestViewModel.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace NavisworksTransport.UnitTests -{ - /// - /// 用于测试的ViewModelBase实现 - /// - public class TestViewModel : ViewModelBase - { - private string _testProperty; - - public string TestProperty - { - get => _testProperty; - set => SetProperty(ref _testProperty, value); - } - - /// - /// 公开OnPropertyChanged方法用于测试 - /// - public void TriggerPropertyChanged(string propertyName) - { - OnPropertyChanged(propertyName); - } - - /// - /// 公开OnPropertiesChanged方法用于测试 - /// - public void TriggerPropertiesChanged(params string[] propertyNames) - { - OnPropertiesChanged(propertyNames); - } - - /// - /// 公开SetProperties方法用于测试 - /// - public void TriggerSetProperties(params (string name, object value)[] properties) - { - SetProperties(properties); - } - - /// - /// 公开OnPropertyChangedAsync方法用于测试 - /// - public async Task TriggerPropertyChangedAsync(string propertyName, int timeout = 3000) - { - await OnPropertyChangedAsync(propertyName, timeout); - } - - /// - /// 公开SafeExecute方法用于测试 - /// - public void TestSafeExecute(Action action, string operationName) - { - SafeExecute(action, operationName); - } - - /// - /// 公开SafeExecute方法用于测试 - /// - public T TestSafeExecuteWithReturn(Func func, T defaultValue, string operationName) - { - return SafeExecute(func, defaultValue, operationName); - } - - /// - /// 公开SafeExecuteAsync方法用于测试 - /// - public async Task TestSafeExecuteAsync(Action action, string operationName) - { - await SafeExecuteAsync(action, operationName); - } - } -} \ No newline at end of file diff --git a/UnitTests/Utils/UnitsConverterTests.cs b/UnitTests/Utils/UnitsConverterTests.cs deleted file mode 100644 index c2c608d..0000000 --- a/UnitTests/Utils/UnitsConverterTests.cs +++ /dev/null @@ -1,368 +0,0 @@ -namespace NavisworksTransport.UnitTests.Utils -{ - /// - /// UnitsConverter工具类的纯逻辑测试 - /// 测试单位转换计算功能,不依赖Navisworks环境 - /// - [TestClass] - public class UnitsConverterTests - { - private const double TOLERANCE = 0.000001; // 浮点数比较精度 - - #region 基础单位转换为米测试 - - [TestMethod] - public void GetUnitsToMetersConversionFactor_Millimeters_ReturnsCorrectFactor() - { - // Arrange & Act - double result = UnitsConverter.GetUnitsToMetersConversionFactor(Units.Millimeters); - - // Assert - Assert.AreEqual(0.001, result, TOLERANCE, "毫米到米的转换系数应该是0.001"); - } - - [TestMethod] - public void GetUnitsToMetersConversionFactor_Centimeters_ReturnsCorrectFactor() - { - // Arrange & Act - double result = UnitsConverter.GetUnitsToMetersConversionFactor(Units.Centimeters); - - // Assert - Assert.AreEqual(0.01, result, TOLERANCE, "厘米到米的转换系数应该是0.01"); - } - - [TestMethod] - public void GetUnitsToMetersConversionFactor_Meters_ReturnsOne() - { - // Arrange & Act - double result = UnitsConverter.GetUnitsToMetersConversionFactor(Units.Meters); - - // Assert - Assert.AreEqual(1.0, result, TOLERANCE, "米到米的转换系数应该是1.0"); - } - - [TestMethod] - public void GetUnitsToMetersConversionFactor_Inches_ReturnsCorrectFactor() - { - // Arrange & Act - double result = UnitsConverter.GetUnitsToMetersConversionFactor(Units.Inches); - - // Assert - Assert.AreEqual(0.0254, result, TOLERANCE, "英寸到米的转换系数应该是0.0254"); - } - - [TestMethod] - public void GetUnitsToMetersConversionFactor_Feet_ReturnsCorrectFactor() - { - // Arrange & Act - double result = UnitsConverter.GetUnitsToMetersConversionFactor(Units.Feet); - - // Assert - Assert.AreEqual(0.3048, result, TOLERANCE, "英尺到米的转换系数应该是0.3048"); - } - - [TestMethod] - public void GetUnitsToMetersConversionFactor_Kilometers_ReturnsCorrectFactor() - { - // Arrange & Act - double result = UnitsConverter.GetUnitsToMetersConversionFactor(Units.Kilometers); - - // Assert - Assert.AreEqual(1000.0, result, TOLERANCE, "公里到米的转换系数应该是1000.0"); - } - - [TestMethod] - public void GetUnitsToMetersConversionFactor_Micrometers_ReturnsCorrectFactor() - { - // Arrange & Act - double result = UnitsConverter.GetUnitsToMetersConversionFactor(Units.Micrometers); - - // Assert - Assert.AreEqual(0.000001, result, TOLERANCE, "微米到米的转换系数应该是0.000001"); - } - - [TestMethod] - public void GetUnitsToMetersConversionFactor_Yards_ReturnsCorrectFactor() - { - // Arrange & Act - double result = UnitsConverter.GetUnitsToMetersConversionFactor(Units.Yards); - - // Assert - Assert.AreEqual(0.9144, result, TOLERANCE, "码到米的转换系数应该是0.9144"); - } - - [TestMethod] - public void GetUnitsToMetersConversionFactor_Miles_ReturnsCorrectFactor() - { - // Arrange & Act - double result = UnitsConverter.GetUnitsToMetersConversionFactor(Units.Miles); - - // Assert - Assert.AreEqual(1609.43, result, TOLERANCE, "英里到米的转换系数应该是1609.43"); - } - - #endregion - - #region 米转换为各单位测试 - - [TestMethod] - public void GetMetersToUnitsConversionFactor_Millimeters_ReturnsCorrectFactor() - { - // Arrange & Act - double result = UnitsConverter.GetMetersToUnitsConversionFactor(Units.Millimeters); - - // Assert - Assert.AreEqual(1000.0, result, TOLERANCE, "米到毫米的转换系数应该是1000.0"); - } - - [TestMethod] - public void GetMetersToUnitsConversionFactor_Centimeters_ReturnsCorrectFactor() - { - // Arrange & Act - double result = UnitsConverter.GetMetersToUnitsConversionFactor(Units.Centimeters); - - // Assert - Assert.AreEqual(100.0, result, TOLERANCE, "米到厘米的转换系数应该是100.0"); - } - - [TestMethod] - public void GetMetersToUnitsConversionFactor_Meters_ReturnsOne() - { - // Arrange & Act - double result = UnitsConverter.GetMetersToUnitsConversionFactor(Units.Meters); - - // Assert - Assert.AreEqual(1.0, result, TOLERANCE, "米到米的转换系数应该是1.0"); - } - - [TestMethod] - public void GetMetersToUnitsConversionFactor_Inches_ReturnsCorrectFactor() - { - // Arrange & Act - double result = UnitsConverter.GetMetersToUnitsConversionFactor(Units.Inches); - - // Assert - Assert.AreEqual(39.37, result, TOLERANCE, "米到英寸的转换系数应该是39.37"); - } - - [TestMethod] - public void GetMetersToUnitsConversionFactor_Feet_ReturnsCorrectFactor() - { - // Arrange & Act - double result = UnitsConverter.GetMetersToUnitsConversionFactor(Units.Feet); - - // Assert - Assert.AreEqual(3.281, result, TOLERANCE, "米到英尺的转换系数应该是3.281"); - } - - [TestMethod] - public void GetMetersToUnitsConversionFactor_Kilometers_ReturnsCorrectFactor() - { - // Arrange & Act - double result = UnitsConverter.GetMetersToUnitsConversionFactor(Units.Kilometers); - - // Assert - Assert.AreEqual(0.001, result, TOLERANCE, "米到公里的转换系数应该是0.001"); - } - - #endregion - - #region 转换系数一致性测试 - - [TestMethod] - public void ConversionFactors_ShouldBeInverse() - { - // 测试各种单位的正向和反向转换系数是否互为倒数 - Units[] testUnits = - { - Units.Millimeters, Units.Centimeters, Units.Meters, - Units.Inches, Units.Feet, Units.Kilometers, - Units.Micrometers, Units.Yards, Units.Miles - }; - - foreach (var unit in testUnits) - { - // Arrange & Act - double toMeters = UnitsConverter.GetUnitsToMetersConversionFactor(unit); - double fromMeters = UnitsConverter.GetMetersToUnitsConversionFactor(unit); - - // Assert - double product = toMeters * fromMeters; - Assert.AreEqual(1.0, product, TOLERANCE, - $"{unit}的正向和反向转换系数应该互为倒数,但得到: {toMeters} * {fromMeters} = {product}"); - } - } - - #endregion - - #region 边界值和异常情况测试 - - [TestMethod] - public void GetUnitsToMetersConversionFactor_InvalidUnit_ReturnsDefaultValue() - { - // Arrange - var invalidUnit = (Units)999; // 不存在的单位值 - - // Act - double result = UnitsConverter.GetUnitsToMetersConversionFactor(invalidUnit); - - // Assert - Assert.AreEqual(1.0, result, TOLERANCE, "无效单位应该返回默认值1.0"); - } - - [TestMethod] - public void GetMetersToUnitsConversionFactor_InvalidUnit_ReturnsDefaultValue() - { - // Arrange - var invalidUnit = (Units)999; // 不存在的单位值 - - // Act - double result = UnitsConverter.GetMetersToUnitsConversionFactor(invalidUnit); - - // Assert - Assert.AreEqual(1.0, result, TOLERANCE, "无效单位应该返回默认值1.0"); - } - - #endregion - - #region 实际转换计算测试 - - [TestMethod] - public void ActualConversion_Millimeters_CalculatesCorrectly() - { - // 测试实际的距离转换计算 - // 1000毫米应该等于1米 - - // Arrange - double millimeters = 1000.0; - double expectedMeters = 1.0; - - // Act - double actualMeters = millimeters * UnitsConverter.GetUnitsToMetersConversionFactor(Units.Millimeters); - - // Assert - Assert.AreEqual(expectedMeters, actualMeters, TOLERANCE, - "1000毫米应该转换为1米"); - } - - [TestMethod] - public void ActualConversion_Feet_CalculatesCorrectly() - { - // 测试实际的距离转换计算 - // 1英尺应该约等于0.3048米 - - // Arrange - double feet = 1.0; - double expectedMeters = 0.3048; - - // Act - double actualMeters = feet * UnitsConverter.GetUnitsToMetersConversionFactor(Units.Feet); - - // Assert - Assert.AreEqual(expectedMeters, actualMeters, TOLERANCE, - "1英尺应该转换为0.3048米"); - } - - [TestMethod] - public void ActualConversion_MetersToFeet_CalculatesCorrectly() - { - // 测试实际的距离转换计算 - // 1米应该约等于3.281英尺 - - // Arrange - double meters = 1.0; - double expectedFeet = 3.281; - - // Act - double actualFeet = meters * UnitsConverter.GetMetersToUnitsConversionFactor(Units.Feet); - - // Assert - Assert.AreEqual(expectedFeet, actualFeet, TOLERANCE, - "1米应该转换为3.281英尺"); - } - - [TestMethod] - public void RoundTripConversion_PreservesOriginalValue() - { - // 测试往返转换是否保持原始值 - Units[] testUnits = - { - Units.Millimeters, Units.Centimeters, Units.Meters, - Units.Inches, Units.Feet, Units.Kilometers - }; - - double[] testValues = { 0.0, 1.0, 10.5, 100.0, 1000.0, 0.001 }; - - foreach (var unit in testUnits) - { - foreach (var originalValue in testValues) - { - // Arrange & Act - double toMeters = UnitsConverter.GetUnitsToMetersConversionFactor(unit); - double fromMeters = UnitsConverter.GetMetersToUnitsConversionFactor(unit); - - double meters = originalValue * toMeters; - double backToOriginal = meters * fromMeters; - - // Assert - Assert.AreEqual(originalValue, backToOriginal, TOLERANCE, - $"往返转换应该保持原始值 {originalValue} ({unit}),但得到 {backToOriginal}"); - } - } - } - - #endregion - - #region 性能和精度测试 - - [TestMethod] - public void ConversionFactors_ShouldBeConsistent() - { - // 测试转换系数的一致性和精度 - - // 已知的精确转换关系 - Assert.AreEqual(1000.0, - UnitsConverter.GetUnitsToMetersConversionFactor(Units.Kilometers), - TOLERANCE, "公里到米的转换应该精确"); - - Assert.AreEqual(0.001, - UnitsConverter.GetUnitsToMetersConversionFactor(Units.Millimeters), - TOLERANCE, "毫米到米的转换应该精确"); - - // 测试英制单位的标准换算 - Assert.AreEqual(0.0254, - UnitsConverter.GetUnitsToMetersConversionFactor(Units.Inches), - TOLERANCE, "英寸到米的转换应该使用标准换算"); - - Assert.AreEqual(0.3048, - UnitsConverter.GetUnitsToMetersConversionFactor(Units.Feet), - TOLERANCE, "英尺到米的转换应该使用标准换算"); - } - - [TestMethod] - public void ZeroDistance_ConversionsHandleCorrectly() - { - // 测试零距离的转换 - Units[] testUnits = - { - Units.Millimeters, Units.Centimeters, Units.Meters, - Units.Inches, Units.Feet, Units.Kilometers - }; - - foreach (var unit in testUnits) - { - // Arrange & Act - double zeroInMeters = 0.0 * UnitsConverter.GetUnitsToMetersConversionFactor(unit); - double zeroFromMeters = 0.0 * UnitsConverter.GetMetersToUnitsConversionFactor(unit); - - // Assert - Assert.AreEqual(0.0, zeroInMeters, TOLERANCE, - $"零距离转换为米应该保持为零 ({unit})"); - Assert.AreEqual(0.0, zeroFromMeters, TOLERANCE, - $"零米转换为{unit}应该保持为零"); - } - } - - #endregion - } -} \ No newline at end of file diff --git a/doc/working/path_analysis_design.md b/doc/working/path_analysis_design.md new file mode 100644 index 0000000..0058f48 --- /dev/null +++ b/doc/working/path_analysis_design.md @@ -0,0 +1,594 @@ +# 路径分析功能深化设计方案 + +> 版本: 1.0 +> 日期: 2026-02-14 +> 适用范围: 大型装置内精密组件物流仿真 + +--- + +## 一、项目背景与目标 + +### 1.1 背景 +- **应用场景**: 大型装置内进行精密组件物流仿真 +- **核心目标**: 确保组件能安全到达安装位置(终点) +- **最高优先级**: 减少碰撞(安全性) +- **次要优先级**: 路径复杂度/效率(时间成本) +- **不涉及**: 经济成本 + +### 1.2 设计原则 +1. **实用优先**: 指标不在于多,而在于有用 +2. **聚焦终点**: 核心目标是"找出到同一安装位置的最佳路径" +3. **可解释性**: 每个指标和评分都要有明确的物理意义 +4. **可操作性**: 建议要具体,告诉用户"怎么做" + +--- + +## 二、策略模式 + +### 2.1 三种分析策略 + +| 策略 | 权重配置(安全/效率/转弯/曲折/冗余) | 适用场景 | +|-----|-----------------------------------|---------| +| **安全优先** | 50% / 20% / 10% / 10% / 10% | 精密/易损组件,首次运输 | +| **效率优先** | 20% / 40% / 15% / 15% / 10% | 紧急任务,多次往返 | +| **平衡模式** | 35% / 30% / 10% / 15% / 10% | 通用场景(默认) | + +### 2.2 权重配置代码 + +```csharp +public static class AnalysisStrategies +{ + public const string SafetyFirst = "安全优先"; + public const string EfficiencyFirst = "效率优先"; + public const string Balanced = "平衡模式"; + + // 权重顺序: 安全、效率、转弯、曲折、冗余 + public static readonly Dictionary Weights = new Dictionary + { + [SafetyFirst] = new[] { 0.50, 0.20, 0.10, 0.10, 0.10 }, + [EfficiencyFirst] = new[] { 0.20, 0.40, 0.15, 0.15, 0.10 }, + [Balanced] = new[] { 0.35, 0.30, 0.10, 0.15, 0.10 } + }; +} +``` + +--- + +## 三、指标体系(5维度) + +### 3.1 安全性(碰撞风险指数)- 权重最高 + +```csharp +// 基础碰撞分(100分制) +// 0次=100分, 1次=80分, 2次=60分, 3次=40分, 4次+=20分 +BaseScore = Max(0, 100 - CollisionCount * 20); + +// 碰撞热点扣分(3米范围内≥2次碰撞视为热点) +// 每个热点扣15分 +HotspotPenalty = HotspotCount * 15; + +// 最终安全分 +SafetyScore = Max(0, BaseScore - HotspotPenalty); +``` + +### 3.2 效率(通行效率指数) + +```csharp +// 速度分(以1.0 m/s为理想速度) +ActualSpeed = TotalLength / EstimatedTime; +SpeedScore = Min(100, ActualSpeed / 1.0 * 100); + +// 时间效率(与组内最短时间的比值) +TimeEfficiency = GroupMinTime / EstimatedTime * 100; + +// 综合效率分(速度70% + 时间效率30%) +EfficiencyScore = SpeedScore * 0.7 + TimeEfficiency * 0.3; +``` + +### 3.3 可行性(转弯难度系数) + +```csharp +// 从 PathRoute.Edges 统计 +ArcEdgeCount = Edges.Count(e => e.SegmentType == PathSegmentType.Arc); +TotalEdgeCount = Edges.Count; + +// 转弯次数扣分(每次扣8分) +TurnPenalty = ArcEdgeCount * 8; + +// 小半径惩罚 +MinRadius = Edges + .Where(e => e.SegmentType == PathSegmentType.Arc) + .Select(e => e.Trajectory?.ActualRadius ?? double.MaxValue) + .DefaultIfEmpty(double.MaxValue) + .Min(); + +RadiusPenalty = MinRadius < 2.0 ? 20 : 0; + +// 转弯难度分 +TurnDifficultyScore = Max(0, 100 - TurnPenalty - RadiusPenalty); +``` + +### 3.4 平顺性(路径曲折度) + +```csharp +// 计算起点到终点的直线距离 +StartPoint = Points.First().Position; +EndPoint = Points.Last().Position; +StraightDistance = Distance(StartPoint, EndPoint); + +// 曲折度(≥1.0) +Tortuosity = TotalLength / StraightDistance; + +// 曲折度分 +TortuosityScore = Max(0, 100 - (Tortuosity - 1.0) * 50); +``` + +### 3.5 冗余度(通道冗余度) + +```csharp +// 获取路径经过的通道 +Channels = GetChannelsAlongPath(this); + +// 通道最小限宽限高 +MinChannelWidth = Channels.Min(c => c.WidthLimit); +MinChannelHeight = Channels.Min(c => c.HeightLimit); + +// 车辆实际尺寸(含安全间隙) +EffectiveVehicleWidth = MaxVehicleWidth + SafetyMargin; +EffectiveVehicleHeight = MaxVehicleHeight + SafetyMargin; + +// 冗余率 +WidthRedundancy = (MinChannelWidth - EffectiveVehicleWidth) / EffectiveVehicleWidth; +HeightRedundancy = (MinChannelHeight - EffectiveVehicleHeight) / EffectiveVehicleHeight; + +// 冗余度分(0-100) +RedundancyScore = Min(100, (WidthRedundancy + HeightRedundancy) / 2 * 100); +``` + +--- + +## 四、同一起终点分析(核心功能) + +### 4.1 路径分组算法 + +```csharp +// 以终点为主的分组(2米阈值) +public List GroupPathsByEndpoint(List routes) +{ + const double ENDPOINT_THRESHOLD = 2.0; // 2米 + + var groups = new List(); + + foreach (var route in routes) + { + var endPoint = route.GetEndPoint()?.Position; + if (endPoint == null) continue; + + // 查找是否已有匹配的组 + var matchingGroup = groups.FirstOrDefault(g => + Distance(g.EndPoint, endPoint) < ENDPOINT_THRESHOLD); + + if (matchingGroup != null) + { + matchingGroup.Routes.Add(route); + // 更新组中心点 + matchingGroup.EndPoint = AveragePoint(matchingGroup.Routes); + } + else + { + groups.Add(new EndpointGroup + { + GroupId = Guid.NewGuid().ToString("N")[..8], + EndPoint = endPoint, + Routes = new List { route } + }); + } + } + + return groups; +} +``` + +### 4.2 组内对比指标 + +| 对比项 | 计算公式 | +|-------|---------| +| 长度差异率 | (路径长度 - 组内最短) / 最短 × 100% | +| 效率差异率 | (路径时间 - 组内最短) / 最短 × 100% | +| 安全差异 | 碰撞数对比(0次为优) | +| 最佳路径 | 按策略加权评分最高者 | + +### 4.3 最佳路径判定逻辑 + +```csharp +public PathRoute FindBestPathInGroup(EndpointGroup group, string strategy) +{ + // 第一步:计算所有路径的加权评分 + foreach (var route in group.Routes) + { + route.WeightedScore = CalculateWeightedScore(route.Analysis, strategy); + } + + // 第二步:按评分排序 + var sortedRoutes = group.Routes.OrderByDescending(r => r.WeightedScore).ToList(); + + // 第三步:如果前两名评分接近(差距<5分),优先选碰撞少的 + if (sortedRoutes.Count >= 2) + { + var first = sortedRoutes[0]; + var second = sortedRoutes[1]; + + if (Math.Abs(first.WeightedScore - second.WeightedScore) < 5) + { + if (second.CollisionCount < first.CollisionCount) + return second; + } + } + + return sortedRoutes.First(); +} +``` + +--- + +## 五、碰撞热点计算 + +### 5.1 热点定义 +- **范围**: 3米半径球体 +- **阈值**: 范围内 ≥2 次碰撞 +- **数据来源**: `ClashDetectiveCollisionObjects` 表的 `Item1PosX/Y/Z` + +### 5.2 热点检测算法 + +```csharp +public List DetectHotspots(List collisions, double radius = 3.0) +{ + var hotspots = new List(); + var processed = new HashSet(); + + for (int i = 0; i < collisions.Count; i++) + { + if (processed.Contains(i)) continue; + + var center = collisions[i].Center; + var nearbyCollisions = new List { collisions[i] }; + + // 查找范围内的其他碰撞 + for (int j = i + 1; j < collisions.Count; j++) + { + if (processed.Contains(j)) continue; + + if (Distance(center, collisions[j].Center) <= radius) + { + nearbyCollisions.Add(collisions[j]); + processed.Add(j); + } + } + + // 如果达到阈值,创建热点 + if (nearbyCollisions.Count >= 2) + { + hotspots.Add(new CollisionHotspot + { + Center = CalculateCenter(nearbyCollisions), + CollisionCount = nearbyCollisions.Count, + Radius = radius, + CollidedObjectNames = nearbyCollisions.Select(c => c.Item2.DisplayName).Distinct().ToList() + }); + } + + processed.Add(i); + } + + return hotspots; +} +``` + +--- + +## 六、优化建议生成 + +### 6.1 建议分类 + +| 类别 | 触发条件 | 建议内容 | +|-----|---------|---------| +| **安全建议** | 碰撞数>0 | 发现X次碰撞,其中Y个热点,建议检查碰撞位置 | +| **效率建议** | 长度差异率>30% | 该路径比组内最短路径长X%,建议优化路线 | +| **转弯建议** | 转弯次数>3 | 路径包含X个转弯,建议减少急转弯 | +| **冗余建议** | 冗余度<20% | 通道余量较小,建议确认车辆尺寸匹配 | +| **组内对比** | 组内路径>1 | 本组共X条路径,推荐【路径名】为最佳选择 | + +### 6.2 建议生成示例 + +```csharp +public List GenerateSuggestions(PathDetailedAnalysis analysis, EndpointGroup group) +{ + var suggestions = new List(); + + // 安全建议 + if (analysis.CollisionCount > 0) + { + if (analysis.HotspotCount > 0) + suggestions.Add($"🔴 发现{analysis.CollisionCount}次碰撞,其中{analysis.HotspotCount}个热点区域,建议重点检查热点位置"); + else + suggestions.Add($"⚠️ 发现{analysis.CollisionCount}次碰撞,建议优化路径避让"); + } + else + { + suggestions.Add("✅ 无碰撞,安全性良好"); + } + + // 组内对比建议 + if (group?.Routes.Count > 1) + { + var lengthDiff = (analysis.TotalLength - group.MinLength) / group.MinLength * 100; + if (lengthDiff > 30) + suggestions.Add($"📊 该路径比组内最短路径长{lengthDiff:F1}%,建议考虑更短路线"); + + // 推荐最佳路径 + if (analysis.RouteId == group.BestRouteId) + suggestions.Add($"🏆 该路径是本组到『{group.EndPointName}』的最佳选择"); + } + + return suggestions; +} +``` + +--- + +## 七、数据库扩展 + +### 7.1 扩展示意图 + +```sql +-- AnalysisResults 表扩展字段 +ALTER TABLE AnalysisResults ADD COLUMN + TurnDifficultyScore REAL, -- 转弯难度分 + TortuosityScore REAL, -- 曲折度分 + RedundancyScore REAL, -- 冗余度分 + HotspotCount INTEGER, -- 热点数量 + AnalysisStrategy TEXT, -- 分析策略 + GroupId TEXT, -- 所属终点组ID + GroupRanking INTEGER; -- 在组内排名 + +-- 新增热点表(可选) +CREATE TABLE IF NOT EXISTS CollisionHotspots ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + RouteId TEXT NOT NULL, + CenterX REAL, + CenterY REAL, + CenterZ REAL, + Radius REAL, + CollisionCount INTEGER, + CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(RouteId) REFERENCES PathRoutes(Id) ON DELETE CASCADE +); +``` + +--- + +## 八、复用现有代码清单 + +| 功能 | 复用代码 | 文件路径 | +|-----|---------|---------| +| 通道属性获取 | `CategoryAttributeManager.GetLogisticsPropertyValue()` | `src/Core/Properties/CategoryAttributeManager.cs` | +| 碰撞数据获取 | `PathDatabase.GetClashDetectiveResultsByPath()` | `src/Core/PathDatabase.cs` | +| 碰撞对象位置 | `ClashDetectiveCollisionObjects` 表 | `src/Core/PathDatabase.cs` | +| 路径点位置 | `PathRoute.GetSortedPoints()` | `src/Core/PathPlanningModels.cs` | +| 路径边分析 | `PathRoute.Edges` | `src/Core/PathPlanningModels.cs` | +| 单位转换 | `UnitsConverter.ConvertToMeters()` | `src/Utils/UnitsConverter.cs` | +| 距离计算 | `GeometryHelper.Distance()` | `src/Utils/GeometryHelper.cs` | +| 报告目录 | `PathHelper.GetReportDirectory()` | `src/Utils/PathHelper.cs` | +| HTML报告模板 | `CollisionReportHtmlGenerator` | `src/Utils/CollisionReportHtmlGenerator.cs` | + +--- + +## 九、分阶段实施计划 + +### Phase 1: 核心算法与数据结构(基础) + +| 任务 | 文件 | 说明 | +|-----|------|------| +| 1.1 数据模型 | `src/Core/Models/PathAnalysisModels.cs` | 新建,定义分析相关模型 | +| 1.2 分析引擎 | `src/Core/PathAnalysisEngine.cs` | 新建,核心计算逻辑 | +| 1.3 数据库扩展 | `src/Core/PathDatabase.cs` | 修改,增加新字段和方法 | +| 1.4 服务扩展 | `src/Core/PathAnalysisService.cs` | 修改,集成新算法 | + +### Phase 2: UI可视化(第二步) + +| 任务 | 文件 | 说明 | +|-----|------|------| +| 2.1 ViewModel更新 | `src/UI/WPF/ViewModels/PathAnalysisViewModel.cs` | 修改,增加新属性和命令 | +| 2.2 对话框更新 | `src/UI/WPF/Views/PathAnalysisDialog.xaml` | 修改,增加可视化元素 | +| 2.3 图表组件 | `src/UI/WPF/Views/PathAnalysisCharts.xaml` | 新建,雷达图/柱状图 | + +### Phase 3: HTML报告(第三步) + +| 任务 | 文件 | 说明 | +|-----|------|------| +| 3.1 报告生成器 | `src/Core/PathAnalysisReportGenerator.cs` | 新建,HTML生成逻辑 | +| 3.2 Chart.js部署 | `resources/js/chart.min.js` | 下载本地Chart.js | +| 3.3 报告模板 | `resources/templates/` | 新建,HTML模板文件 | + +--- + +## 十、关键算法伪代码 + +### 10.1 综合评分计算 + +```csharp +public double CalculateWeightedScore(PathDetailedAnalysis analysis, string strategy) +{ + var weights = AnalysisStrategies.Weights[strategy]; + + // 各维度分数 + double safety = analysis.SafetyScore; // 安全 + double efficiency = analysis.EfficiencyScore; // 效率 + double turn = analysis.TurnDifficultyScore; // 转弯 + double tortuosity = analysis.TortuosityScore; // 曲折 + double redundancy = analysis.RedundancyScore; // 冗余 + + // 加权计算 + double weightedScore = + safety * weights[0] + + efficiency * weights[1] + + turn * weights[2] + + tortuosity * weights[3] + + redundancy * weights[4]; + + return Math.Round(weightedScore, 1); +} +``` + +### 10.2 完整分析流程 + +```csharp +public PathDetailedAnalysis AnalyzePath(PathRoute route, AnalysisContext context) +{ + var analysis = new PathDetailedAnalysis + { + RouteId = route.Id, + RouteName = route.Name + }; + + // 1. 获取碰撞数据 + var collisions = GetCollisionsForRoute(route.Id); + analysis.CollisionCount = collisions.Count; + + // 2. 检测热点 + analysis.Hotspots = DetectHotspots(collisions, radius: 3.0); + analysis.HotspotCount = analysis.Hotspots.Count; + + // 3. 计算各维度分数 + analysis.SafetyScore = CalculateSafetyScore(analysis.CollisionCount, analysis.HotspotCount); + analysis.EfficiencyScore = CalculateEfficiencyScore(route, context.GroupMinTime); + analysis.TurnDifficultyScore = CalculateTurnDifficultyScore(route.Edges); + analysis.TortuosityScore = CalculateTortuosityScore(route); + analysis.RedundancyScore = CalculateRedundancyScore(route, context.Channels); + + // 4. 计算综合评分 + analysis.WeightedScore = CalculateWeightedScore(analysis, context.Strategy); + + return analysis; +} +``` + +--- + +## 十一、界面设计要点 + +### 11.1 路径分析对话框布局 + +``` ++----------------------------------------------------------+ +| 路径分析 - 多路径对比 [关闭] | ++----------------------------------------------------------+ +| +----------------+ +------------------+ +-----------+ | +| | 路径选择 | | 对比分析表格 | | 分析结果 | | +| | - 路径A | | 名称|安全|效率|... | | 最佳路径 | | +| | - 路径B [x] | | A | 90 | 85 |... | | 优化建议 | | +| | - 路径C [x] | | B |100 | 70 |... | | ... | | +| | | | C | 80 | 90 |... | | | | +| | [全选][清空] | +------------------+ +-----------+ | +| | | +------------------+ | +| | 策略: | | 雷达图可视化 | | +| | ○ 安全优先 | | | | +| | ○ 效率优先 | +------------------+ | +| | ● 平衡模式 | +------------------+ | +| | | | 同终点组分析 | | +| | [开始分析] | | 组1: 终点A (3条) | | +| +----------------+ | - 路径B [推荐] | | +| | - 路径A | | +| | - 路径C | | +| +------------------+ | ++----------------------------------------------------------+ +| [导出报告] [关闭] | ++----------------------------------------------------------+ +``` + +### 11.2 可视化元素 + +1. **雷达图**: 展示单条路径5维度分数 +2. **柱状图**: 组内路径长度/时间对比 +3. **表格**: 所有路径的详细指标 +4. **推荐卡片**: 高亮显示最佳路径 +5. **热点标记**: 在路径上标注碰撞热点位置 + +--- + +## 十二、HTML报告结构 + +```html + + + + + 路径分析报告 + + + + + + + + + + + + + + +``` + +--- + +## 十三、风险评估与应对 + +| 风险 | 影响 | 应对措施 | +|-----|------|---------| +| 碰撞位置数据缺失 | 热点计算失败 | 使用碰撞对象中心点作为近似 | +| 通道属性未设置 | 冗余度无法计算 | 使用默认值,并在报告中提示 | +| 路径无边数据 | 转弯分析失败 | 退化为直线分析 | +| 单一路径无对比 | 组内分析无意义 | 隐藏组内对比部分 | + +--- + +## 十四、验收标准 + +### 14.1 Phase 1 验收 +- [ ] 能正确计算5维度分数 +- [ ] 能正确检测碰撞热点(3米范围) +- [ ] 能按策略计算加权评分 +- [ ] 数据能正确保存到数据库 + +### 14.2 Phase 2 验收 +- [ ] UI能显示5维度雷达图 +- [ ] 同终点组能正确分组(2米阈值) +- [ ] 能正确标识组内最佳路径 +- [ ] 优化建议具体且可执行 + +### 14.3 Phase 3 验收 +- [ ] HTML报告能离线打开(使用本地Chart.js) +- [ ] 报告包含所有必要信息 +- [ ] 图表能正确显示 +- [ ] 样式符合专业报告标准 + +--- + +## 附录:参考文件 + +- 现有碰撞报告生成器: `src/Utils/CollisionReportHtmlGenerator.cs` +- 通道属性管理器: `src/Core/Properties/CategoryAttributeManager.cs` +- 路径数据模型: `src/Core/PathPlanningModels.cs` +- 数据库操作: `src/Core/PathDatabase.cs` +- 单位转换: `src/Utils/UnitsConverter.cs` diff --git a/doc/working/path_analysis_implementation_plan.md b/doc/working/path_analysis_implementation_plan.md new file mode 100644 index 0000000..de45679 --- /dev/null +++ b/doc/working/path_analysis_implementation_plan.md @@ -0,0 +1,402 @@ +# 路径分析功能实施计划 + +> 版本: 1.0 +> 日期: 2026-02-14 +> 总预计工期: 3-4天 + +--- + +## 实施阶段概览 + +| 阶段 | 内容 | 预计工期 | 优先级 | +|-----|------|---------|-------| +| Phase 1 | 核心算法与数据结构 | 1.5天 | P0 - 必须先完成 | +| Phase 2 | UI可视化 | 1天 | P1 - 依赖Phase 1 | +| Phase 3 | HTML报告 | 0.5天 | P2 - 依赖Phase 1 | + +--- + +## Phase 1: 核心算法与数据结构(Day 1-2) + +### 任务 1.1: 数据模型(2小时) + +**文件**: `src/Core/Models/PathAnalysisModels.cs` (新建) + +**内容**: +```csharp +// 主要模型列表: +// 1. PathDetailedAnalysis - 单条路径详细分析结果 +// 2. EndpointGroup - 同终点路径组 +// 3. CollisionHotspot - 碰撞热点 +// 4. PathComparison - 路径对比 +// 5. AnalysisStrategies - 策略常量与权重 +``` + +**验收标准**: +- [ ] 所有模型类定义完整 +- [ ] 包含所有必要属性 +- [ ] XML注释完整 + +--- + +### 任务 1.2: 分析引擎(6小时) + +**文件**: `src/Core/PathAnalysisEngine.cs` (新建) + +**核心方法**: +```csharp +public class PathAnalysisEngine +{ + // 1. 单路径分析 + public PathDetailedAnalysis AnalyzePath(PathRoute route, AnalysisContext context); + + // 2. 路径分组(按终点) + public List GroupPathsByEndpoint(List routes); + + // 3. 热点检测 + public List DetectHotspots(List collisions, double radius); + + // 4. 各维度评分 + private double CalculateSafetyScore(int collisionCount, int hotspotCount); + private double CalculateEfficiencyScore(PathRoute route, double groupMinTime); + private double CalculateTurnDifficultyScore(List edges); + private double CalculateTortuosityScore(PathRoute route); + private double CalculateRedundancyScore(PathRoute route, List channels); + + // 5. 加权评分 + public double CalculateWeightedScore(PathDetailedAnalysis analysis, string strategy); + + // 6. 组内最佳路径 + public PathRoute FindBestPathInGroup(EndpointGroup group, string strategy); +} +``` + +**验收标准**: +- [ ] 能正确计算5维度分数 +- [ ] 能正确检测碰撞热点(3米范围,≥2次) +- [ ] 路径分组正确(2米阈值) +- [ ] 加权评分符合权重配置 + +--- + +### 任务 1.3: 数据库扩展(3小时) + +**文件**: `src/Core/PathDatabase.cs` (修改) + +**修改内容**: + +1. **CreateTables() 方法** - 扩展 AnalysisResults 表 +```sql +-- 新增字段 +TurnDifficultyScore REAL +TortuosityScore REAL +RedundancyScore REAL +HotspotCount INTEGER +AnalysisStrategy TEXT +GroupId TEXT +GroupRanking INTEGER +``` + +2. **新增方法**: +```csharp +// 保存详细分析结果(扩展) +public void SaveDetailedAnalysisResult(PathDetailedAnalysis analysis); + +// 根据RouteId获取最新ClashDetective结果 +public ClashDetectiveResultRecord GetLatestClashDetectiveResultByRouteId(string routeId); + +// 保存热点数据 +public void SaveCollisionHotspots(string routeId, List hotspots); + +// 获取路径的热点数据 +public List GetCollisionHotspots(string routeId); +``` + +**验收标准**: +- [ ] 新表结构创建成功 +- [ ] 数据能正确保存和读取 +- [ ] 旧数据兼容性良好 + +--- + +### 任务 1.4: 服务层扩展(2小时) + +**文件**: `src/Core/PathAnalysisService.cs` (修改) + +**修改内容**: + +1. **扩展 CompareRoutes 方法**: +```csharp +public PathComparisonResult CompareRoutes(List routes, string strategy) +{ + // 1. 路径分组 + // 2. 分析每条路径 + // 3. 计算组内排名 + // 4. 找出最佳路径 + // 5. 生成优化建议 +} +``` + +2. **新增方法**: +```csharp +// 分析多条路径并分组 +public List AnalyzeAndGroupPaths(List routeIds, string strategy); + +// 获取同终点组分析 +public EndpointGroupAnalysis GetEndpointGroupAnalysis(string groupId); +``` + +**验收标准**: +- [ ] 能完成完整分析流程 +- [ ] 建议生成合理 +- [ ] 与现有代码兼容 + +--- + +## Phase 2: UI可视化(Day 2-3) + +### 任务 2.1: ViewModel更新(3小时) + +**文件**: `src/UI/WPF/ViewModels/PathAnalysisViewModel.cs` (修改) + +**新增属性**: +```csharp +// 策略选择(3种) +public List AvailableStrategies { get; } + +// 分析结果 +public ObservableCollection AnalysisResults { get; } + +// 同终点组 +public ObservableCollection EndpointGroups { get; } + +// 当前选中组 +public EndpointGroup SelectedGroup { get; set; } + +// 组内最佳路径 +public PathDetailedAnalysis GroupBestPath { get; set; } + +// 优化建议(分类) +public ObservableCollection CategorizedSuggestions { get; } +``` + +**新增命令**: +```csharp +// 切换策略 +public ICommand ChangeStrategyCommand { get; } + +// 选择组 +public ICommand SelectGroupCommand { get; } + +// 高亮最佳路径 +public ICommand HighlightBestPathCommand { get; } +``` + +**验收标准**: +- [ ] 数据绑定正确 +- [ ] 策略切换触发重新计算 +- [ ] 组选择显示详细信息 + +--- + +### 任务 2.2: 对话框界面更新(4小时) + +**文件**: `src/UI/WPF/Views/PathAnalysisDialog.xaml` (修改) + +**布局调整**: + +``` +左侧面板(300px): +- 路径选择列表(CheckBox) +- 策略选择(RadioButton,3种) +- [开始分析] 按钮 + +中间面板(可变): +- 数据对比表格(5维度分数) +- 雷达图(选中路径) +- 同终点组列表 + - 组标题(终点位置) + - 组内路径(最佳路径高亮) + +右侧面板(300px): +- 最佳路径卡片 +- 优化建议列表(分类) +- [导出报告] 按钮 +``` + +**验收标准**: +- [ ] 布局合理,信息清晰 +- [ ] 最佳路径视觉突出 +- [ ] 表格数据完整 + +--- + +### 任务 2.3: 图表可视化(3小时) + +**实现方式**: 使用 WPF 原生图表或简单自定义控件 + +**图表类型**: +1. **雷达图**: 展示5维度分数 + - 使用 WPF Polygon 实现 + - 或集成简单图表库 + +2. **柱状图**: 组内路径对比 + - 使用 WPF Rectangle 实现 + - 颜色区分不同路径 + +**文件**: 可在 `PathAnalysisDialog.xaml` 中直接实现,或新建 `PathAnalysisCharts.xaml` + +**验收标准**: +- [ ] 雷达图正确显示5维度 +- [ ] 柱状图对比清晰 +- [ ] 图表随选择更新 + +--- + +## Phase 3: HTML报告(Day 3-4) + +### 任务 3.1: 报告生成器(4小时) + +**文件**: `src/Core/PathAnalysisReportGenerator.cs` (新建) + +**核心方法**: +```csharp +public class PathAnalysisReportGenerator +{ + // 生成完整报告 + public string GenerateHtmlReport(PathAnalysisReportData data); + + // 生成雷达图数据(纯CSS实现) + private string GenerateRadarChartCss(PathDetailedAnalysis analysis); + + // 生成柱状图数据(纯CSS实现) + private string GenerateBarChartCss(EndpointGroup group); + + // 生成表格HTML + private string GenerateComparisonTable(List analyses); + + // 保存报告到文件 + public string SaveReportToFile(string htmlContent, string fileName); +} +``` + +**报告结构**: +```html +1. 头部 - 标题、时间、策略 +2. 执行摘要 - 最佳路径推荐(高亮卡片) +3. 雷达图 - CSS实现的5维雷达图 +4. 同终点组分析 - 组内对比表格 +5. 柱状图 - CSS实现的对比图 +6. 优化建议 - 分类建议列表 +7. 技术参数 - 车辆参数、检测参数 +``` + +**验收标准**: +- [ ] HTML能离线打开 +- [ ] 包含所有必要信息 +- [ ] 样式专业美观 + +--- + +### 任务 3.2: Chart.js本地部署(可选,1小时) + +**如需更复杂图表**: + +**文件**: +- 下载 `chart.min.js` 到 `resources/js/` +- HTML中引用 `` + +**注意**: 如果CSS图表足够,可跳过此步骤 + +--- + +## 每日工作计划 + +### Day 1 +| 时间 | 任务 | 输出 | +|-----|------|------| +| 09:00-11:00 | 任务1.1: 数据模型 | PathAnalysisModels.cs | +| 11:00-12:00 | 任务1.3: 数据库扩展(上)| PathDatabase.cs 修改 | +| 14:00-17:00 | 任务1.2: 分析引擎(上)| PathAnalysisEngine.cs 框架 | +| 17:00-18:00 | 任务1.3: 数据库扩展(下)| 完成数据库方法 | + +### Day 2 +| 时间 | 任务 | 输出 | +|-----|------|------| +| 09:00-12:00 | 任务1.2: 分析引擎(下)| 完成核心算法 | +| 14:00-16:00 | 任务1.4: 服务层扩展 | PathAnalysisService.cs 修改 | +| 16:00-18:00 | Phase 1 测试调试 | 验证算法正确性 | + +### Day 3 +| 时间 | 任务 | 输出 | +|-----|------|------| +| 09:00-12:00 | 任务2.1: ViewModel更新 | PathAnalysisViewModel.cs | +| 14:00-17:00 | 任务2.2: 对话框界面 | PathAnalysisDialog.xaml | +| 17:00-18:00 | 任务2.3: 图表可视化 | 雷达图/柱状图实现 | + +### Day 4 +| 时间 | 任务 | 输出 | +|-----|------|------| +| 09:00-12:00 | 任务3.1: 报告生成器 | PathAnalysisReportGenerator.cs | +| 14:00-15:00 | 任务3.2: Chart.js部署(可选)| resources/js/ | +| 15:00-17:00 | 整体测试优化 | 修复问题 | +| 17:00-18:00 | 文档更新 | 更新设计文档 | + +--- + +## 关键检查点 + +### 检查点1: Phase 1完成(Day 2下午) +- [ ] 能成功分析单条路径 +- [ ] 5维度分数计算正确 +- [ ] 热点检测正确 +- [ ] 数据能保存到数据库 + +### 检查点2: Phase 2完成(Day 3下午) +- [ ] UI能显示分析结果 +- [ ] 雷达图/柱状图正常 +- [ ] 同终点组分组正确 +- [ ] 最佳路径高亮显示 + +### 检查点3: 项目完成(Day 4下午) +- [ ] HTML报告生成成功 +- [ ] 所有功能集成测试通过 +- [ ] 代码审查完成 + +--- + +## 风险与应对 + +| 风险 | 概率 | 影响 | 应对 | +|-----|------|------|------| +| 碰撞位置数据格式不符 | 中 | 高 | 提前检查数据结构,准备备选方案 | +| 通道属性获取复杂 | 中 | 中 | 先使用简化方案,后续优化 | +| WPF图表实现困难 | 低 | 中 | 使用纯CSS图表,或简化显示 | +| 数据库升级失败 | 低 | 高 | 备份数据,使用ALTER TABLE增量升级 | + +--- + +## 交付物清单 + +### 代码文件 +1. `src/Core/Models/PathAnalysisModels.cs` (新建) +2. `src/Core/PathAnalysisEngine.cs` (新建) +3. `src/Core/PathAnalysisReportGenerator.cs` (新建) +4. `src/Core/PathDatabase.cs` (修改) +5. `src/Core/PathAnalysisService.cs` (修改) +6. `src/UI/WPF/ViewModels/PathAnalysisViewModel.cs` (修改) +7. `src/UI/WPF/Views/PathAnalysisDialog.xaml` (修改) +8. `src/UI/WPF/Views/PathAnalysisDialog.xaml.cs` (修改) + +### 资源文件 +9. `resources/js/chart.min.js` (可选,下载) + +### 文档 +10. `doc/working/path_analysis_design.md` (已存在) +11. `doc/working/path_analysis_implementation_plan.md` (本文件) + +--- + +## 开始实施 + +现在开始 **Phase 1 - 任务 1.1: 数据模型** diff --git a/run-unit-tests.bat b/run-unit-tests.bat deleted file mode 100644 index 04dc3f9..0000000 --- a/run-unit-tests.bat +++ /dev/null @@ -1,74 +0,0 @@ -@echo off -echo Running NavisworksTransport Standalone Unit Tests... -echo ===================================================== - -cd /d "%~dp0" - -echo. -echo [1/3] Building main project... -call compile.bat -if errorlevel 1 ( - echo Main project build failed! Cannot run tests. - pause - exit /b 1 -) - -echo. -echo [2/3] Building test project... -echo. - -rem 检查MSBuild路径 -set "MSBUILD_PATH=C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" -if not exist "%MSBUILD_PATH%" ( - echo MSBuild not found at expected location: %MSBUILD_PATH% - echo Please install Visual Studio Build Tools or adjust the path. - pause - exit /b 1 -) - -echo Using MSBuild: "%MSBUILD_PATH%" -echo Building test project... - -"%MSBUILD_PATH%" NavisworksTransport.UnitTests.csproj /p:Configuration=Release /p:Platform=x64 /verbosity:minimal - -if errorlevel 1 ( - echo Test project build failed! - pause - exit /b 1 -) - -echo Test project build successful! - -echo. -echo [3/3] Running Unit Tests... -echo. - -rem 检查VSTest路径 -set "VSTEST_PATH=" -if exist "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe" ( - set "VSTEST_PATH=C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe" -) - -if "%VSTEST_PATH%"=="" ( - echo VSTest not found. Please install Visual Studio Test Platform. - pause - exit /b 1 -) - -echo Using VSTest at: %VSTEST_PATH% -echo. - -rem 运行独立测试 -echo Running UIStateManager Tests... -"%VSTEST_PATH%" "bin\Debug\NavisworksTransport.UnitTests.dll" /Framework:".NETFramework,Version=v4.8" /TestAdapterPath:"packages\MSTest.TestAdapter.3.0.4\build\net462" /logger:console;verbosity=normal - -echo. -echo [Test Summary] -echo ============= -echo Standalone unit tests completed. -echo These tests validate the core UI architecture improvements. -echo. -echo Note: These tests run independently of Navisworks environment. -echo For full integration testing, Navisworks 2026 environment is required. - -pause \ No newline at end of file diff --git a/src/Commands/CreateAerialPathCommand.cs b/src/Commands/CreateAerialPathCommand.cs index 21e87a8..705e9aa 100644 --- a/src/Commands/CreateAerialPathCommand.cs +++ b/src/Commands/CreateAerialPathCommand.cs @@ -87,15 +87,8 @@ namespace NavisworksTransport.Commands route.MaxVehicleHeight = config.PathEditing.VehicleHeightMeters; route.SafetyMargin = config.PathEditing.SafetyMarginMeters; - // 计算总长度 - double totalLengthInModelUnits = 0; - for (int i = 0; i < pathPoints.Count - 1; i++) - { - totalLengthInModelUnits += GeometryHelper.CalculatePointDistance( - pathPoints[i].Position, - pathPoints[i + 1].Position); - } - route.TotalLength = UnitsConverter.ConvertToMeters(totalLengthInModelUnits); + // 注意:TotalLength 由计算属性实时提供,无需手动设置 + // 它会自动从 Points 计算直线距离 LogManager.Info($"{pathTypeName}路径创建成功: {route.Name}, 路径点数: {pathPoints.Count}, 总长度: {route.TotalLength:F2}米"); diff --git a/src/Core/Models/PathAnalysisModels.cs b/src/Core/Models/PathAnalysisModels.cs new file mode 100644 index 0000000..ce0c686 --- /dev/null +++ b/src/Core/Models/PathAnalysisModels.cs @@ -0,0 +1,650 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.Core.Models +{ + /// + /// 分析策略常量定义 + /// + public static class AnalysisStrategies + { + public const string SafetyFirst = "安全优先"; + public const string EfficiencyFirst = "效率优先"; + public const string Balanced = "平衡模式"; + + /// + /// 策略权重配置(四维度) + /// 顺序: 安全、效率、转弯、直达 + /// + public static readonly Dictionary Weights = new Dictionary + { + [SafetyFirst] = new[] { 0.50, 0.20, 0.15, 0.15 }, + [EfficiencyFirst] = new[] { 0.20, 0.40, 0.20, 0.20 }, + [Balanced] = new[] { 0.35, 0.30, 0.175, 0.175 } + }; + + /// + /// 获取所有可用策略列表 + /// + public static List GetAllStrategies() + { + return new List { SafetyFirst, EfficiencyFirst, Balanced }; + } + + /// + /// 验证策略名称是否有效 + /// + public static bool IsValidStrategy(string strategy) + { + return Weights.ContainsKey(strategy); + } + + /// + /// 获取策略权重,无效策略返回平衡模式权重 + /// + public static double[] GetWeights(string strategy) + { + if (Weights.TryGetValue(strategy, out var weights)) + return weights; + return Weights[Balanced]; + } + } + + /// + /// 单条路径的详细分析结果 + /// + public class PathDetailedAnalysis + { + /// + /// 路径ID + /// + public string RouteId { get; set; } + + /// + /// 路径名称 + /// + public string RouteName { get; set; } + + /// + /// 路径总长度(米) + /// + public double TotalLength { get; set; } + + /// + /// 预估通行时间(秒) + /// + public double EstimatedTime { get; set; } + + /// + /// 碰撞数量 + /// + public int CollisionCount { get; set; } + + /// + /// 碰撞热点数量 + /// + public int HotspotCount { get; set; } + + /// + /// 碰撞热点列表 + /// + public List Hotspots { get; set; } = new List(); + + // ========== 五维度评分(0-100)========== + + /// + /// 安全性分数(碰撞风险指数) + /// + public double SafetyScore { get; set; } + + /// + /// 效率分数(通行效率指数) + /// + public double EfficiencyScore { get; set; } + + /// + /// 转弯难度分数 + /// + public double TurnDifficultyScore { get; set; } + + /// + /// 路径曲折度分数 + /// + public double TortuosityScore { get; set; } + + /// + /// 通道冗余度分数 + /// + public double RedundancyScore { get; set; } + + /// + /// 综合加权评分 + /// + public double WeightedScore { get; set; } + + /// + /// 分析使用的策略 + /// + public string Strategy { get; set; } + + /// + /// 分析时间 + /// + public DateTime AnalysisTime { get; set; } + + // ========== 组内排名信息 ========== + + /// + /// 所属终点组ID + /// + public string GroupId { get; set; } + + /// + /// 在组内排名(1开始) + /// + public int GroupRanking { get; set; } + + /// + /// 是否为组内最佳路径 + /// + public bool IsBestInGroup => GroupRanking == 1; + + /// + /// 获取四维度分数数组(用于雷达图) + /// 顺序: 安全、效率、转弯、直达 + /// + public double[] GetDimensionScores() + { + return new[] { SafetyScore, EfficiencyScore, TurnDifficultyScore, TortuosityScore }; + } + + /// + /// 获取维度分数字典 + /// + public Dictionary GetDimensionScoreDict() + { + return new Dictionary + { + ["安全性"] = SafetyScore, + ["效率"] = EfficiencyScore, + ["转弯难度"] = TurnDifficultyScore, + ["直达性"] = TortuosityScore + }; + } + } + + /// + /// 碰撞热点 + /// + public class CollisionHotspot + { + /// + /// 热点ID + /// + public string Id { get; set; } = Guid.NewGuid().ToString("N").Substring(0, 8); + + /// + /// 热点中心位置 + /// + public Point3D Center { get; set; } + + /// + /// 热点半径(米) + /// + public double Radius { get; set; } = 3.0; + + /// + /// 范围内碰撞数量 + /// + public int CollisionCount { get; set; } + + /// + /// 碰撞对象名称列表 + /// + public List CollidedObjectNames { get; set; } = new List(); + + /// + /// 热点严重程度 + /// + public HotspotSeverity Severity + { + get + { + if (CollisionCount >= 5) return HotspotSeverity.Critical; + if (CollisionCount >= 3) return HotspotSeverity.High; + return HotspotSeverity.Medium; + } + } + + /// + /// 获取热点描述 + /// + public string GetDescription() + { + return $"位置({Center.X:F2}, {Center.Y:F2}, {Center.Z:F2})范围内有{CollisionCount}次碰撞"; + } + } + + /// + /// 热点严重程度 + /// + public enum HotspotSeverity + { + /// + /// 中等(2次碰撞) + /// + Medium, + + /// + /// 严重(3-4次碰撞) + /// + High, + + /// + /// 危急(5次及以上) + /// + Critical + } + + /// + /// 同终点路径组 + /// + public class EndpointGroup + { + /// + /// 组ID + /// + public string GroupId { get; set; } + + /// + /// 组名称(基于终点描述) + /// + public string GroupName { get; set; } + + /// + /// 终点位置(组中心点) + /// + public Point3D EndPoint { get; set; } + + /// + /// 终点描述 + /// + public string EndPointDescription { get; set; } + + /// + /// 组内所有路径的分析结果 + /// + public List PathAnalyses { get; set; } = new List(); + + /// + /// 路径数量 + /// + public int RouteCount => PathAnalyses?.Count ?? 0; + + /// + /// 组内最短路径长度 + /// + public double MinLength => PathAnalyses.Min(p => p.TotalLength); + + /// + /// 组内最短通行时间 + /// + public double MinTime => PathAnalyses.Min(p => p.EstimatedTime); + + /// + /// 组内最少碰撞数 + /// + public int MinCollisionCount => PathAnalyses.Min(p => p.CollisionCount); + + /// + /// 组内最佳路径(按当前策略) + /// + public PathDetailedAnalysis BestPath => PathAnalyses.OrderByDescending(p => p.WeightedScore).FirstOrDefault(); + + /// + /// 获取组内某路径的排名 + /// + public int GetRanking(string routeId) + { + var sorted = PathAnalyses.OrderByDescending(p => p.WeightedScore).ToList(); + var index = sorted.FindIndex(p => p.RouteId == routeId); + return index >= 0 ? index + 1 : 0; + } + + /// + /// 获取组内某路径与最佳路径的长度差异率 + /// + public double GetLengthDiffPercent(string routeId) + { + var path = PathAnalyses.FirstOrDefault(p => p.RouteId == routeId); + if (path == null || MinLength <= 0) return 0; + return (path.TotalLength - MinLength) / MinLength * 100; + } + + /// + /// 获取组内某路径与最快路径的时间差异率 + /// + public double GetTimeDiffPercent(string routeId) + { + var path = PathAnalyses.FirstOrDefault(p => p.RouteId == routeId); + if (path == null || MinTime <= 0) return 0; + return (path.EstimatedTime - MinTime) / MinTime * 100; + } + } + + /// + /// 路径对比结果 + /// + public class PathComparison + { + /// + /// 路径A ID + /// + public string PathAId { get; set; } + + /// + /// 路径A名称 + /// + public string PathAName { get; set; } + + /// + /// 路径B ID + /// + public string PathBId { get; set; } + + /// + /// 路径B名称 + /// + public string PathBName { get; set; } + + /// + /// 长度差异(米) + /// + public double LengthDiff { get; set; } + + /// + /// 长度差异率(%) + /// + public double LengthDiffPercent { get; set; } + + /// + /// 时间差异(秒) + /// + public double TimeDiff { get; set; } + + /// + /// 时间差异率(%) + /// + public double TimeDiffPercent { get; set; } + + /// + /// 碰撞数差异 + /// + public int CollisionDiff { get; set; } + + /// + /// 综合评分差异 + /// + public double ScoreDiff { get; set; } + + /// + /// 优势方(A更优、B更优、相当) + /// + public string Advantage { get; set; } + } + + /// + /// 分析上下文 + /// + public class AnalysisContext + { + /// + /// 分析策略 + /// + public string Strategy { get; set; } = AnalysisStrategies.Balanced; + + /// + /// 组内最短通行时间(用于效率计算) + /// + public double GroupMinTime { get; set; } + + /// + /// 组内最短路径长度 + /// + public double GroupMinLength { get; set; } + + /// + /// 路径经过的通道信息 + /// + public List Channels { get; set; } = new List(); + + /// + /// 组内最高成本(用于归一化,如需要) + /// + public double GroupMaxCost { get; set; } + } + + /// + /// 通道信息 + /// + public class ChannelInfo + { + /// + /// 通道模型ID + /// + public string ModelItemId { get; set; } + + /// + /// 通道显示名称 + /// + public string DisplayName { get; set; } + + /// + /// 通道类型 + /// + public CategoryAttributeManager.LogisticsElementType ChannelType { get; set; } + + /// + /// 宽度限制(米) + /// + public double WidthLimit { get; set; } + + /// + /// 高度限制(米) + /// + public double HeightLimit { get; set; } + + /// + /// 速度限制(m/s) + /// + public double SpeedLimit { get; set; } + } + + /// + /// 分类优化建议 + /// + public class CategorizedSuggestion + { + /// + /// 建议类别 + /// + public SuggestionCategory Category { get; set; } + + /// + /// 类别显示名称 + /// + public string CategoryName => GetCategoryDisplayName(Category); + + /// + /// 建议标题 + /// + public string Title { get; set; } + + /// + /// 建议详细描述 + /// + public string Description { get; set; } + + /// + /// 关联的路径ID + /// + public string RelatedRouteId { get; set; } + + /// + /// 关联的路径名称 + /// + public string RelatedRouteName { get; set; } + + /// + /// 优先级(1-5,5最高) + /// + public int Priority { get; set; } + + /// + /// 获取类别显示名称 + /// + private static string GetCategoryDisplayName(SuggestionCategory category) + { + switch (category) + { + case SuggestionCategory.Safety: return "安全建议"; + case SuggestionCategory.Efficiency: return "效率建议"; + case SuggestionCategory.TurnComplexity: return "转弯建议"; + case SuggestionCategory.Redundancy: return "冗余建议"; + case SuggestionCategory.GroupComparison: return "组内对比"; + case SuggestionCategory.General: return "一般建议"; + default: return "其他"; + } + } + } + + /// + /// 建议类别 + /// + public enum SuggestionCategory + { + /// + /// 安全建议 + /// + Safety, + + /// + /// 效率建议 + /// + Efficiency, + + /// + /// 转弯复杂度建议 + /// + TurnComplexity, + + /// + /// 冗余度建议 + /// + Redundancy, + + /// + /// 组内对比建议 + /// + GroupComparison, + + /// + /// 一般建议 + /// + General + } + + /// + /// 完整分析报告数据 + /// + public class PathAnalysisReportData + { + /// + /// 报告生成时间 + /// + public DateTime ReportTime { get; set; } + + /// + /// 分析策略 + /// + public string Strategy { get; set; } + + /// + /// 分析的路径总数 + /// + public int TotalPathCount { get; set; } + + /// + /// 同终点组数量 + /// + public int GroupCount { get; set; } + + /// + /// 所有同终点组 + /// + public List Groups { get; set; } = new List(); + + /// + /// 所有路径分析结果 + /// + public List AllPathAnalyses { get; set; } = new List(); + + /// + /// 全局最佳路径 + /// + public PathDetailedAnalysis OverallBestPath { get; set; } + + /// + /// 所有优化建议 + /// + public List AllSuggestions { get; set; } = new List(); + + /// + /// 文档名称 + /// + public string DocumentName { get; set; } + + /// + /// 车辆参数信息 + /// + public VehicleParameters VehicleParams { get; set; } + } + + /// + /// 车辆参数 + /// + public class VehicleParameters + { + /// + /// 车辆长度(米) + /// + public double Length { get; set; } + + /// + /// 车辆宽度(米) + /// + public double Width { get; set; } + + /// + /// 车辆高度(米) + /// + public double Height { get; set; } + + /// + /// 安全间隙(米) + /// + public double SafetyMargin { get; set; } + + /// + /// 有效宽度(含间隙) + /// + public double EffectiveWidth => Width + SafetyMargin * 2; + + /// + /// 有效高度(含间隙) + /// + public double EffectiveHeight => Height + SafetyMargin; + } +} diff --git a/src/Core/PathAnalysisEngine.cs b/src/Core/PathAnalysisEngine.cs new file mode 100644 index 0000000..8d9a350 --- /dev/null +++ b/src/Core/PathAnalysisEngine.cs @@ -0,0 +1,811 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Autodesk.Navisworks.Api; +using NavisworksTransport.Core.Models; +using NavisworksTransport.Utils; + +namespace NavisworksTransport.Core +{ + /// + /// 路径分析引擎 + /// 负责计算路径的各项指标和评分 + /// + public class PathAnalysisEngine + { + private readonly PathDatabase _database; + private const double HOTSPOT_RADIUS = 3.0; // 热点半径:3米 + private const double ENDPOINT_THRESHOLD_METERS = 2.0; // 终点分组阈值:2米 + private const double IDEAL_SPEED = 1.0; // 理想速度:1.0 m/s + + /// + /// 获取终点分组阈值(转换为模型单位用于距离比较) + /// + private double EndpointThreshold => NavisworksTransport.Utils.UnitsConverter.ConvertFromMeters(ENDPOINT_THRESHOLD_METERS); + + /// + /// 构造函数 + /// + public PathAnalysisEngine(PathDatabase database) + { + _database = database ?? throw new ArgumentNullException(nameof(database)); + } + + #region 公共方法 + + /// + /// 分析单条路径的所有指标 + /// + public PathDetailedAnalysis AnalyzePath(PathRoute route, AnalysisContext context) + { + if (route == null) + throw new ArgumentNullException(nameof(route)); + + var analysis = new PathDetailedAnalysis + { + RouteId = route.Id, + RouteName = route.Name, + TotalLength = route.TotalLength, + EstimatedTime = route.EstimatedTime, + Strategy = context?.Strategy ?? AnalysisStrategies.Balanced, + AnalysisTime = DateTime.Now + }; + + try + { + // 1. 获取碰撞数据 + var collisions = GetCollisionsForRoute(route.Id); + analysis.CollisionCount = collisions?.Count ?? 0; + + // 2. 检测碰撞热点 + analysis.Hotspots = DetectHotspots(collisions, HOTSPOT_RADIUS); + analysis.HotspotCount = analysis.Hotspots.Count; + + // 3. 计算各维度分数(四维度:安全、效率、转弯、直达) + analysis.SafetyScore = CalculateSafetyScore(analysis.CollisionCount, analysis.HotspotCount); + analysis.EfficiencyScore = CalculateEfficiencyScore(route, context?.GroupMinLength ?? route.TotalLength); + analysis.TurnDifficultyScore = CalculateTurnDifficultyScore(route.Edges); + analysis.TortuosityScore = CalculateTortuosityScore(route); + analysis.RedundancyScore = 0; // 已移除,保留字段兼容 + + // 4. 计算综合加权评分 + analysis.WeightedScore = CalculateWeightedScore(analysis, analysis.Strategy); + + LogManager.Info($"[路径分析] {route.Name}: 安全{analysis.SafetyScore:F0}, 效率{analysis.EfficiencyScore:F0}, " + + $"转弯{analysis.TurnDifficultyScore:F0}, 直达{analysis.TortuosityScore:F0}, 综合{analysis.WeightedScore:F0}"); + } + catch (Exception ex) + { + LogManager.Error($"[路径分析] 分析路径 {route.Name} 失败: {ex.Message}", ex); + // 设置默认值,避免崩溃 + SetDefaultScores(analysis); + } + + return analysis; + } + + /// + /// 对多条路径进行分组(按终点) + /// + public List GroupPathsByEndpoint(List routes) + { + if (routes == null || routes.Count == 0) + return new List(); + + var groups = new List(); + + foreach (var route in routes) + { + var endPoint = route.GetEndPoint()?.Position; + if (endPoint == null) + { + LogManager.Warning($"[路径分组] 路径 {route.Name} 没有终点,跳过"); + continue; + } + + // 查找是否已有匹配的组(以终点为主,2米阈值) + var matchingGroup = groups.FirstOrDefault(g => + Distance(g.EndPoint, endPoint) < EndpointThreshold); + + if (matchingGroup != null) + { + // 添加到现有组 + var analysis = AnalyzePath(route, new AnalysisContext { GroupMinTime = 0 }); + matchingGroup.PathAnalyses.Add(analysis); + // 更新组中心点 + matchingGroup.EndPoint = CalculateGroupCenter(matchingGroup.PathAnalyses, routes); + LogManager.Debug($"[路径分组] 路径 {route.Name} 加入现有组 {matchingGroup.GroupId}"); + } + else + { + // 创建新组 + var newGroup = new EndpointGroup + { + GroupId = Guid.NewGuid().ToString("N").Substring(0, 8), + GroupName = $"终点组-{groups.Count + 1}", + EndPoint = endPoint, + EndPointDescription = route.GetEndPoint()?.Name ?? "未知终点" + }; + + var analysis = AnalyzePath(route, new AnalysisContext { GroupMinTime = 0 }); + newGroup.PathAnalyses.Add(analysis); + groups.Add(newGroup); + + LogManager.Info($"[路径分组] 创建新组 {newGroup.GroupId},终点: {newGroup.EndPointDescription}"); + } + } + + // 更新各组的组内统计数据 + foreach (var group in groups) + { + UpdateGroupStatistics(group); + } + + LogManager.Info($"[路径分组] 完成,共 {groups.Count} 个组,{routes.Count} 条路径"); + return groups; + } + + /// + /// 分析多条路径并分组,包含完整的上下文计算 + /// + public List AnalyzeAndGroupPaths(List routes, string strategy) + { + if (routes == null || routes.Count == 0) + return new List(); + + // 第一步:初步分析所有路径(获取基础数据) + var analyses = new List(); + foreach (var route in routes) + { + var analysis = AnalyzePath(route, new AnalysisContext { Strategy = strategy }); + analyses.Add(analysis); + } + + // 第二步:按终点分组 + var groups = new List(); + var processed = new HashSet(); + + LogManager.Info($"[路径分组] 开始对 {analyses.Count} 条分析结果进行分组"); + + foreach (var analysis in analyses) + { + if (processed.Contains(analysis.RouteId)) + { + LogManager.Debug($"[路径分组] 跳过已处理: {analysis.RouteId}"); + continue; + } + + var route = routes.FirstOrDefault(r => r.Id == analysis.RouteId); + if (route == null) + { + LogManager.Warning($"[路径分组] 找不到路径: {analysis.RouteId}"); + continue; + } + + var endPointObj = route.GetEndPoint(); + var endPoint = endPointObj?.Position; + if (endPoint == null) + { + LogManager.Warning($"[路径分组] 路径 {route.Name} 没有终点"); + continue; + } + + LogManager.Info($"[路径分组] 处理路径 {route.Name}, 终点: ({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})"); + + // 找到同组的所有路径(包括当前路径) + var groupAnalyses = new List(); + // 首先添加当前路径 + groupAnalyses.Add(analysis); + processed.Add(analysis.RouteId); + + foreach (var otherAnalysis in analyses) + { + if (processed.Contains(otherAnalysis.RouteId)) + continue; + + var otherRoute = routes.FirstOrDefault(r => r.Id == otherAnalysis.RouteId); + if (otherRoute == null) + continue; + + var otherEndPoint = otherRoute.GetEndPoint()?.Position; + if (otherEndPoint == null) + continue; + + if (Distance(endPoint, otherEndPoint) < EndpointThreshold) + { + groupAnalyses.Add(otherAnalysis); + processed.Add(otherAnalysis.RouteId); + } + } + + if (groupAnalyses.Count > 0) + { + LogManager.Info($"[路径分组] 创建组,包含 {groupAnalyses.Count} 条路径"); + + try + { + var group = new EndpointGroup + { + GroupId = Guid.NewGuid().ToString("N").Substring(0, 8), + EndPoint = CalculateGroupCenter(groupAnalyses, routes), + PathAnalyses = groupAnalyses + }; + + // 设置组名称和终点描述 + var firstRoute = routes.FirstOrDefault(r => r.Id == groupAnalyses.First().RouteId); + if (firstRoute != null) + { + var firstEndPoint = firstRoute.GetEndPoint(); + group.GroupName = $"终点: {firstEndPoint?.Name ?? "未知"}"; + group.EndPointDescription = $"坐标: ({group.EndPoint.X:F1}, {group.EndPoint.Y:F1}, {group.EndPoint.Z:F1})"; + } + + groups.Add(group); + LogManager.Info($"[路径分组] 组创建成功: {group.GroupName}"); + } + catch (Exception ex) + { + LogManager.Error($"[路径分组] 创建组失败: {ex.Message}", ex); + } + } + else + { + LogManager.Warning($"[路径分组] 组内没有路径"); + } + } + + // 第三步:更新效率分数(基于组内最短路径)和排名 + foreach (var group in groups) + { + UpdateGroupStatistics(group); + + // 只更新效率分数和综合评分,保留其他固有属性 + foreach (var analysis in group.PathAnalyses) + { + var route = routes.FirstOrDefault(r => r.Id == analysis.RouteId); + if (route != null) + { + // 只重新计算效率(基于组内最短路径) + analysis.EfficiencyScore = CalculateEfficiencyScore(route, group.MinLength); + // 重新计算综合加权评分 + analysis.WeightedScore = CalculateWeightedScore(analysis, strategy); + } + analysis.GroupId = group.GroupId; + } + + // 排序并更新排名 + var sortedAnalyses = group.PathAnalyses + .OrderByDescending(a => a.WeightedScore) + .ToList(); + + for (int i = 0; i < sortedAnalyses.Count; i++) + { + sortedAnalyses[i].GroupRanking = i + 1; + } + + group.PathAnalyses = sortedAnalyses; + } + + return groups; + } + + /// + /// 检测碰撞热点 + /// + public List DetectHotspots(List collisions, double radius = HOTSPOT_RADIUS) + { + var hotspots = new List(); + + if (collisions == null || collisions.Count < 2) + return hotspots; + + var processed = new HashSet(); + + for (int i = 0; i < collisions.Count; i++) + { + if (processed.Contains(i)) + continue; + + var center = collisions[i].Center; + if (center == null) + continue; + + var nearbyCollisions = new List { collisions[i] }; + + // 查找范围内的其他碰撞 + for (int j = i + 1; j < collisions.Count; j++) + { + if (processed.Contains(j)) + continue; + + var otherCenter = collisions[j].Center; + if (otherCenter == null) + continue; + + if (Distance(center, otherCenter) <= radius) + { + nearbyCollisions.Add(collisions[j]); + processed.Add(j); + } + } + + // 如果达到阈值(≥2次),创建热点 + if (nearbyCollisions.Count >= 2) + { + var hotspot = new CollisionHotspot + { + Center = CalculateCenter(nearbyCollisions), + Radius = radius, + CollisionCount = nearbyCollisions.Count, + CollidedObjectNames = nearbyCollisions + .Select(c => GetSafeDisplayName(c.Item2)) + .Distinct() + .ToList() + }; + hotspots.Add(hotspot); + } + + processed.Add(i); + } + + LogManager.Debug($"[热点检测] 发现 {hotspots.Count} 个热点"); + return hotspots.OrderByDescending(h => h.CollisionCount).ToList(); + } + + /// + /// 计算加权综合评分 + /// + public double CalculateWeightedScore(PathDetailedAnalysis analysis, string strategy) + { + var weights = AnalysisStrategies.GetWeights(strategy); + + // 四维度加权计算:安全、效率、转弯、直达 + double weightedScore = + analysis.SafetyScore * weights[0] + + analysis.EfficiencyScore * weights[1] + + analysis.TurnDifficultyScore * weights[2] + + analysis.TortuosityScore * weights[3]; + + return Math.Round(weightedScore, 1); + } + + /// + /// 查找组内最佳路径 + /// + public PathDetailedAnalysis FindBestPathInGroup(EndpointGroup group, string strategy) + { + if (group?.PathAnalyses == null || group.PathAnalyses.Count == 0) + return null; + + // 如果只有一条路径,直接返回 + if (group.PathAnalyses.Count == 1) + return group.PathAnalyses[0]; + + // 重新计算加权评分 + foreach (var analysis in group.PathAnalyses) + { + analysis.WeightedScore = CalculateWeightedScore(analysis, strategy); + } + + // 按评分排序 + var sortedPaths = group.PathAnalyses.OrderByDescending(a => a.WeightedScore).ToList(); + + // 如果前两名评分接近(差距<5分),优先选碰撞少的 + if (sortedPaths.Count >= 2) + { + var first = sortedPaths[0]; + var second = sortedPaths[1]; + + if (Math.Abs(first.WeightedScore - second.WeightedScore) < 5) + { + if (second.CollisionCount < first.CollisionCount) + { + LogManager.Info($"[最佳路径] 评分接近,选择碰撞更少的路径: {second.RouteName}"); + return second; + } + } + } + + return sortedPaths[0]; + } + + /// + /// 生成优化建议 + /// + public List GenerateSuggestions(PathDetailedAnalysis analysis, EndpointGroup group) + { + var suggestions = new List(); + + // 1. 安全建议 + if (analysis.CollisionCount > 0) + { + if (analysis.HotspotCount > 0) + { + suggestions.Add(new CategorizedSuggestion + { + Category = SuggestionCategory.Safety, + Title = $"发现 {analysis.CollisionCount} 次碰撞({analysis.HotspotCount} 个热点)", + Description = $"路径 {analysis.RouteName} 存在 {analysis.HotspotCount} 个碰撞热点区域," + + $"建议重点检查热点位置并优化路径避让", + RelatedRouteId = analysis.RouteId, + RelatedRouteName = analysis.RouteName, + Priority = analysis.HotspotCount >= 2 ? 5 : 4 + }); + } + else + { + suggestions.Add(new CategorizedSuggestion + { + Category = SuggestionCategory.Safety, + Title = $"发现 {analysis.CollisionCount} 次分散碰撞", + Description = $"路径 {analysis.RouteName} 有 {analysis.CollisionCount} 次碰撞," + + $"建议优化路径避让", + RelatedRouteId = analysis.RouteId, + RelatedRouteName = analysis.RouteName, + Priority = 3 + }); + } + } + else + { + suggestions.Add(new CategorizedSuggestion + { + Category = SuggestionCategory.Safety, + Title = "无碰撞,安全性良好", + Description = $"路径 {analysis.RouteName} 未检测到碰撞", + RelatedRouteId = analysis.RouteId, + RelatedRouteName = analysis.RouteName, + Priority = 1 + }); + } + + // 2. 组内对比建议 + if (group?.PathAnalyses?.Count > 1) + { + var lengthDiff = group.GetLengthDiffPercent(analysis.RouteId); + var timeDiff = group.GetTimeDiffPercent(analysis.RouteId); + + if (lengthDiff > 30) + { + suggestions.Add(new CategorizedSuggestion + { + Category = SuggestionCategory.Efficiency, + Title = $"路径偏长(+{lengthDiff:F1}%)", + Description = $"该路径比组内最短路径长 {lengthDiff:F1}%," + + $"建议考虑更短路线", + RelatedRouteId = analysis.RouteId, + RelatedRouteName = analysis.RouteName, + Priority = 3 + }); + } + + // 推荐最佳路径 + if (analysis.IsBestInGroup) + { + suggestions.Add(new CategorizedSuggestion + { + Category = SuggestionCategory.GroupComparison, + Title = $"🏆 本组最佳路径推荐", + Description = $"路径 {analysis.RouteName} 是到达『{group.EndPointDescription}』的最佳选择," + + $"综合评分 {analysis.WeightedScore:F1} 分", + RelatedRouteId = analysis.RouteId, + RelatedRouteName = analysis.RouteName, + Priority = 5 + }); + } + } + + // 3. 转弯建议 + if (analysis.TurnDifficultyScore < 60) + { + suggestions.Add(new CategorizedSuggestion + { + Category = SuggestionCategory.TurnComplexity, + Title = "转弯难度较高", + Description = $"路径 {analysis.RouteName} 转弯难度分数较低," + + $"建议检查急转弯位置", + RelatedRouteId = analysis.RouteId, + RelatedRouteName = analysis.RouteName, + Priority = 3 + }); + } + + return suggestions.OrderByDescending(s => s.Priority).ToList(); + } + + #endregion + + #region 私有计算方法 + + /// + /// 计算安全性分数 + /// + private double CalculateSafetyScore(int collisionCount, int hotspotCount) + { + // 简单扣分制:每个碰撞扣1分,每个热点扣1分 + double penalty = collisionCount * 1.0 + hotspotCount * 1.0; + return Math.Max(0, 100 - penalty); + } + + /// + /// 计算效率分数 - 基于路径长度(越短越高效) + /// + public double CalculateEfficiencyScore(PathRoute route, double groupMinLength) + { + // 如果没有组内最短路径作为参考,使用当前路径长度 + double referenceLength = groupMinLength > 0 ? groupMinLength : route.TotalLength; + + if (referenceLength <= 0) + return 50; // 默认中等分数 + + // 计算与最短路径的差异率 + double lengthDiffPercent = (route.TotalLength - referenceLength) / referenceLength; + + // 长度效率分:相同为100分,每多10%扣10分 + double efficiencyScore = Math.Max(0, 100 - lengthDiffPercent * 100); + + return Math.Round(Math.Min(100, efficiencyScore), 1); + } + + /// + /// 计算转弯难度分数 + /// + private double CalculateTurnDifficultyScore(List edges) + { + if (edges == null || edges.Count == 0) + return 100; // 无转弯,满分 + + // 简单扣分制:每个圆弧转弯扣1分 + int arcEdgeCount = edges.Count(e => e.SegmentType == PathSegmentType.Arc); + return Math.Max(0, 100 - arcEdgeCount * 1.0); + } + + /// + /// 计算路径曲折度分数 + /// + private double CalculateTortuosityScore(PathRoute route) + { + var sortedPoints = route.GetSortedPoints(); + if (sortedPoints.Count < 2) + return 100; + + var startPoint = sortedPoints.First().Position; + var endPoint = sortedPoints.Last().Position; + + // 计算直线距离(模型单位) + double straightDistanceModelUnits = Distance(startPoint, endPoint); + // 转换为米(与 TotalLength 单位一致) + double straightDistanceMeters = NavisworksTransport.Utils.UnitsConverter.ConvertToMeters(straightDistanceModelUnits); + + if (straightDistanceMeters <= 0) + return 100; + + // 直达性 = 直线距离 / 实际路径长度 × 100(越接近100越好) + // 实际路径长度由 TotalLength 计算属性提供(已转换为米) + if (route.TotalLength <= 0.001) + return 100; + + double directness = straightDistanceMeters / route.TotalLength * 100; + double score = Math.Min(100, Math.Max(0, Math.Round(directness, 1))); + + LogManager.Info($"[直达性计算] {route.Name}: 直线距离={straightDistanceMeters:F2}m, 实际长度={route.TotalLength:F2}m, 直达性={score:F1}"); + + return score; + } + + /// + /// 计算冗余度分数 + /// + private double CalculateRedundancyScore(PathRoute route, List channels) + { + // 简单扣分制:基于通道窄宽缩减量扣分 + // 每缩减10%扣1分 + + // 如果路径没有车辆参数,使用配置默认值 + double vehicleWidth = route.MaxVehicleWidth > 0 ? route.MaxVehicleWidth : 1.0; + double safetyMargin = route.SafetyMargin > 0 ? route.SafetyMargin : 0.1; + double effectiveVehicleWidth = vehicleWidth + safetyMargin * 2; + + double channelWidth; + if (channels == null || channels.Count == 0) + { + var config = NavisworksTransport.Core.Config.ConfigManager.Instance.Current.Logistics; + channelWidth = config.WidthLimitMeters > 0 ? config.WidthLimitMeters : 3.0; + } + else + { + channelWidth = channels.Min(c => c.WidthLimit); + } + + // 计算宽度缩减率(正值表示有冗余,负值表示不足) + double widthMargin = (channelWidth - effectiveVehicleWidth) / effectiveVehicleWidth; + + // 每缩减10%扣1分(反向计算:越宽越高分) + double score = 100 - widthMargin * 100 * 0.1; + return Math.Min(100, Math.Max(0, score)); + } + + #endregion + + #region 辅助方法 + + /// + /// 获取路径的碰撞数据 + /// + private List GetCollisionsForRoute(string routeId) + { + try + { + // 获取最新的ClashDetective结果 + var clashResult = _database.GetLatestClashDetectiveResultByRouteId(routeId); + if (clashResult == null) + return new List(); + + // 获取碰撞对象 + var collisionObjects = _database.GetClashDetectiveCollisionObjects(clashResult.Id); + + // 转换为CollisionResult列表 + var collisions = new List(); + foreach (var obj in collisionObjects) + { + if (obj.Item1PosX.HasValue && obj.Item1PosY.HasValue && obj.Item1PosZ.HasValue) + { + collisions.Add(new CollisionResult + { + Center = new Point3D( + obj.Item1PosX.Value, + obj.Item1PosY.Value, + obj.Item1PosZ.Value), + Item2 = ModelItemProxyHelper.CreateProxy(obj.DisplayName) + }); + } + } + + return collisions; + } + catch (Exception ex) + { + LogManager.Error($"[获取碰撞数据] 路径 {routeId} 失败: {ex.Message}"); + return new List(); + } + } + + /// + /// 计算两点距离 + /// + private double Distance(Point3D p1, Point3D p2) + { + if (p1 == null || p2 == null) + return double.MaxValue; + + double dx = p1.X - p2.X; + double dy = p1.Y - p2.Y; + double dz = p1.Z - p2.Z; + + return Math.Sqrt(dx * dx + dy * dy + dz * dz); + } + + /// + /// 计算碰撞中心点 + /// + private Point3D CalculateCenter(List collisions) + { + if (collisions == null || collisions.Count == 0) + return new Point3D(0, 0, 0); + + double sumX = 0, sumY = 0, sumZ = 0; + int count = 0; + + foreach (var collision in collisions) + { + if (collision.Center != null) + { + sumX += collision.Center.X; + sumY += collision.Center.Y; + sumZ += collision.Center.Z; + count++; + } + } + + if (count == 0) + return new Point3D(0, 0, 0); + + return new Point3D(sumX / count, sumY / count, sumZ / count); + } + + /// + /// 计算组中心点(取所有路径终点的平均值) + /// + private Point3D CalculateGroupCenter(List analyses, List routes) + { + if (analyses == null || analyses.Count == 0 || routes == null) + return new Point3D(0, 0, 0); + + double sumX = 0, sumY = 0, sumZ = 0; + int count = 0; + + foreach (var analysis in analyses) + { + var route = routes.FirstOrDefault(r => r.Id == analysis.RouteId); + var endPoint = route?.GetEndPoint()?.Position; + if (endPoint != null) + { + sumX += endPoint.X; + sumY += endPoint.Y; + sumZ += endPoint.Z; + count++; + } + } + + if (count == 0) + return new Point3D(0, 0, 0); + + return new Point3D(sumX / count, sumY / count, sumZ / count); + } + + /// + /// 更新组内统计信息 + /// + private void UpdateGroupStatistics(EndpointGroup group) + { + if (group?.PathAnalyses == null || group.PathAnalyses.Count == 0) + return; + + // 更新排名 + var sorted = group.PathAnalyses.OrderByDescending(a => a.WeightedScore).ToList(); + for (int i = 0; i < sorted.Count; i++) + { + sorted[i].GroupRanking = i + 1; + } + + LogManager.Debug($"[组统计] 组 {group.GroupId} 共 {group.RouteCount} 条路径," + + $"最佳: {group.BestPath?.RouteName}"); + } + + /// + /// 设置默认分数(错误恢复) + /// + private void SetDefaultScores(PathDetailedAnalysis analysis) + { + analysis.SafetyScore = 50; + analysis.EfficiencyScore = 50; + analysis.TurnDifficultyScore = 50; + analysis.TortuosityScore = 50; + analysis.RedundancyScore = 50; + analysis.WeightedScore = 50; + } + + /// + /// 获取安全的显示名称 + /// + private string GetSafeDisplayName(ModelItem item) + { + if (item == null) + return "未知对象"; + + try + { + return item.DisplayName ?? "未命名对象"; + } + catch + { + return "未知对象"; + } + } + + #endregion + } + + /// + /// ModelItem代理类(用于当没有实际ModelItem时) + /// + internal static class ModelItemProxyHelper + { + public static ModelItem CreateProxy(string displayName) + { + // 这是一个简化实现,实际可能需要更复杂的处理 + // 这里仅用于存储显示名称 + return null; + } + } +} diff --git a/src/Core/PathAnalysisReportGenerator.cs b/src/Core/PathAnalysisReportGenerator.cs new file mode 100644 index 0000000..5d80bca --- /dev/null +++ b/src/Core/PathAnalysisReportGenerator.cs @@ -0,0 +1,574 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using NavisworksTransport.Core.Models; +using NavisworksTransport.Utils; + +namespace NavisworksTransport.Core +{ + /// + /// 路径分析报告HTML生成器 + /// 生成包含图表的专业HTML报告 + /// + public class PathAnalysisReportGenerator + { + private readonly string _chartJsPath; + + public PathAnalysisReportGenerator() + { + // Chart.js本地路径 + _chartJsPath = Path.Combine( + PathHelper.GetPluginDirectory(), + "resources", "js", "chart.min.js" + ); + } + + /// + /// 生成完整HTML报告 + /// + public string GenerateHtmlReport(PathAnalysisReportData data) + { + var html = new StringBuilder(); + + // HTML头部 + html.AppendLine(GenerateHtmlHead(data)); + + // 报告主体 + html.AppendLine(""); + html.AppendLine(GenerateHeader(data)); + html.AppendLine(GenerateExecutiveSummary(data)); + + // 如果有组数据,显示组分析 + if (data.Groups?.Count > 0) + { + foreach (var group in data.Groups) + { + html.AppendLine(GenerateGroupAnalysis(group)); + } + } + + html.AppendLine(GenerateAllPathsTable(data)); + html.AppendLine(GenerateSuggestions(data)); + html.AppendLine(GenerateTechnicalParameters(data)); + html.AppendLine(GenerateFooter(data)); + html.AppendLine(""); + html.AppendLine(""); + + return html.ToString(); + } + + /// + /// 保存报告到指定文件路径 + /// + /// 报告数据 + /// 完整的文件路径 + /// 是否保存成功 + public bool SaveReportToFile(PathAnalysisReportData data, string filePath) + { + try + { + // 确保目录存在 + string directory = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(directory)) + { + PathHelper.EnsureDirectoryExists(directory); + } + + string htmlContent = GenerateHtmlReport(data); + File.WriteAllText(filePath, htmlContent, Encoding.UTF8); + + LogManager.Info($"路径分析报告已保存: {filePath}"); + return true; + } + catch (Exception ex) + { + LogManager.Error($"保存路径分析报告失败: {ex.Message}", ex); + return false; + } + } + + #region HTML生成方法 + + private string GenerateHtmlHead(PathAnalysisReportData data) + { + var sb = new StringBuilder(); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine($" 路径分析报告 - {data.DocumentName}"); + sb.AppendLine(GenerateStyles()); + sb.AppendLine(GenerateChartJsInclude()); + sb.AppendLine(""); + return sb.ToString(); + } + + private string GenerateStyles() + { + return @" + "; + } + + private string GenerateChartJsInclude() + { + // 使用内联Chart.js,避免外部依赖 + // 如果本地存在则使用本地,否则使用CDN + if (File.Exists(_chartJsPath)) + { + string chartJs = File.ReadAllText(_chartJsPath); + return $""; + } + + // 使用Chart.js CDN作为备选 + return ""; + } + + private string GenerateHeader(PathAnalysisReportData data) + { + var sb = new StringBuilder(); + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine("

📊 物流路径规划分析报告

"); + sb.AppendLine("
"); + sb.AppendLine($"

文档: {EscapeHtml(data.DocumentName)}

"); + sb.AppendLine($"

生成时间: {data.ReportTime:yyyy年MM月dd日 HH:mm:ss}

"); + sb.AppendLine($"

分析策略: {data.Strategy} | 路径数量: {data.TotalPathCount} | 组数量: {data.GroupCount}

"); + sb.AppendLine("
"); + sb.AppendLine("
"); + return sb.ToString(); + } + + private string GenerateExecutiveSummary(PathAnalysisReportData data) + { + var sb = new StringBuilder(); + sb.AppendLine("
"); + sb.AppendLine("

🏆 推荐最佳路径

"); + + if (data.OverallBestPath != null) + { + var best = data.OverallBestPath; + sb.AppendLine($"
{EscapeHtml(best.RouteName)}
"); + sb.AppendLine("
"); + sb.AppendLine($" 长度: {best.TotalLength:F1}米 | "); + sb.AppendLine($" 碰撞: {best.CollisionCount}次 | "); + sb.AppendLine($" 综合评分: {best.WeightedScore:F1}分"); + sb.AppendLine("
"); + } + else + { + sb.AppendLine("
未找到推荐路径
"); + } + + sb.AppendLine("
"); + return sb.ToString(); + } + + private string GenerateGroupAnalysis(EndpointGroup group) + { + var sb = new StringBuilder(); + sb.AppendLine("
"); + sb.AppendLine($"
🎯 {EscapeHtml(group.GroupName)} - {group.EndPointDescription}
"); + sb.AppendLine($"

共 {group.RouteCount} 条路径到达此终点

"); + + // 生成雷达图 + if (group.BestPath != null) + { + string chartId = $"radarChart_{group.GroupId}"; + sb.AppendLine("
"); + sb.AppendLine($" "); + sb.AppendLine("
"); + sb.AppendLine(GenerateRadarChartScript(chartId, group.BestPath)); + } + + sb.AppendLine("
"); + return sb.ToString(); + } + + private string GenerateAllPathsTable(PathAnalysisReportData data) + { + var sb = new StringBuilder(); + sb.AppendLine("
"); + sb.AppendLine("
📋 所有路径详细数据
"); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + + foreach (var analysis in data.AllPathAnalyses.OrderByDescending(a => a.WeightedScore)) + { + string rowClass = analysis.IsBestInGroup ? "best-row" : ""; + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine(" "); + } + + sb.AppendLine(" "); + sb.AppendLine("
路径名称长度(m)时间(s)碰撞安全分效率分转弯分直达分综合分
{EscapeHtml(analysis.RouteName)}{analysis.TotalLength:F1}{analysis.CollisionCount}{GenerateScoreBar(analysis.SafetyScore)}{GenerateScoreBar(analysis.EfficiencyScore)}{GenerateScoreBar(analysis.TurnDifficultyScore)}{GenerateScoreBar(analysis.TortuosityScore)}{analysis.WeightedScore:F1}
"); + sb.AppendLine("
"); + return sb.ToString(); + } + + private string GenerateSuggestions(PathAnalysisReportData data) + { + var sb = new StringBuilder(); + sb.AppendLine("
"); + sb.AppendLine("
💡 优化建议
"); + + if (data.AllSuggestions?.Count > 0) + { + foreach (var suggestion in data.AllSuggestions.OrderByDescending(s => s.Priority)) + { + string priorityClass = suggestion.Priority >= 4 ? "high-priority" : + suggestion.Priority >= 3 ? "medium-priority" : "low-priority"; + string categoryColor = GetCategoryColor(suggestion.Category); + + sb.AppendLine($"
"); + sb.AppendLine($" {suggestion.CategoryName}"); + sb.AppendLine($"

{EscapeHtml(suggestion.Title)}

"); + sb.AppendLine($"

{EscapeHtml(suggestion.Description)}

"); + if (!string.IsNullOrEmpty(suggestion.RelatedRouteName)) + { + sb.AppendLine($" 相关路径: {EscapeHtml(suggestion.RelatedRouteName)}"); + } + sb.AppendLine("
"); + } + } + else + { + sb.AppendLine("

暂无优化建议

"); + } + + sb.AppendLine("
"); + return sb.ToString(); + } + + private string GenerateTechnicalParameters(PathAnalysisReportData data) + { + var sb = new StringBuilder(); + sb.AppendLine("
"); + sb.AppendLine("
⚙️ 技术参数
"); + + if (data.VehicleParams != null) + { + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine("
车辆长度
"); + sb.AppendLine($"
{data.VehicleParams.Length:F2}m
"); + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine("
车辆宽度
"); + sb.AppendLine($"
{data.VehicleParams.Width:F2}m
"); + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine("
车辆高度
"); + sb.AppendLine($"
{data.VehicleParams.Height:F2}m
"); + sb.AppendLine("
"); + sb.AppendLine("
"); + } + + sb.AppendLine("
"); + sb.AppendLine("

评分标准:

"); + sb.AppendLine("
    "); + sb.AppendLine("
  • 安全性: 基于碰撞数量和热点检测
  • "); + sb.AppendLine("
  • 效率: 基于路径长度比较
  • "); + sb.AppendLine("
  • 转弯难度: 基于转弯次数
  • "); + sb.AppendLine("
  • 直达性: 越高表示路径越直接
  • "); + sb.AppendLine("
"); + sb.AppendLine("
"); + + sb.AppendLine("
"); + return sb.ToString(); + } + + private string GenerateFooter(PathAnalysisReportData data) + { + var sb = new StringBuilder(); + sb.AppendLine("
"); + sb.AppendLine($"

NavisworksTransport 路径规划分析系统

"); + sb.AppendLine($"

报告生成时间: {data.ReportTime:yyyy-MM-dd HH:mm:ss}

"); + sb.AppendLine("
"); + sb.AppendLine("
"); // 关闭container + return sb.ToString(); + } + + #endregion + + #region 辅助方法 + + private string GenerateScoreBar(double score) + { + string barClass = score >= 80 ? "high" : score >= 60 ? "medium" : "low"; + return $"
{score:F0}
"; + } + + private string GenerateRadarChartScript(string chartId, PathDetailedAnalysis analysis) + { + var labels = new[] { "'安全性'", "'效率'", "'转弯难度'", "'直达性'" }; + var data = new[] { analysis.SafetyScore, analysis.EfficiencyScore, analysis.TurnDifficultyScore, analysis.TortuosityScore }; + + var sb = new StringBuilder(); + sb.AppendLine(""); + return sb.ToString(); + } + + private string GetCategoryColor(SuggestionCategory category) + { + switch (category) + { + case SuggestionCategory.Safety: return "#DC3545"; + case SuggestionCategory.Efficiency: return "#FFC107"; + case SuggestionCategory.TurnComplexity: return "#17A2B8"; + case SuggestionCategory.Redundancy: return "#6C757D"; + case SuggestionCategory.GroupComparison: return "#28A745"; + default: return "#2C5AA0"; + } + } + + private string EscapeHtml(string text) + { + if (string.IsNullOrEmpty(text)) + return ""; + return text + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace("\"", """) + .Replace("'", "'"); + } + + #endregion + } + + /// + /// 分析报告数据(与ViewModel数据兼容) + /// + public class PathAnalysisReportData + { + public DateTime ReportTime { get; set; } + public string Strategy { get; set; } + public int TotalPathCount { get; set; } + public int GroupCount { get; set; } + public List Groups { get; set; } + public List AllPathAnalyses { get; set; } + public PathDetailedAnalysis OverallBestPath { get; set; } + public List AllSuggestions { get; set; } + public string DocumentName { get; set; } + public VehicleParameters VehicleParams { get; set; } + } +} diff --git a/src/Core/PathAnalysisService.cs b/src/Core/PathAnalysisService.cs index 072a15e..4c20f04 100644 --- a/src/Core/PathAnalysisService.cs +++ b/src/Core/PathAnalysisService.cs @@ -1,20 +1,28 @@ using System; using System.Collections.Generic; using System.Linq; +using NavisworksTransport.Core; +using NavisworksTransport.Core.Models; +using NavisworksTransport.Core.Collision; namespace NavisworksTransport { /// - /// 路径分析服务 - 简化版 - /// 负责路径碰撞分析和评分计算 + /// 路径分析服务 - 扩展版 + /// 负责路径碰撞分析、多维度评分计算和同终点组分析 /// public class PathAnalysisService { private readonly PathDatabase _database; + private readonly PathAnalysisEngine _analysisEngine; public PathAnalysisService(PathDatabase database) { _database = database ?? throw new ArgumentNullException(nameof(database)); + _analysisEngine = new PathAnalysisEngine(database); + + // 确保数据库表结构已扩展 + _database.EnsureAnalysisResultsTableExtended(); } /// @@ -264,4 +272,135 @@ namespace NavisworksTransport /// 优化建议列表 public List Suggestions { get; set; } } + + #region 扩展分析功能 + + public class ExtendedPathAnalysisService + { + private readonly PathAnalysisEngine _engine; + private readonly PathDatabase _database; + + public ExtendedPathAnalysisService(PathDatabase database) + { + _database = database; + _engine = new PathAnalysisEngine(database); + } + + /// + /// 执行完整的多维度路径分析 + /// + public PathDetailedAnalysis AnalyzePathDetailed(PathRoute route, string strategy) + { + var context = new AnalysisContext { Strategy = strategy }; + return _engine.AnalyzePath(route, context); + } + + /// + /// 分析多条路径并按终点分组 + /// + public List AnalyzePathsWithGrouping(List routes, string strategy) + { + return _engine.AnalyzeAndGroupPaths(routes, strategy); + } + + /// + /// 获取同终点组的详细分析 + /// + public EndpointGroupAnalysis GetEndpointGroupAnalysis(EndpointGroup group, string strategy) + { + var analysis = new EndpointGroupAnalysis + { + Group = group, + BestPath = _engine.FindBestPathInGroup(group, strategy), + Suggestions = new List() + }; + + // 为组内每条路径生成建议 + foreach (var pathAnalysis in group.PathAnalyses) + { + var pathSuggestions = _engine.GenerateSuggestions(pathAnalysis, group); + analysis.Suggestions.AddRange(pathSuggestions); + } + + // 去重并排序 + analysis.Suggestions = analysis.Suggestions + .GroupBy(s => s.Title) + .Select(g => g.First()) + .OrderByDescending(s => s.Priority) + .ToList(); + + return analysis; + } + + /// + /// 保存详细分析结果到数据库 + /// + public void SaveDetailedAnalysis(PathDetailedAnalysis analysis) + { + var result = new PathAnalysisResult + { + RouteId = analysis.RouteId, + CollisionCount = analysis.CollisionCount, + SafetyScore = analysis.SafetyScore, + EfficiencyScore = analysis.EfficiencyScore, + TurnDifficultyScore = analysis.TurnDifficultyScore, + TortuosityScore = analysis.TortuosityScore, + RedundancyScore = analysis.RedundancyScore, + OverallScore = analysis.WeightedScore, + HotspotCount = analysis.HotspotCount, + AnalysisTime = analysis.AnalysisTime, + Strategy = analysis.Strategy, + GroupId = analysis.GroupId, + GroupRanking = analysis.GroupRanking + }; + + _database.SaveDetailedAnalysisResult(result); + } + + /// + /// 生成建议(供View使用) + /// + public List GenerateSuggestionsForView(PathDetailedAnalysis analysis, EndpointGroup group) + { + return _engine.GenerateSuggestions(analysis, group); + } + } + + /// + /// 同终点组详细分析结果 + /// + public class EndpointGroupAnalysis + { + /// 组信息 + public EndpointGroup Group { get; set; } + + /// 最佳路径 + public PathDetailedAnalysis BestPath { get; set; } + + /// 组内路径对比 + public List Comparisons { get; set; } = new List(); + + /// 优化建议 + public List Suggestions { get; set; } = new List(); + + /// 分析策略 + public string Strategy { get; set; } + } + + /// + /// 路径对比详情 + /// + public class PathComparisonDetail + { + public string PathAId { get; set; } + public string PathAName { get; set; } + public string PathBId { get; set; } + public string PathBName { get; set; } + public double LengthDiffPercent { get; set; } + public double TimeDiffPercent { get; set; } + public int CollisionDiff { get; set; } + public string Advantage { get; set; } + } + + #endregion } diff --git a/src/Core/PathCurveEngine.cs b/src/Core/PathCurveEngine.cs index 9a7da6c..dacc5c4 100644 --- a/src/Core/PathCurveEngine.cs +++ b/src/Core/PathCurveEngine.cs @@ -493,18 +493,17 @@ namespace NavisworksTransport } /// - /// 重新计算路径总长度 + /// 【已废弃】重新计算路径总长度 + /// + /// TotalLength 现在是计算属性,自动从 Edges/Points 实时计算,无需手动调用此方法。 + /// 保留此方法用于兼容,但实际不执行任何操作。 /// /// 路径 + [Obsolete("TotalLength 是计算属性,自动实时计算,无需调用此方法。")] public static void RecalculateRouteLength(PathRoute route) { - if (route == null || route.Edges == null) - { - route.TotalLength = 0; - return; - } - - route.TotalLength = UnitsConverter.ConvertToMeters(route.Edges.Sum(e => e.PhysicalLength)); + // 不再设置 route.TotalLength,因为它现在是只读计算属性 + // 长度会自动从 Edges 或 Points 计算 } } } \ No newline at end of file diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs index b531b35..69e2a92 100644 --- a/src/Core/PathDatabase.cs +++ b/src/Core/PathDatabase.cs @@ -57,9 +57,7 @@ namespace NavisworksTransport ExecuteNonQuery("PRAGMA journal_mode=WAL"); ExecuteNonQuery("PRAGMA foreign_keys=ON"); - // 无论数据库是否为新数据库,都调用CreateTables() - // 因为使用了CREATE TABLE IF NOT EXISTS语法,所以是安全的 - // 这样可以确保所有表都存在,避免在旧数据库中缺少新表的问题 + // 创建数据库表结构 CreateTables(); if (isNewDatabase) @@ -78,11 +76,14 @@ namespace NavisworksTransport private void CreateTables() { // 1. 路径基本信息表 + // 注意:TotalLength 字段已从数据库中删除 + // 路径长度由 PathRoute.TotalLength 计算属性实时计算: + // - 地面路径:从 Edges.PhysicalLength 累加(支持圆弧) + // - 空轨/吊装路径:从 Points 计算直线距离 ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS PathRoutes ( Id TEXT PRIMARY KEY, Name TEXT NOT NULL, - TotalLength REAL, EstimatedTime REAL, TurnRadius REAL, IsCurved INTEGER, @@ -114,7 +115,7 @@ namespace NavisworksTransport ) "); - // 3. 分析结果表 + // 3. 分析结果表(扩展版,包含5维度评分) ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS AnalysisResults ( RouteId TEXT PRIMARY KEY, @@ -124,6 +125,12 @@ namespace NavisworksTransport OverallScore REAL, AnalysisTime DATETIME, Strategy TEXT, + TurnDifficultyScore REAL, + TortuosityScore REAL, + RedundancyScore REAL, + HotspotCount INTEGER DEFAULT 0, + GroupId TEXT, + GroupRanking INTEGER DEFAULT 0, FOREIGN KEY(RouteId) REFERENCES PathRoutes(Id) ON DELETE CASCADE ) "); @@ -350,17 +357,18 @@ namespace NavisworksTransport try { // 保存路径基本信息 + // 注意:TotalLength 字段已从数据库中删除 + // 路径长度由 PathRoute.TotalLength 计算属性实时从 Edges/Points 计算 var sql = @" INSERT OR REPLACE INTO PathRoutes - (Id, Name, TotalLength, EstimatedTime, TurnRadius, IsCurved, MaxVehicleLength, MaxVehicleWidth, MaxVehicleHeight, SafetyMargin, GridSize, PathType, LiftHeightMeters, CreatedTime, LastModified) - VALUES (@id, @name, @length, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @created, @modified) + (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxVehicleLength, MaxVehicleWidth, MaxVehicleHeight, SafetyMargin, GridSize, PathType, LiftHeightMeters, CreatedTime, LastModified) + VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @created, @modified) "; using (var cmd = new SQLiteCommand(sql, _connection)) { cmd.Parameters.AddWithValue("@id", route.Id); cmd.Parameters.AddWithValue("@name", route.Name); - cmd.Parameters.AddWithValue("@length", route.TotalLength); cmd.Parameters.AddWithValue("@time", route.EstimatedTime); cmd.Parameters.AddWithValue("@turnRadius", route.TurnRadius); cmd.Parameters.AddWithValue("@isCurved", route.IsCurved ? 1 : 0); @@ -1168,7 +1176,6 @@ namespace NavisworksTransport } #endregion - /// /// 保存分析评分结果 /// @@ -1312,11 +1319,12 @@ namespace NavisworksTransport while (reader.Read()) { // 🔥 不使用默认值,数据缺失时抛出异常,避免隐藏bug + // 注意:TotalLength 字段已废弃,不再从数据库加载(由计算属性实时提供) var route = new PathRoute { Id = reader["Id"].ToString(), Name = reader["Name"].ToString(), - TotalLength = Convert.ToDouble(reader["TotalLength"]), + // TotalLength:不再从数据库加载,由 PathRoute.TotalLength 计算属性实时提供 EstimatedTime = Convert.ToDouble(reader["EstimatedTime"]), TurnRadius = Convert.ToDouble(reader["TurnRadius"]), IsCurved = Convert.ToInt32(reader["IsCurved"]) == 1, @@ -1393,6 +1401,8 @@ namespace NavisworksTransport route.Points.Add(point); } + + LogManager.Debug($"[LoadPathPoints] 路径 {route.Name} (Id={route.Id}) 加载了 {index} 个路径点"); } } } @@ -1583,6 +1593,8 @@ namespace NavisworksTransport } } + // 注意:TotalLength 是计算属性,自动从 Edges/Points 实时计算,无需手动设置 + // 重新计算SampledPoints(因为数据库中没有保存采样点) if (route.Edges.Count > 0) { @@ -2155,8 +2167,9 @@ namespace NavisworksTransport { using (var cmd = new SQLiteCommand(_connection)) { + // 注意:TotalLength 字段已从数据库中删除 cmd.CommandText = @" - SELECT Id, Name, CreatedTime, LastModified, TotalLength, + SELECT Id, Name, CreatedTime, LastModified, TurnRadius, IsCurved, MaxVehicleLength, MaxVehicleWidth, MaxVehicleHeight, SafetyMargin, GridSize, PathType, LiftHeightMeters FROM PathRoutes @@ -2173,7 +2186,7 @@ namespace NavisworksTransport Id = reader["Id"].ToString(), CreatedTime = DateTime.Parse(reader["CreatedTime"].ToString()), LastModified = DateTime.Parse(reader["LastModified"].ToString()), - TotalLength = Convert.ToDouble(reader["TotalLength"]), + // TotalLength:由 PathRoute.TotalLength 计算属性实时提供 TurnRadius = Convert.ToDouble(reader["TurnRadius"]), IsCurved = Convert.ToInt32(reader["IsCurved"]) == 1, MaxVehicleLength = Convert.ToDouble(reader["MaxVehicleLength"]), @@ -2589,6 +2602,158 @@ namespace NavisworksTransport _connection?.Dispose(); } } + + #region 路径分析扩展方法 + + /// + /// 获取路径最新的ClashDetective检测结果 + /// + public ClashDetectiveResultRecord GetLatestClashDetectiveResultByRouteId(string routeId) + { + try + { + var sql = @" + SELECT cdr.Id, cdr.TestName, cdr.RouteId, pr.Name AS PathName, cdr.TestTime, + cdr.CollisionCount, cdr.AnimationCollisionCount, + cdr.FrameRate, cdr.Duration, cdr.DetectionGap, cdr.AnimatedObjectName, cdr.CreatedAt + FROM ClashDetectiveResults cdr + INNER JOIN PathRoutes pr ON cdr.RouteId = pr.Id + WHERE cdr.RouteId = @routeId + ORDER BY cdr.TestTime DESC + LIMIT 1 + "; + + using (var cmd = new SQLiteCommand(sql, _connection)) + { + cmd.Parameters.AddWithValue("@routeId", routeId ?? ""); + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + return new ClashDetectiveResultRecord + { + Id = Convert.ToInt32(reader["Id"]), + TestName = reader["TestName"].ToString(), + PathName = reader["PathName"].ToString(), + RouteId = reader["RouteId"].ToString(), + TestTime = Convert.ToDateTime(reader["TestTime"]), + CollisionCount = Convert.ToInt32(reader["CollisionCount"]), + AnimationCollisionCount = Convert.ToInt32(reader["AnimationCollisionCount"]), + FrameRate = Convert.ToInt32(reader["FrameRate"]), + Duration = Convert.ToDouble(reader["Duration"]), + DetectionGap = Convert.ToDouble(reader["DetectionGap"]), + AnimatedObjectName = reader["AnimatedObjectName"].ToString(), + CreatedAt = Convert.ToDateTime(reader["CreatedAt"]) + }; + } + } + } + } + catch (Exception ex) + { + LogManager.Error($"获取路径最新ClashDetective结果失败: {ex.Message}", ex); + } + + return null; + } + + /// + /// 保存详细分析结果(扩展字段) + /// + public void SaveDetailedAnalysisResult(PathAnalysisResult result) + { + try + { + var sql = @" + INSERT OR REPLACE INTO AnalysisResults + (RouteId, CollisionCount, SafetyScore, EfficiencyScore, OverallScore, AnalysisTime, Strategy, + TurnDifficultyScore, TortuosityScore, RedundancyScore, HotspotCount, GroupId, GroupRanking) + VALUES (@routeId, @collisionCount, @safetyScore, @efficiencyScore, @overallScore, @analysisTime, @strategy, + @turnDifficultyScore, @tortuosityScore, @redundancyScore, @hotspotCount, @groupId, @groupRanking) + "; + + using (var cmd = new SQLiteCommand(sql, _connection)) + { + cmd.Parameters.AddWithValue("@routeId", result.RouteId); + cmd.Parameters.AddWithValue("@collisionCount", result.CollisionCount); + cmd.Parameters.AddWithValue("@safetyScore", result.SafetyScore); + cmd.Parameters.AddWithValue("@efficiencyScore", result.EfficiencyScore); + cmd.Parameters.AddWithValue("@overallScore", result.OverallScore); + cmd.Parameters.AddWithValue("@analysisTime", result.AnalysisTime); + cmd.Parameters.AddWithValue("@strategy", result.Strategy ?? "平衡模式"); + cmd.Parameters.AddWithValue("@turnDifficultyScore", result.TurnDifficultyScore); + cmd.Parameters.AddWithValue("@tortuosityScore", result.TortuosityScore); + cmd.Parameters.AddWithValue("@redundancyScore", result.RedundancyScore); + cmd.Parameters.AddWithValue("@hotspotCount", result.HotspotCount); + cmd.Parameters.AddWithValue("@groupId", result.GroupId ?? (object)DBNull.Value); + cmd.Parameters.AddWithValue("@groupRanking", result.GroupRanking); + + cmd.ExecuteNonQuery(); + } + + LogManager.Debug($"保存详细分析结果: RouteId={result.RouteId}, OverallScore={result.OverallScore:F1}"); + } + catch (Exception ex) + { + LogManager.Error($"保存详细分析结果失败: {ex.Message}", ex); + throw; + } + } + + /// + /// 检查并扩展 AnalysisResults 表结构(用于升级现有数据库) + /// + public void EnsureAnalysisResultsTableExtended() + { + try + { + // 检查表是否存在扩展字段 + var checkSql = "PRAGMA table_info(AnalysisResults)"; + var existingColumns = new HashSet(); + + using (var cmd = new SQLiteCommand(checkSql, _connection)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + existingColumns.Add(reader["name"].ToString().ToLower()); + } + } + + // 需要添加的字段 + var columnsToAdd = new Dictionary + { + ["turndifficultyscore"] = "ALTER TABLE AnalysisResults ADD COLUMN TurnDifficultyScore REAL", + ["tortuosityscore"] = "ALTER TABLE AnalysisResults ADD COLUMN TortuosityScore REAL", + ["redundancyscore"] = "ALTER TABLE AnalysisResults ADD COLUMN RedundancyScore REAL", + ["hotspotcount"] = "ALTER TABLE AnalysisResults ADD COLUMN HotspotCount INTEGER DEFAULT 0", + ["groupid"] = "ALTER TABLE AnalysisResults ADD COLUMN GroupId TEXT", + ["groupranking"] = "ALTER TABLE AnalysisResults ADD COLUMN GroupRanking INTEGER DEFAULT 0" + }; + + foreach (var column in columnsToAdd) + { + if (!existingColumns.Contains(column.Key)) + { + try + { + ExecuteNonQuery(column.Value); + LogManager.Info($"扩展 AnalysisResults 表: 添加字段 {column.Key}"); + } + catch (Exception ex) + { + LogManager.Warning($"添加字段 {column.Key} 可能已存在: {ex.Message}"); + } + } + } + } + catch (Exception ex) + { + LogManager.Error($"扩展 AnalysisResults 表失败: {ex.Message}", ex); + } + } + + #endregion } /// @@ -2654,7 +2819,7 @@ namespace NavisworksTransport } /// - /// 路径分析结果数据模型 + /// 路径分析结果数据模型(扩展版) /// public class PathAnalysisResult { @@ -2665,6 +2830,14 @@ namespace NavisworksTransport public double OverallScore { get; set; } public DateTime AnalysisTime { get; set; } public string Strategy { get; set; } + + // 扩展字段 + public double TurnDifficultyScore { get; set; } + public double TortuosityScore { get; set; } + public double RedundancyScore { get; set; } + public int HotspotCount { get; set; } + public string GroupId { get; set; } + public int GroupRanking { get; set; } } #region 排除列表数据模型 @@ -2686,4 +2859,6 @@ namespace NavisworksTransport } #endregion + + } diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs index d97c6a1..0e12ae2 100644 --- a/src/Core/PathPlanningManager.cs +++ b/src/Core/PathPlanningManager.cs @@ -1260,8 +1260,7 @@ namespace NavisworksTransport { route.Points.Add(point); } - // 更新其他属性 - route.TotalLength = loadedRoute.TotalLength; + // 更新其他属性(不要覆盖TotalLength,因为LoadPathEdges已经根据Edges重新计算了) route.LiftHeightMeters = loadedRoute.LiftHeightMeters; LogManager.Debug($"已从数据库加载路径数据: {route.Name}, 共 {loadedRoute.Points.Count} 个点"); } @@ -1714,8 +1713,7 @@ namespace NavisworksTransport // 优化路径点:处理斜线和清除多余点 OptimizeRightAnglePathPoints(CurrentRoute); - // 重新计算路径长度 - CurrentRoute.RecalculateLength(); + // 注意:TotalLength 现在是计算属性,自动从几何数据计算,无需手动更新 // 重新渲染路径 _renderPlugin?.RenderPath(CurrentRoute); @@ -2185,9 +2183,8 @@ namespace NavisworksTransport if (hasChanges) { - // 重新计算路径长度 - route.RecalculateLength(); - LogManager.Info($"[直角路径优化] 路径点优化完成,当前点数: {points.Count}"); + // 注意:TotalLength 现在是计算属性,自动从几何数据计算,无需手动更新 + LogManager.Info($"[直角路径优化] 路径点优化完成,当前点数: {points.Count}, 路径长度: {route.TotalLength:F2}米"); } return hasChanges; @@ -4217,8 +4214,8 @@ namespace NavisworksTransport route.AddPoint(pathPoint); } - // 计算路径长度 - route.RecalculateLength(); + // 注意:AddPoint已经调用RecalculateAndSaveRoute计算了曲线化长度 + // 不需要再调用RecalculateLength(),否则会覆盖正确的曲线长度 LogManager.Info($"创建自动路径: {routeName}, 点数: {pathPoints.Count}, 长度: {route.TotalLength:F2}米"); return route; diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs index 917bed0..41b39ef 100644 --- a/src/Core/PathPlanningModels.cs +++ b/src/Core/PathPlanningModels.cs @@ -517,9 +517,67 @@ namespace NavisworksTransport public List AssociatedChannelIds { get; set; } /// - /// 路径总长度(米) + /// 路径总长度(米)- 只读计算属性 + /// + /// 【重要】此属性实时从几何数据计算,不从数据库存储或加载。 + /// 数据库中的 TotalLength 字段已废弃,保留仅用于兼容旧版本。 + /// + /// 计算方式: + /// - 地面路径(Ground):从 Edges.PhysicalLength 累加(支持圆弧) + /// - 空轨路径(Rail):从 Points 计算直线距离 + /// - 吊装路径(Hoisting):从 Points 计算直线距离 /// - public double TotalLength { get; set; } + public double TotalLength + { + get + { + if (Points.Count < 2) return 0.0; + + // 地面路径:优先使用 Edges(支持圆弧),否则从 Points 计算 + if (PathType == PathType.Ground) + { + if (Edges.Count > 0) + { + double lengthInModelUnits = Edges.Sum(e => e.PhysicalLength); + return NavisworksTransport.Utils.UnitsConverter.ConvertToMeters(lengthInModelUnits); + } + return CalculateLengthFromPoints(); + } + + // 空轨/吊装路径:使用直线距离 + if (PathType == PathType.Rail || PathType == PathType.Hoisting) + { + return CalculateLengthFromPoints(); + } + + // 未知类型:默认直线计算 + return CalculateLengthFromPoints(); + } + } + + /// + /// 从路径点计算直线距离总长度(辅助方法) + /// + private double CalculateLengthFromPoints() + { + double totalLength = 0.0; + var sortedPoints = GetSortedPoints(); + + for (int i = 0; i < sortedPoints.Count - 1; i++) + { + var p1 = sortedPoints[i].Position; + var p2 = sortedPoints[i + 1].Position; + + double dx = p2.X - p1.X; + double dy = p2.Y - p1.Y; + double dz = p2.Z - p1.Z; + double distanceInModelUnits = Math.Sqrt(dx*dx + dy*dy + dz*dz); + + totalLength += NavisworksTransport.Utils.UnitsConverter.ConvertToMeters(distanceInModelUnits); + } + + return totalLength; + } /// /// 创建时间 @@ -640,7 +698,7 @@ namespace NavisworksTransport Id = Guid.NewGuid().ToString(); EstimatedTime = 0.0; AssociatedChannelIds = new List(); - TotalLength = 0.0; + // TotalLength:计算属性,自动从几何数据计算,无需初始化 CreatedTime = DateTime.Now; LastModified = DateTime.Now; Description = string.Empty; @@ -660,7 +718,7 @@ namespace NavisworksTransport Id = Guid.NewGuid().ToString(); EstimatedTime = 0.0; AssociatedChannelIds = new List(); - TotalLength = 0.0; + // TotalLength:计算属性,自动从几何数据计算,无需初始化 CreatedTime = DateTime.Now; TurnRadius = 0.0; // 0.0 表示未设置,实际使用时从配置获取默认值 IsCurved = false; @@ -762,38 +820,6 @@ namespace NavisworksTransport return sortedPoints; } - /// - /// 重新计算路径长度 - /// - public void RecalculateLength() - { - UpdateTotalLength(); - } - - /// - /// 更新路径总长度 - /// - private void UpdateTotalLength() - { - TotalLength = 0.0; - var sortedPoints = GetSortedPoints(); - - for (int i = 0; i < sortedPoints.Count - 1; i++) - { - var point1 = sortedPoints[i].Position; - var point2 = sortedPoints[i + 1].Position; - - // 计算两点间距离(模型单位) - double dx = point2.X - point1.X; - double dy = point2.Y - point1.Y; - double dz = point2.Z - point1.Z; - double distanceInModelUnits = Math.Sqrt(dx * dx + dy * dy + dz * dz); - - // 转换为米并累加 - TotalLength += NavisworksTransport.Utils.UnitsConverter.ConvertToMeters(distanceInModelUnits); - } - } - /// /// 重新计算路径数据并保存到数据库 /// 统一的路径数据更新入口:重新设置索引、更新长度、计算曲线、保存到数据库 @@ -839,7 +865,7 @@ namespace NavisworksTransport // 空轨/吊装路径:使用直线,不需要曲线化 Edges.Clear(); IsCurved = false; - UpdateTotalLength(); + // 注意:TotalLength 现在是计算属性,自动从 Points 计算 string pathTypeName = PathType == PathType.Rail ? "空轨" : "吊装"; LogManager.Info($"{pathTypeName}路径已更新(直线模式): {Name}, 原因: {context}"); } @@ -848,7 +874,7 @@ namespace NavisworksTransport // 未知路径类型:使用直线 Edges.Clear(); IsCurved = false; - UpdateTotalLength(); + // 注意:TotalLength 现在是计算属性,自动从 Points 计算 LogManager.Warning($"未知路径类型,使用直线模式: {Name}, 原因: {context}"); } @@ -895,7 +921,7 @@ namespace NavisworksTransport { Id = Guid.NewGuid().ToString(), EstimatedTime = EstimatedTime, - TotalLength = TotalLength, + // TotalLength:计算属性,自动从几何数据计算,克隆时会重新计算 CreatedTime = CreatedTime, LastModified = DateTime.Now, Description = Description, diff --git a/src/UI/WPF/ViewModels/PathAnalysisViewModel.cs b/src/UI/WPF/ViewModels/PathAnalysisViewModel.cs index caca1ac..089374e 100644 --- a/src/UI/WPF/ViewModels/PathAnalysisViewModel.cs +++ b/src/UI/WPF/ViewModels/PathAnalysisViewModel.cs @@ -1,41 +1,52 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Windows.Input; +using Autodesk.Navisworks.Api; +using NavisworksTransport.Core; +using NavisworksTransport.Core.Models; namespace NavisworksTransport.UI.WPF.ViewModels { /// - /// 路径分析视图模型 - /// 用于路径规划分析对话框的数据绑定和业务逻辑 + /// 路径分析视图模型 - 扩展版 + /// 支持多维度评分和同终点组分析 /// public class PathAnalysisViewModel : INotifyPropertyChanged { #region 字段 - private string _analysisStrategy = "安全优先"; + private string _analysisStrategy = AnalysisStrategies.Balanced; private bool _isAnalyzing = false; private PathAnalysisItem _recommendedPath; private string _analysisStatus = "准备就绪"; + private EndpointGroupViewModel _selectedGroup; + private PathDetailedAnalysis _selectedPathAnalysis; #endregion #region 属性 + /// + /// 可用策略列表 + /// + public List AvailableStrategies => AnalysisStrategies.GetAllStrategies(); + /// /// 可用路径列表 /// public ObservableCollection AvailablePaths { get; set; } /// - /// 选中的路径列表(用于对比分析和结果显示) + /// 选中的路径列表 /// public ObservableCollection SelectedPaths { get; set; } /// - /// 分析策略(安全优先/效率优先) + /// 分析策略 /// public string AnalysisStrategy { @@ -44,7 +55,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels { _analysisStrategy = value; OnPropertyChanged(); - // 策略改变时重新计算推荐路径 UpdateRecommendedPath(); } } @@ -89,52 +99,68 @@ namespace NavisworksTransport.UI.WPF.ViewModels } /// - /// 优化建议列表 + /// 同终点组列表 /// - public ObservableCollection OptimizationSuggestions { get; set; } + public ObservableCollection EndpointGroups { get; set; } /// - /// 瓶颈位置列表 + /// 当前选中的组 /// - public ObservableCollection BottleneckLocations { get; set; } + public EndpointGroupViewModel SelectedGroup + { + get => _selectedGroup; + set + { + _selectedGroup = value; + OnPropertyChanged(); + UpdateSelectedGroupDetails(); + } + } /// - /// 开始分析命令 + /// 当前选中的路径详细分析 + /// + public PathDetailedAnalysis SelectedPathAnalysis + { + get => _selectedPathAnalysis; + set + { + _selectedPathAnalysis = value; + OnPropertyChanged(); + } + } + + /// + /// 优化建议列表(分类) + /// + public ObservableCollection CategorizedSuggestions { get; set; } + + /// + /// 四维度分数 + /// + public ObservableCollection DimensionScores { get; set; } + + /// + /// 命令 /// public ICommand StartAnalysisCommand { get; private set; } - - /// - /// 全选命令 - /// public ICommand SelectAllCommand { get; private set; } - - /// - /// 清空选择命令 - /// public ICommand ClearSelectionCommand { get; private set; } - - /// - /// 导出报告命令 - /// public ICommand ExportReportCommand { get; private set; } + public ICommand SelectGroupCommand { get; private set; } + public ICommand HighlightBestPathCommand { get; private set; } #endregion #region 构造函数 - /// - /// 初始化路径分析视图模型 - /// public PathAnalysisViewModel() { try { InitializeCollections(); InitializeCommands(); - - // 确保PathPlanningManager的数据库已初始化 EnsurePathDatabaseInitialized(); - LoadPathData(); LogManager.Info("PathAnalysisViewModel初始化完成"); } @@ -144,30 +170,25 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } - /// - /// 初始化命令 - /// + private void InitializeCollections() + { + AvailablePaths = new ObservableCollection(); + SelectedPaths = new ObservableCollection(); + EndpointGroups = new ObservableCollection(); + CategorizedSuggestions = new ObservableCollection(); + DimensionScores = new ObservableCollection(); + } + private void InitializeCommands() { StartAnalysisCommand = new RelayCommand(ExecuteStartAnalysis, CanExecuteStartAnalysis); SelectAllCommand = new RelayCommand(SelectAll); ClearSelectionCommand = new RelayCommand(ClearSelection); ExportReportCommand = new RelayCommand(ExportReport, CanExportReport); + SelectGroupCommand = new RelayCommand(SelectGroup); + HighlightBestPathCommand = new RelayCommand(HighlightBestPath, CanHighlightBestPath); } - private bool CanExecuteStartAnalysis() - { - return !IsAnalyzing && AvailablePaths.Any(p => p.IsSelected); - } - - private void ExecuteStartAnalysis() - { - PerformAnalysis(); - } - - /// - /// 确保PathPlanningManager的路径数据库已初始化 - /// private void EnsurePathDatabaseInitialized() { try @@ -183,40 +204,20 @@ namespace NavisworksTransport.UI.WPF.ViewModels #endregion - #region 初始化方法 + #region 数据加载 - /// - /// 初始化集合 - /// - private void InitializeCollections() - { - AvailablePaths = new ObservableCollection(); - SelectedPaths = new ObservableCollection(); - OptimizationSuggestions = new ObservableCollection(); - BottleneckLocations = new ObservableCollection(); - } - - /// - /// 加载真实路径数据 - /// private void LoadPathData() { try { - // 从PathPlanningManager获取所有路径(包括数据库中的历史路径) var allRoutes = PathPlanningManager.Instance.GetAllRoutes(); - if (allRoutes == null || allRoutes.Count == 0) { - LogManager.Info("当前没有可用的路径数据"); AnalysisStatus = "暂无可用路径数据,请先规划路径"; return; } - // 清空现有数据 AvailablePaths.Clear(); - - // 将路径数据转换为UI显示项 foreach (var route in allRoutes) { var item = new PathAnalysisItem @@ -226,30 +227,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels Type = route.CreatedTime != default(DateTime) ? "历史路径" : "当前会话", Length = Math.Round(route.TotalLength, 2), EstimatedTime = (int)route.EstimatedTime, - IsSelected = false + IsSelected = false, + CollisionCount = route.CollisionCount ?? 0, + Score = (int)(route.OverallScore ?? 0) }; - // 如果数据库中有分析结果,加载相关数据 - if (route.CollisionCount.HasValue) - { - item.CollisionCount = route.CollisionCount.Value; - item.Score = (int)route.OverallScore.GetValueOrDefault(0); - item.AffectedObjects = route.CollisionCount.Value; // 简化:使用碰撞数作为受影响对象数 - } - else - { - item.CollisionCount = 0; - item.Score = 0; - item.AffectedObjects = 0; - } - - // 计算平均速度 if (route.EstimatedTime > 0) { item.AverageSpeed = Math.Round(route.TotalLength / route.EstimatedTime, 2); } - // 监听选择状态变化,以刷新命令状态 item.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(PathAnalysisItem.IsSelected)) @@ -261,101 +248,31 @@ namespace NavisworksTransport.UI.WPF.ViewModels AvailablePaths.Add(item); } - LogManager.Info($"加载路径数据完成:共{AvailablePaths.Count}条路径(包括历史路径)"); AnalysisStatus = $"已加载 {AvailablePaths.Count} 条路径,请选择要分析的路径"; } catch (Exception ex) { - LogManager.Error($"加载真实路径数据失败: {ex.Message}", ex); + LogManager.Error($"加载路径数据失败: {ex.Message}", ex); AnalysisStatus = "加载路径数据失败"; } } - #endregion - #region 业务逻辑方法 + #region 命令执行 - /// - /// 全选所有路径 - /// - private void SelectAll() + private bool CanExecuteStartAnalysis() { - foreach (var path in AvailablePaths) - { - path.IsSelected = true; - } - CommandManager.InvalidateRequerySuggested(); + return !IsAnalyzing && AvailablePaths.Any(p => p.IsSelected); } - /// - /// 清空所有选择 - /// - private void ClearSelection() - { - foreach (var path in AvailablePaths) - { - path.IsSelected = false; - } - CommandManager.InvalidateRequerySuggested(); - } - - /// - /// 更新推荐路径 - /// - private void UpdateRecommendedPath() - { - try - { - if (!SelectedPaths.Any()) return; - - PathAnalysisItem recommended = null; - - switch (AnalysisStrategy) - { - case "安全优先": - // 优先选择碰撞次数最少的路径 - recommended = SelectedPaths.OrderBy(p => p.CollisionCount) - .ThenBy(p => p.AffectedObjects) - .FirstOrDefault(); - break; - - case "效率优先": - // 优先选择路径最短的路径 - recommended = SelectedPaths.OrderBy(p => p.Length) - .ThenBy(p => p.EstimatedTime) - .FirstOrDefault(); - break; - - default: - // 默认按综合评分选择 - recommended = SelectedPaths.OrderByDescending(p => p.Score) - .FirstOrDefault(); - break; - } - - RecommendedPath = recommended; - AnalysisStatus = recommended != null ? $"推荐方案: {recommended.Name}" : "暂无推荐"; - - LogManager.Info($"根据{AnalysisStrategy}策略更新推荐路径: {recommended?.Name}"); - } - catch (Exception ex) - { - LogManager.Error($"更新推荐路径失败: {ex.Message}", ex); - } - } - - /// - /// 执行路径分析 - /// - public void PerformAnalysis() + private void ExecuteStartAnalysis() { try { IsAnalyzing = true; AnalysisStatus = "正在分析..."; - // 获取选中的路径 var selectedRoutes = AvailablePaths.Where(p => p.IsSelected).ToList(); if (selectedRoutes.Count == 0) { @@ -364,31 +281,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels return; } - // 更新选中路径列表用于UI显示 - SelectedPaths.Clear(); - foreach (var route in selectedRoutes) - { - SelectedPaths.Add(route); - } + // 获取完整路径对象 + var allRoutes = PathPlanningManager.Instance.GetAllRoutes(); + var routesToAnalyze = allRoutes.Where(r => selectedRoutes.Any(s => s.RouteId == r.Id)).ToList(); - // 执行真实的路径分析 - var result = PathPlanningManager.Instance.AnalyzePathsAndCompare( - selectedRoutes.Select(p => p.RouteId).ToList(), - AnalysisStrategy - ); + // 执行分析 + var analysisService = new ExtendedPathAnalysisService(PathPlanningManager.Instance.GetPathDatabase()); + var groups = analysisService.AnalyzePathsWithGrouping(routesToAnalyze, AnalysisStrategy); - if (result != null) - { - // 更新UI数据 - UpdateAnalysisResults(result); - AnalysisStatus = "分析完成"; - LogManager.Info($"路径分析完成,最佳路径: {result.BestRouteName}"); - } - else - { - AnalysisStatus = "分析失败"; - } + // 更新UI + UpdateAnalysisResults(groups, selectedRoutes); + AnalysisStatus = "分析完成"; IsAnalyzing = false; } catch (Exception ex) @@ -399,146 +303,278 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } - /// - /// 导出报告 - /// - private void ExportReport() + private void UpdateAnalysisResults(List groups, List selectedItems) { - try + LogManager.Info($"[路径分析UI] 开始更新结果: {groups.Count}个组, {selectedItems.Count}条选中路径"); + + // 清空现有数据 + EndpointGroups.Clear(); + SelectedPaths.Clear(); + CategorizedSuggestions.Clear(); + + // 更新选中路径 + foreach (var item in selectedItems) { - if (RecommendedPath == null || SelectedPaths.Count == 0) + SelectedPaths.Add(item); + } + + // 更新组信息 + foreach (var group in groups) + { + var groupVm = new EndpointGroupViewModel { - LogManager.Warning("没有分析结果可以导出"); - return; - } + GroupId = group.GroupId, + GroupName = group.GroupName, + EndPointDescription = group.EndPointDescription, + RouteCount = group.RouteCount, + PathAnalyses = new ObservableCollection(group.PathAnalyses), + BestPath = group.BestPath + }; + EndpointGroups.Add(groupVm); - // 生成报告文件名 - var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); - var fileName = $"路径分析报告_{timestamp}.txt"; - var filePath = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Desktop), - fileName - ); - - // 生成报告内容 - var report = new System.Text.StringBuilder(); - report.AppendLine("===================================="); - report.AppendLine(" 物流路径分析报告"); - report.AppendLine("===================================="); - report.AppendLine(); - report.AppendLine($"生成时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}"); - report.AppendLine($"分析策略:{AnalysisStrategy}"); - report.AppendLine(); - - report.AppendLine("【分析路径列表】"); - report.AppendLine("----------------------------------------"); - foreach (var path in SelectedPaths) + // 收集建议 + foreach (var analysis in group.PathAnalyses) { - report.AppendLine($"路径名称:{path.Name}"); - report.AppendLine($" - 长度:{path.Length:F2} 米"); - report.AppendLine($" - 预估时间:{path.EstimatedTime} 秒"); - report.AppendLine($" - 碰撞数量:{path.CollisionCount}"); - report.AppendLine($" - 综合评分:{path.Score}"); - report.AppendLine(); - } - - report.AppendLine("【推荐路径】"); - report.AppendLine("----------------------------------------"); - if (RecommendedPath != null) - { - report.AppendLine($"推荐路径:{RecommendedPath.Name}"); - report.AppendLine($"推荐理由:基于{AnalysisStrategy}策略,该路径具有最优综合评分"); - report.AppendLine(); - } - - report.AppendLine("【优化建议】"); - report.AppendLine("----------------------------------------"); - if (OptimizationSuggestions.Count > 0) - { - int index = 1; - foreach (var suggestion in OptimizationSuggestions) + var service = new ExtendedPathAnalysisService(PathPlanningManager.Instance.GetPathDatabase()); + var suggestions = service.GenerateSuggestionsForView(analysis, group); + foreach (var suggestion in suggestions) { - report.AppendLine($"{index}. {suggestion.Title}"); - index++; + CategorizedSuggestions.Add(new CategorizedSuggestionViewModel + { + Category = suggestion.Category.ToString(), + CategoryName = GetCategoryDisplayName(suggestion.Category), + Title = suggestion.Title, + Description = suggestion.Description, + RelatedRouteName = suggestion.RelatedRouteName, + Priority = suggestion.Priority + }); } } + } + + // 设置推荐路径 + var overallBest = groups.Select(g => g.BestPath).OrderByDescending(p => p.WeightedScore).FirstOrDefault(); + if (overallBest != null) + { + var bestItem = selectedItems.FirstOrDefault(i => i.RouteId == overallBest.RouteId); + if (bestItem != null) + { + // 更新分数为分析后的综合评分 + bestItem.Score = (int)overallBest.WeightedScore; + bestItem.CollisionCount = overallBest.CollisionCount; + LogManager.Info($"[路径分析UI] 设置推荐路径: {bestItem.Name}, 评分={bestItem.Score}"); + } else { - report.AppendLine("暂无优化建议"); + LogManager.Warning($"[路径分析UI] 未找到推荐路径对应的选中项: RouteId={overallBest.RouteId}"); } - - // 写入文件 - System.IO.File.WriteAllText(filePath, report.ToString(), System.Text.Encoding.UTF8); - - AnalysisStatus = $"报告已导出到:{filePath}"; - LogManager.Info($"分析报告已导出:{filePath}"); - - // 打开文件所在目录并选中文件 - System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{filePath}\""); + RecommendedPath = bestItem; + + // 更新雷达图数据 + UpdateDimensionScores(overallBest); } - catch (Exception ex) + else { - LogManager.Error($"导出报告失败:{ex.Message}", ex); - AnalysisStatus = "导出报告失败"; + LogManager.Warning("[路径分析UI] 未找到 overallBest"); + } + + // 默认选中第一个组 + if (EndpointGroups.Count > 0) + { + SelectedGroup = EndpointGroups[0]; + LogManager.Info($"[路径分析UI] 选中第一个组: {SelectedGroup.GroupName}"); + } + + LogManager.Info($"[路径分析UI] 更新完成: EndpointGroups={EndpointGroups.Count}, Suggestions={CategorizedSuggestions.Count}"); + } + + private void UpdateDimensionScores(PathDetailedAnalysis analysis) + { + DimensionScores.Clear(); + DimensionScores.Add(new DimensionScoreViewModel { Name = "安全性", Score = analysis.SafetyScore, MaxScore = 100 }); + DimensionScores.Add(new DimensionScoreViewModel { Name = "效率", Score = analysis.EfficiencyScore, MaxScore = 100 }); + DimensionScores.Add(new DimensionScoreViewModel { Name = "转弯难度", Score = analysis.TurnDifficultyScore, MaxScore = 100 }); + DimensionScores.Add(new DimensionScoreViewModel { Name = "直达性", Score = analysis.TortuosityScore, MaxScore = 100 }); + } + + private void SelectGroup(EndpointGroupViewModel group) + { + if (group != null) + { + SelectedGroup = group; } } - /// - /// 判断是否可以导出报告 - /// + private void UpdateSelectedGroupDetails() + { + if (SelectedGroup?.BestPath != null) + { + SelectedPathAnalysis = SelectedGroup.BestPath; + UpdateDimensionScores(SelectedGroup.BestPath); + } + } + + private void SelectAll() + { + foreach (var path in AvailablePaths) + { + path.IsSelected = true; + } + CommandManager.InvalidateRequerySuggested(); + } + + private void ClearSelection() + { + foreach (var path in AvailablePaths) + { + path.IsSelected = false; + } + CommandManager.InvalidateRequerySuggested(); + } + private bool CanExportReport() { return SelectedPaths != null && SelectedPaths.Count > 0 && RecommendedPath != null; } - /// - /// 更新分析结果到UI - /// - private void UpdateAnalysisResults(PathComparisonResult result) + private void ExportReport() { - // 更新路径的碰撞和评分数据 - foreach (var analysis in result.Analyses) + try { - // 更新AvailablePaths中的数据 - var pathItem = AvailablePaths.FirstOrDefault(p => p.RouteId == analysis.RouteId); - if (pathItem != null) + if (SelectedPaths.Count == 0 || RecommendedPath == null) { - pathItem.CollisionCount = analysis.CollisionCount; - pathItem.Score = (int)analysis.OverallScore; - pathItem.AverageSpeed = pathItem.Length / pathItem.EstimatedTime; - pathItem.AffectedObjects = analysis.CollisionCount; // 简化:使用碰撞数作为受影响对象数 + AnalysisStatus = "请先执行分析"; + return; } - // 同时更新SelectedPaths中的数据 - var selectedItem = SelectedPaths.FirstOrDefault(p => p.RouteId == analysis.RouteId); - if (selectedItem != null) + // 构建报告数据 + var reportData = new NavisworksTransport.Core.PathAnalysisReportData { - selectedItem.CollisionCount = analysis.CollisionCount; - selectedItem.Score = (int)analysis.OverallScore; - selectedItem.AverageSpeed = selectedItem.Length / selectedItem.EstimatedTime; - selectedItem.AffectedObjects = analysis.CollisionCount; + ReportTime = DateTime.Now, + Strategy = AnalysisStrategy, + TotalPathCount = SelectedPaths.Count, + GroupCount = EndpointGroups.Count, + DocumentName = Application.ActiveDocument?.FileName ?? "未命名文档", + Groups = EndpointGroups.Select(g => new EndpointGroup + { + GroupId = g.GroupId, + GroupName = g.GroupName, + EndPointDescription = g.EndPointDescription, + PathAnalyses = g.PathAnalyses.ToList() + }).ToList(), + AllPathAnalyses = SelectedGroup?.PathAnalyses.ToList() ?? new List(), + OverallBestPath = SelectedGroup?.BestPath, + AllSuggestions = CategorizedSuggestions.Select(s => new CategorizedSuggestion + { + Category = (SuggestionCategory)Enum.Parse(typeof(SuggestionCategory), s.Category), + Title = s.Title, + Description = s.Description, + RelatedRouteName = s.RelatedRouteName, + Priority = s.Priority + }).ToList() + }; + + // 弹出保存对话框 + var saveFileDialog = new Microsoft.Win32.SaveFileDialog + { + Title = "导出路径分析报告", + Filter = "HTML报告 (*.html)|*.html|所有文件 (*.*)|*.*", + DefaultExt = "html", + FileName = $"路径分析报告_{DateTime.Now:yyyyMMdd_HHmmss}", + AddExtension = true + }; + + if (saveFileDialog.ShowDialog() == true) + { + // 生成报告 + var generator = new PathAnalysisReportGenerator(); + bool success = generator.SaveReportToFile(reportData, saveFileDialog.FileName); + + if (success) + { + AnalysisStatus = $"报告已保存: {saveFileDialog.FileName}"; + LogManager.Info($"路径分析报告已导出: {saveFileDialog.FileName}"); + + // 打开报告所在目录并选中文件 + System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{saveFileDialog.FileName}\""); + } + else + { + AnalysisStatus = "报告保存失败"; + System.Windows.MessageBox.Show("报告保存失败,请检查文件路径是否有写入权限。", "保存失败", + System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); + } + } + else + { + LogManager.Info("用户取消了路径分析报告导出"); } } - - // 设置推荐路径 - RecommendedPath = SelectedPaths.FirstOrDefault(p => p.RouteId == result.BestRouteId); - - // 更新优化建议 - OptimizationSuggestions.Clear(); - foreach (var suggestion in result.Suggestions) + catch (Exception ex) { - OptimizationSuggestions.Add(new OptimizationSuggestion + LogManager.Error($"导出报告失败: {ex.Message}", ex); + AnalysisStatus = "导出报告失败"; + System.Windows.MessageBox.Show($"导出报告失败: {ex.Message}", "错误", + System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); + } + } + + private bool CanHighlightBestPath() + { + return RecommendedPath != null; + } + + private void HighlightBestPath() + { + try + { + if (RecommendedPath == null) return; + + // 通过PathPlanningManager设置当前路径,触发高亮显示 + var manager = PathPlanningManager.Instance; + var route = manager.GetAllRoutes().FirstOrDefault(r => r.Id == RecommendedPath.RouteId); + + if (route != null) { - Title = suggestion, - Priority = "中", - Status = "待处理" - }); + manager.SetCurrentRoute(route); + LogManager.Info($"已高亮显示最佳路径: {RecommendedPath.Name}"); + } + else + { + LogManager.Warning($"未找到路径: {RecommendedPath.RouteId}"); + } + } + catch (Exception ex) + { + LogManager.Error($"高亮显示路径失败: {ex.Message}"); + } + } + + private void UpdateRecommendedPath() + { + // 策略改变时重新计算 + if (SelectedPaths.Count > 0) + { + ExecuteStartAnalysis(); + } + } + + private string GetCategoryDisplayName(SuggestionCategory category) + { + switch (category) + { + case SuggestionCategory.Safety: return "安全建议"; + case SuggestionCategory.Efficiency: return "效率建议"; + case SuggestionCategory.TurnComplexity: return "转弯建议"; + case SuggestionCategory.Redundancy: return "冗余建议"; + case SuggestionCategory.GroupComparison: return "组内对比"; + default: return "一般建议"; } } #endregion - #region INotifyPropertyChanged实现 + #region INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; @@ -550,11 +586,40 @@ namespace NavisworksTransport.UI.WPF.ViewModels #endregion } - #region 数据模型 + #region 视图模型辅助类 + + public class EndpointGroupViewModel + { + public string GroupId { get; set; } + public string GroupName { get; set; } + public string EndPointDescription { get; set; } + public int RouteCount { get; set; } + public ObservableCollection PathAnalyses { get; set; } + public PathDetailedAnalysis BestPath { get; set; } + } + + public class CategorizedSuggestionViewModel + { + public string Category { get; set; } + public string CategoryName { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public string RelatedRouteName { get; set; } + public int Priority { get; set; } + } + + public class DimensionScoreViewModel + { + public string Name { get; set; } + public double Score { get; set; } + public double MaxScore { get; set; } + public double Percentage => MaxScore > 0 ? Score / MaxScore * 100 : 0; + } + + #endregion + + #region 原有数据模型(保留兼容) - /// - /// 路径分析项目 - /// public class PathAnalysisItem : INotifyPropertyChanged { private bool _isSelected; @@ -563,74 +628,40 @@ namespace NavisworksTransport.UI.WPF.ViewModels private double _averageSpeed; private int _score; - /// 路径ID public string RouteId { get; set; } - - /// 路径名称 public string Name { get; set; } - - /// 路径类型 public string Type { get; set; } - - /// 路径长度(米) public double Length { get; set; } - - /// 估算时间(秒) public int EstimatedTime { get; set; } - /// 碰撞次数 public int CollisionCount { get => _collisionCount; - set - { - _collisionCount = value; - OnPropertyChanged(); - } + set { _collisionCount = value; OnPropertyChanged(); } } - /// 受影响对象数 public int AffectedObjects { get => _affectedObjects; - set - { - _affectedObjects = value; - OnPropertyChanged(); - } + set { _affectedObjects = value; OnPropertyChanged(); } } - /// 平均速度(m/s) public double AverageSpeed { get => _averageSpeed; - set - { - _averageSpeed = value; - OnPropertyChanged(); - } + set { _averageSpeed = value; OnPropertyChanged(); } } - /// 综合评分 public int Score { get => _score; - set - { - _score = value; - OnPropertyChanged(); - } + set { _score = value; OnPropertyChanged(); } } - /// 是否被选中用于分析 public bool IsSelected { get => _isSelected; - set - { - _isSelected = value; - OnPropertyChanged(); - } + set { _isSelected = value; OnPropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; @@ -641,41 +672,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } - /// - /// 优化建议 - /// public class OptimizationSuggestion { - /// 建议标题 public string Title { get; set; } - - /// 建议描述 public string Description { get; set; } - - /// 优先级 public string Priority { get; set; } - - /// 状态 public string Status { get; set; } } - /// - /// 瓶颈位置 - /// public class BottleneckLocation { - /// 位置名称 public string Name { get; set; } - - /// 影响路径数 public int AffectedPaths { get; set; } - - /// 严重程度 public string Severity { get; set; } - - /// 改进建议 public string Suggestion { get; set; } } #endregion -} \ No newline at end of file +} diff --git a/src/UI/WPF/Views/PathAnalysisDialog.xaml b/src/UI/WPF/Views/PathAnalysisDialog.xaml index 796e00a..739a7b8 100644 --- a/src/UI/WPF/Views/PathAnalysisDialog.xaml +++ b/src/UI/WPF/Views/PathAnalysisDialog.xaml @@ -1,287 +1,338 @@ - + MinHeight="700" + MinWidth="1100"> + - - - + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + - + - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +