实现路径分析功能

This commit is contained in:
tian 2026-02-14 17:39:40 +08:00
parent b36d20933e
commit 77b9da40fc
28 changed files with 4343 additions and 3300 deletions

View File

@ -309,20 +309,89 @@ Visibility="{Binding HasItems, Converter={StaticResource BoolToVisibilityConvert
```
**项目中已定义的资源AnimationControlView.xaml**
| 资源名 | 类型 | 说明 |
|--------|------|------|
| `BoolToVisibilityConverter` | BoolToVisibilityConverter | 布尔转可见性支持Inverse参数 |
**检查步骤**
1. 添加XAML代码时检查所有`{StaticResource xxx}`引用
2. 确认资源在文件顶部`<Window.Resources>`或父级资源字典中已定义
3. 不确定时,在项目中搜索该资源名确认存在
4. 避免复制其他项目/文件的代码直接使用(资源可能不同)
**XAML开发工作流程必须遵守**
**常见错误模式**
- `InverseBoolToVisibilityConverter` → 改用 `BoolToVisibilityConverter` + `ConverterParameter=Inverse`
- `VisibilityConverter` → 改用 `BoolToVisibilityConverter`
- 自定义Style名称拼写错误 → 检查Resources中的定义
1. **每添加一个资源引用,立即确认其存在**
```
❌ 错误做法:
- 复制其他文件的XAML代码
- 一次性写大量XAML再测试
✅ 正确做法:
- 每写一行 {StaticResource xxx},立即检查 xxx 是否已定义
- 使用 Ctrl+F 搜索 xxx 确认在当前文件或合并字典中存在
```
2. **资源定义位置优先级**
- 本文件 `<Window.Resources>``<UserControl.Resources>` 内定义
- 引用的外部资源字典(如 `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

View File

@ -1,102 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{2B5F1A8D-3CEB-4154-8761-F568AD9393FF}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>NavisworksTransport.UnitTests</RootNamespace>
<AssemblyName>NavisworksTransport.UnitTests</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<!-- Navisworks 2026 API References -->
<Reference Include="Autodesk.Navisworks.Api">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2026\Autodesk.Navisworks.Api.dll</HintPath>
<Private>False</Private>
</Reference>
<!-- System References -->
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="System.Runtime" />
<!-- WPF References -->
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="WindowsBase" />
<Reference Include="System.Xaml" />
<Reference Include="WindowsFormsIntegration" />
<!-- MSTest Framework for Unit Testing -->
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework">
<HintPath>packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions">
<HintPath>packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<!-- 引用主项目 -->
<ItemGroup>
<Reference Include="NavisworksTransportPlugin">
<HintPath>bin\x64\Debug\NavisworksTransportPlugin.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<!-- 简单测试 -->
<Compile Include="UnitTests\SimpleTest.cs" />
<!-- 路径曲线化引擎测试 -->
<Compile Include="UnitTests\Core\PathCurveEngineTests.cs" />
<Compile Include="UnitTests\Core\PathCurveEngineStandaloneTests.cs" />
<Compile Include="UnitTests\Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- Import MSTest TestAdapter props -->
<Import Project="packages\MSTest.TestAdapter.3.0.4\build\net462\MSTest.TestAdapter.props" Condition="Exists('packages\MSTest.TestAdapter.3.0.4\build\net462\MSTest.TestAdapter.props')" />
<!-- Import MSTest TestAdapter targets -->
<Import Project="packages\MSTest.TestAdapter.3.0.4\build\net462\MSTest.TestAdapter.targets" Condition="Exists('packages\MSTest.TestAdapter.3.0.4\build\net462\MSTest.TestAdapter.targets')" />
</Project>

View File

@ -114,7 +114,10 @@
<Compile Include="src\Core\PathPlanningManager.cs" />
<Compile Include="src\Core\PathDatabase.cs" />
<Compile Include="src\Core\PathAnalysisService.cs" />
<Compile Include="src\Core\PathAnalysisEngine.cs" />
<Compile Include="src\Core\PathAnalysisReportGenerator.cs" />
<Compile Include="src\Core\PathPlanningModels.cs" />
<Compile Include="src\Core\Models\PathAnalysisModels.cs" />
<Compile Include="src\Core\Models\BatchQueueItem.cs" />
<Compile Include="src\Core\Models\CollisionDetectionConfig.cs" />
<Compile Include="src\Core\PathCurveEngine.cs" />

View File

@ -1,284 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NavisworksTransport.UnitTests
{
/// <summary>
/// ThreadSafeObservableCollection基础功能测试
/// 只包含可以在控制台环境下稳定运行的纯数据操作测试,不涉及事件
/// </summary>
[TestClass]
public class ThreadSafeObservableCollectionBasicTests
{
private ThreadSafeObservableCollection<string> _collection;
[TestInitialize]
public void Setup()
{
_collection = new ThreadSafeObservableCollection<string>();
}
[TestCleanup]
public void Cleanup()
{
_collection = null;
}
#region
[TestMethod]
public void Constructor_ShouldCreateEmptyCollection()
{
// Arrange & Act
var collection = new ThreadSafeObservableCollection<int>();
// Assert
Assert.AreEqual(0, collection.Count, "新创建的集合应该为空");
}
[TestMethod]
public void Constructor_WithInitialItems_ShouldContainAllItems()
{
// Arrange
var initialItems = new[] { "A", "B", "C" };
// Act
var collection = new ThreadSafeObservableCollection<string>(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<ArgumentNullException>(() =>
{
_collection.Add(null);
}, "添加null元素应该抛出ArgumentNullException");
}
[TestMethod]
public void AddRange_WithNull_ShouldThrowArgumentNullException()
{
// Act & Assert
Assert.ThrowsException<ArgumentNullException>(() =>
{
_collection.AddRange(null);
}, "AddRange传入null应该抛出ArgumentNullException");
}
[TestMethod]
public void IndexAccess_WithInvalidIndex_ShouldThrowArgumentOutOfRangeException()
{
// Arrange
_collection.Add("OnlyItem");
// Act & Assert
Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
{
var item = _collection[5]; // 无效索引
}, "访问无效索引应该抛出ArgumentOutOfRangeException");
}
#endregion
}
}

View File

@ -1,428 +0,0 @@
namespace NavisworksTransport.UnitTests.Commands
{
/// <summary>
/// CommandBase抽象类的纯逻辑测试
/// 通过创建测试实现类来测试基础功能不依赖Navisworks环境
/// </summary>
[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<Exception>();
var tasks = new List<Task>();
// 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<Exception>();
var tasks = new List<Task>();
// 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实现
/// <summary>
/// 用于测试的Command实现类
/// </summary>
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<PathPlanningResult> ValidateAsync(CancellationToken cancellationToken)
{
if (_validationShouldFail)
{
return PathPlanningResult.ValidationFailure("测试验证失败");
}
return PathPlanningResult.Success("验证成功");
}
protected override async Task<PathPlanningResult> 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
}

View File

@ -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
/// <summary>
/// 简单的 3D 点结构体(用于测试)
/// </summary>
public struct TestPoint3D : IEquatable<TestPoint3D>
{
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);
}
}
/// <summary>
/// 简单的 3D 向量结构体(用于测试)
/// </summary>
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
);
}
}
/// <summary>
/// 测试用圆弧轨迹数据
/// </summary>
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
/// <summary>
/// PathCurveEngine 独立测试版本(不依赖 Navisworks API
/// 用于测试核心算法逻辑的正确性
/// </summary>
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;
}
/// <summary>
/// 计算圆弧切点和轨迹参数
/// </summary>
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
};
}
/// <summary>
/// 采样圆弧为离散点序列
/// </summary>
public static List<TestPoint3D> SampleArc(TestArcTrajectory trajectory, double samplingStep)
{
var points = new List<TestPoint3D>();
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;
}
/// <summary>
/// 绕轴旋转点(罗德里格斯旋转公式)
/// </summary>
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;
}
}
/// <summary>
/// PathCurveEngine 核心算法独立测试
/// 测试路径曲线化算法的正确性(不依赖 Navisworks API
/// </summary>
[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
}
}

View File

@ -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
{
/// <summary>
/// PathCurveEngine 核心算法测试
/// 测试路径曲线化算法的正确性
/// </summary>
[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<PathPoint>
{
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<PathPoint>
{
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<PathPoint>
{
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<PathPoint>
{
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<PathEdge>
{
new PathEdge
{
Id = Guid.NewGuid().ToString(),
SegmentType = PathSegmentType.Straight,
PhysicalLength = 10.0,
SampledPoints = new List<Point3D>()
},
new PathEdge
{
Id = Guid.NewGuid().ToString(),
SegmentType = PathSegmentType.Straight,
PhysicalLength = 5.0,
SampledPoints = new List<Point3D>()
}
};
// 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<PathEdge>
{
new PathEdge
{
Id = Guid.NewGuid().ToString(),
SegmentType = PathSegmentType.Straight,
PhysicalLength = 10.0,
SampledPoints = new List<Point3D>()
},
new PathEdge
{
Id = Guid.NewGuid().ToString(),
SegmentType = PathSegmentType.Arc,
PhysicalLength = Math.PI, // 半圆
SampledPoints = new List<Point3D>()
},
new PathEdge
{
Id = Guid.NewGuid().ToString(),
SegmentType = PathSegmentType.Straight,
PhysicalLength = 5.0,
SampledPoints = new List<Point3D>()
}
};
// 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<PathEdge>()
};
// 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<PathPoint>
{
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
}
}

View File

@ -1,117 +0,0 @@
namespace NavisworksTransport.UnitTests
{
/// <summary>
/// UIStateManager基础功能测试
/// 只包含可以在控制台环境下稳定运行的纯逻辑测试
/// </summary>
[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
}
}

View File

@ -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")]

View File

@ -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");
}
}
}

View File

@ -1,75 +0,0 @@
using System;
using System.Threading.Tasks;
namespace NavisworksTransport.UnitTests
{
/// <summary>
/// 用于测试的ViewModelBase实现
/// </summary>
public class TestViewModel : ViewModelBase
{
private string _testProperty;
public string TestProperty
{
get => _testProperty;
set => SetProperty(ref _testProperty, value);
}
/// <summary>
/// 公开OnPropertyChanged方法用于测试
/// </summary>
public void TriggerPropertyChanged(string propertyName)
{
OnPropertyChanged(propertyName);
}
/// <summary>
/// 公开OnPropertiesChanged方法用于测试
/// </summary>
public void TriggerPropertiesChanged(params string[] propertyNames)
{
OnPropertiesChanged(propertyNames);
}
/// <summary>
/// 公开SetProperties方法用于测试
/// </summary>
public void TriggerSetProperties(params (string name, object value)[] properties)
{
SetProperties(properties);
}
/// <summary>
/// 公开OnPropertyChangedAsync方法用于测试
/// </summary>
public async Task TriggerPropertyChangedAsync(string propertyName, int timeout = 3000)
{
await OnPropertyChangedAsync(propertyName, timeout);
}
/// <summary>
/// 公开SafeExecute方法用于测试
/// </summary>
public void TestSafeExecute(Action action, string operationName)
{
SafeExecute(action, operationName);
}
/// <summary>
/// 公开SafeExecute<T>方法用于测试
/// </summary>
public T TestSafeExecuteWithReturn<T>(Func<T> func, T defaultValue, string operationName)
{
return SafeExecute(func, defaultValue, operationName);
}
/// <summary>
/// 公开SafeExecuteAsync方法用于测试
/// </summary>
public async Task TestSafeExecuteAsync(Action action, string operationName)
{
await SafeExecuteAsync(action, operationName);
}
}
}

View File

@ -1,368 +0,0 @@
namespace NavisworksTransport.UnitTests.Utils
{
/// <summary>
/// UnitsConverter工具类的纯逻辑测试
/// 测试单位转换计算功能不依赖Navisworks环境
/// </summary>
[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
}
}

View File

@ -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<string, double[]> Weights = new Dictionary<string, double[]>
{
[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<EndpointGroup> GroupPathsByEndpoint(List<PathRoute> routes)
{
const double ENDPOINT_THRESHOLD = 2.0; // 2米
var groups = new List<EndpointGroup>();
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<PathRoute> { 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<CollisionHotspot> DetectHotspots(List<CollisionResult> collisions, double radius = 3.0)
{
var hotspots = new List<CollisionHotspot>();
var processed = new HashSet<int>();
for (int i = 0; i < collisions.Count; i++)
{
if (processed.Contains(i)) continue;
var center = collisions[i].Center;
var nearbyCollisions = new List<CollisionResult> { 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<string> GenerateSuggestions(PathDetailedAnalysis analysis, EndpointGroup group)
{
var suggestions = new List<string>();
// 安全建议
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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>路径分析报告</title>
<!-- 本地Chart.js -->
<script src="./js/chart.min.js"></script>
<style>
/* 简洁专业风格(蓝白配色) */
:root { --primary: #2c5aa0; --success: #28a745; --warning: #ffc107; --danger: #dc3545; }
body { font-family: 'Microsoft YaHei', Arial; margin: 40px; }
.header { text-align: center; border-bottom: 3px solid var(--primary); padding-bottom: 20px; }
.best-path-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 12px; }
.metric-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 15px; }
.comparison-table { width: 100%; border-collapse: collapse; }
.comparison-table th { background: var(--primary); color: white; padding: 12px; }
/* ... */
</style>
</head>
<body>
<!-- 1. 头部 -->
<!-- 2. 执行摘要+最佳路径 -->
<!-- 3. 雷达图 -->
<!-- 4. 同终点组分析 -->
<!-- 5. 详细数据表格 -->
<!-- 6. 优化建议 -->
<!-- 7. 技术参数 -->
</body>
</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`

View File

@ -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<EndpointGroup> GroupPathsByEndpoint(List<PathRoute> routes);
// 3. 热点检测
public List<CollisionHotspot> DetectHotspots(List<CollisionResult> collisions, double radius);
// 4. 各维度评分
private double CalculateSafetyScore(int collisionCount, int hotspotCount);
private double CalculateEfficiencyScore(PathRoute route, double groupMinTime);
private double CalculateTurnDifficultyScore(List<PathEdge> edges);
private double CalculateTortuosityScore(PathRoute route);
private double CalculateRedundancyScore(PathRoute route, List<ChannelInfo> 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<CollisionHotspot> hotspots);
// 获取路径的热点数据
public List<CollisionHotspot> GetCollisionHotspots(string routeId);
```
**验收标准**:
- [ ] 新表结构创建成功
- [ ] 数据能正确保存和读取
- [ ] 旧数据兼容性良好
---
### 任务 1.4: 服务层扩展2小时
**文件**: `src/Core/PathAnalysisService.cs` (修改)
**修改内容**:
1. **扩展 CompareRoutes 方法**:
```csharp
public PathComparisonResult CompareRoutes(List<PathRoute> routes, string strategy)
{
// 1. 路径分组
// 2. 分析每条路径
// 3. 计算组内排名
// 4. 找出最佳路径
// 5. 生成优化建议
}
```
2. **新增方法**:
```csharp
// 分析多条路径并分组
public List<EndpointGroup> AnalyzeAndGroupPaths(List<string> 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<string> AvailableStrategies { get; }
// 分析结果
public ObservableCollection<PathDetailedAnalysis> AnalysisResults { get; }
// 同终点组
public ObservableCollection<EndpointGroup> EndpointGroups { get; }
// 当前选中组
public EndpointGroup SelectedGroup { get; set; }
// 组内最佳路径
public PathDetailedAnalysis GroupBestPath { get; set; }
// 优化建议(分类)
public ObservableCollection<CategorizedSuggestion> 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
- 策略选择RadioButton3种
- [开始分析] 按钮
中间面板(可变):
- 数据对比表格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<PathDetailedAnalysis> 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中引用 `<script src="./js/chart.min.js"></script>`
**注意**: 如果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: 数据模型**

View File

@ -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

View File

@ -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}米");

View File

@ -0,0 +1,650 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.Core.Models
{
/// <summary>
/// 分析策略常量定义
/// </summary>
public static class AnalysisStrategies
{
public const string SafetyFirst = "安全优先";
public const string EfficiencyFirst = "效率优先";
public const string Balanced = "平衡模式";
/// <summary>
/// 策略权重配置(四维度)
/// 顺序: 安全、效率、转弯、直达
/// </summary>
public static readonly Dictionary<string, double[]> Weights = new Dictionary<string, double[]>
{
[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 }
};
/// <summary>
/// 获取所有可用策略列表
/// </summary>
public static List<string> GetAllStrategies()
{
return new List<string> { SafetyFirst, EfficiencyFirst, Balanced };
}
/// <summary>
/// 验证策略名称是否有效
/// </summary>
public static bool IsValidStrategy(string strategy)
{
return Weights.ContainsKey(strategy);
}
/// <summary>
/// 获取策略权重,无效策略返回平衡模式权重
/// </summary>
public static double[] GetWeights(string strategy)
{
if (Weights.TryGetValue(strategy, out var weights))
return weights;
return Weights[Balanced];
}
}
/// <summary>
/// 单条路径的详细分析结果
/// </summary>
public class PathDetailedAnalysis
{
/// <summary>
/// 路径ID
/// </summary>
public string RouteId { get; set; }
/// <summary>
/// 路径名称
/// </summary>
public string RouteName { get; set; }
/// <summary>
/// 路径总长度(米)
/// </summary>
public double TotalLength { get; set; }
/// <summary>
/// 预估通行时间(秒)
/// </summary>
public double EstimatedTime { get; set; }
/// <summary>
/// 碰撞数量
/// </summary>
public int CollisionCount { get; set; }
/// <summary>
/// 碰撞热点数量
/// </summary>
public int HotspotCount { get; set; }
/// <summary>
/// 碰撞热点列表
/// </summary>
public List<CollisionHotspot> Hotspots { get; set; } = new List<CollisionHotspot>();
// ========== 五维度评分0-100==========
/// <summary>
/// 安全性分数(碰撞风险指数)
/// </summary>
public double SafetyScore { get; set; }
/// <summary>
/// 效率分数(通行效率指数)
/// </summary>
public double EfficiencyScore { get; set; }
/// <summary>
/// 转弯难度分数
/// </summary>
public double TurnDifficultyScore { get; set; }
/// <summary>
/// 路径曲折度分数
/// </summary>
public double TortuosityScore { get; set; }
/// <summary>
/// 通道冗余度分数
/// </summary>
public double RedundancyScore { get; set; }
/// <summary>
/// 综合加权评分
/// </summary>
public double WeightedScore { get; set; }
/// <summary>
/// 分析使用的策略
/// </summary>
public string Strategy { get; set; }
/// <summary>
/// 分析时间
/// </summary>
public DateTime AnalysisTime { get; set; }
// ========== 组内排名信息 ==========
/// <summary>
/// 所属终点组ID
/// </summary>
public string GroupId { get; set; }
/// <summary>
/// 在组内排名1开始
/// </summary>
public int GroupRanking { get; set; }
/// <summary>
/// 是否为组内最佳路径
/// </summary>
public bool IsBestInGroup => GroupRanking == 1;
/// <summary>
/// 获取四维度分数数组(用于雷达图)
/// 顺序: 安全、效率、转弯、直达
/// </summary>
public double[] GetDimensionScores()
{
return new[] { SafetyScore, EfficiencyScore, TurnDifficultyScore, TortuosityScore };
}
/// <summary>
/// 获取维度分数字典
/// </summary>
public Dictionary<string, double> GetDimensionScoreDict()
{
return new Dictionary<string, double>
{
["安全性"] = SafetyScore,
["效率"] = EfficiencyScore,
["转弯难度"] = TurnDifficultyScore,
["直达性"] = TortuosityScore
};
}
}
/// <summary>
/// 碰撞热点
/// </summary>
public class CollisionHotspot
{
/// <summary>
/// 热点ID
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString("N").Substring(0, 8);
/// <summary>
/// 热点中心位置
/// </summary>
public Point3D Center { get; set; }
/// <summary>
/// 热点半径(米)
/// </summary>
public double Radius { get; set; } = 3.0;
/// <summary>
/// 范围内碰撞数量
/// </summary>
public int CollisionCount { get; set; }
/// <summary>
/// 碰撞对象名称列表
/// </summary>
public List<string> CollidedObjectNames { get; set; } = new List<string>();
/// <summary>
/// 热点严重程度
/// </summary>
public HotspotSeverity Severity
{
get
{
if (CollisionCount >= 5) return HotspotSeverity.Critical;
if (CollisionCount >= 3) return HotspotSeverity.High;
return HotspotSeverity.Medium;
}
}
/// <summary>
/// 获取热点描述
/// </summary>
public string GetDescription()
{
return $"位置({Center.X:F2}, {Center.Y:F2}, {Center.Z:F2})范围内有{CollisionCount}次碰撞";
}
}
/// <summary>
/// 热点严重程度
/// </summary>
public enum HotspotSeverity
{
/// <summary>
/// 中等2次碰撞
/// </summary>
Medium,
/// <summary>
/// 严重3-4次碰撞
/// </summary>
High,
/// <summary>
/// 危急5次及以上
/// </summary>
Critical
}
/// <summary>
/// 同终点路径组
/// </summary>
public class EndpointGroup
{
/// <summary>
/// 组ID
/// </summary>
public string GroupId { get; set; }
/// <summary>
/// 组名称(基于终点描述)
/// </summary>
public string GroupName { get; set; }
/// <summary>
/// 终点位置(组中心点)
/// </summary>
public Point3D EndPoint { get; set; }
/// <summary>
/// 终点描述
/// </summary>
public string EndPointDescription { get; set; }
/// <summary>
/// 组内所有路径的分析结果
/// </summary>
public List<PathDetailedAnalysis> PathAnalyses { get; set; } = new List<PathDetailedAnalysis>();
/// <summary>
/// 路径数量
/// </summary>
public int RouteCount => PathAnalyses?.Count ?? 0;
/// <summary>
/// 组内最短路径长度
/// </summary>
public double MinLength => PathAnalyses.Min(p => p.TotalLength);
/// <summary>
/// 组内最短通行时间
/// </summary>
public double MinTime => PathAnalyses.Min(p => p.EstimatedTime);
/// <summary>
/// 组内最少碰撞数
/// </summary>
public int MinCollisionCount => PathAnalyses.Min(p => p.CollisionCount);
/// <summary>
/// 组内最佳路径(按当前策略)
/// </summary>
public PathDetailedAnalysis BestPath => PathAnalyses.OrderByDescending(p => p.WeightedScore).FirstOrDefault();
/// <summary>
/// 获取组内某路径的排名
/// </summary>
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;
}
/// <summary>
/// 获取组内某路径与最佳路径的长度差异率
/// </summary>
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;
}
/// <summary>
/// 获取组内某路径与最快路径的时间差异率
/// </summary>
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;
}
}
/// <summary>
/// 路径对比结果
/// </summary>
public class PathComparison
{
/// <summary>
/// 路径A ID
/// </summary>
public string PathAId { get; set; }
/// <summary>
/// 路径A名称
/// </summary>
public string PathAName { get; set; }
/// <summary>
/// 路径B ID
/// </summary>
public string PathBId { get; set; }
/// <summary>
/// 路径B名称
/// </summary>
public string PathBName { get; set; }
/// <summary>
/// 长度差异(米)
/// </summary>
public double LengthDiff { get; set; }
/// <summary>
/// 长度差异率(%
/// </summary>
public double LengthDiffPercent { get; set; }
/// <summary>
/// 时间差异(秒)
/// </summary>
public double TimeDiff { get; set; }
/// <summary>
/// 时间差异率(%
/// </summary>
public double TimeDiffPercent { get; set; }
/// <summary>
/// 碰撞数差异
/// </summary>
public int CollisionDiff { get; set; }
/// <summary>
/// 综合评分差异
/// </summary>
public double ScoreDiff { get; set; }
/// <summary>
/// 优势方A更优、B更优、相当
/// </summary>
public string Advantage { get; set; }
}
/// <summary>
/// 分析上下文
/// </summary>
public class AnalysisContext
{
/// <summary>
/// 分析策略
/// </summary>
public string Strategy { get; set; } = AnalysisStrategies.Balanced;
/// <summary>
/// 组内最短通行时间(用于效率计算)
/// </summary>
public double GroupMinTime { get; set; }
/// <summary>
/// 组内最短路径长度
/// </summary>
public double GroupMinLength { get; set; }
/// <summary>
/// 路径经过的通道信息
/// </summary>
public List<ChannelInfo> Channels { get; set; } = new List<ChannelInfo>();
/// <summary>
/// 组内最高成本(用于归一化,如需要)
/// </summary>
public double GroupMaxCost { get; set; }
}
/// <summary>
/// 通道信息
/// </summary>
public class ChannelInfo
{
/// <summary>
/// 通道模型ID
/// </summary>
public string ModelItemId { get; set; }
/// <summary>
/// 通道显示名称
/// </summary>
public string DisplayName { get; set; }
/// <summary>
/// 通道类型
/// </summary>
public CategoryAttributeManager.LogisticsElementType ChannelType { get; set; }
/// <summary>
/// 宽度限制(米)
/// </summary>
public double WidthLimit { get; set; }
/// <summary>
/// 高度限制(米)
/// </summary>
public double HeightLimit { get; set; }
/// <summary>
/// 速度限制m/s
/// </summary>
public double SpeedLimit { get; set; }
}
/// <summary>
/// 分类优化建议
/// </summary>
public class CategorizedSuggestion
{
/// <summary>
/// 建议类别
/// </summary>
public SuggestionCategory Category { get; set; }
/// <summary>
/// 类别显示名称
/// </summary>
public string CategoryName => GetCategoryDisplayName(Category);
/// <summary>
/// 建议标题
/// </summary>
public string Title { get; set; }
/// <summary>
/// 建议详细描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// 关联的路径ID
/// </summary>
public string RelatedRouteId { get; set; }
/// <summary>
/// 关联的路径名称
/// </summary>
public string RelatedRouteName { get; set; }
/// <summary>
/// 优先级1-55最高
/// </summary>
public int Priority { get; set; }
/// <summary>
/// 获取类别显示名称
/// </summary>
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 "其他";
}
}
}
/// <summary>
/// 建议类别
/// </summary>
public enum SuggestionCategory
{
/// <summary>
/// 安全建议
/// </summary>
Safety,
/// <summary>
/// 效率建议
/// </summary>
Efficiency,
/// <summary>
/// 转弯复杂度建议
/// </summary>
TurnComplexity,
/// <summary>
/// 冗余度建议
/// </summary>
Redundancy,
/// <summary>
/// 组内对比建议
/// </summary>
GroupComparison,
/// <summary>
/// 一般建议
/// </summary>
General
}
/// <summary>
/// 完整分析报告数据
/// </summary>
public class PathAnalysisReportData
{
/// <summary>
/// 报告生成时间
/// </summary>
public DateTime ReportTime { get; set; }
/// <summary>
/// 分析策略
/// </summary>
public string Strategy { get; set; }
/// <summary>
/// 分析的路径总数
/// </summary>
public int TotalPathCount { get; set; }
/// <summary>
/// 同终点组数量
/// </summary>
public int GroupCount { get; set; }
/// <summary>
/// 所有同终点组
/// </summary>
public List<EndpointGroup> Groups { get; set; } = new List<EndpointGroup>();
/// <summary>
/// 所有路径分析结果
/// </summary>
public List<PathDetailedAnalysis> AllPathAnalyses { get; set; } = new List<PathDetailedAnalysis>();
/// <summary>
/// 全局最佳路径
/// </summary>
public PathDetailedAnalysis OverallBestPath { get; set; }
/// <summary>
/// 所有优化建议
/// </summary>
public List<CategorizedSuggestion> AllSuggestions { get; set; } = new List<CategorizedSuggestion>();
/// <summary>
/// 文档名称
/// </summary>
public string DocumentName { get; set; }
/// <summary>
/// 车辆参数信息
/// </summary>
public VehicleParameters VehicleParams { get; set; }
}
/// <summary>
/// 车辆参数
/// </summary>
public class VehicleParameters
{
/// <summary>
/// 车辆长度(米)
/// </summary>
public double Length { get; set; }
/// <summary>
/// 车辆宽度(米)
/// </summary>
public double Width { get; set; }
/// <summary>
/// 车辆高度(米)
/// </summary>
public double Height { get; set; }
/// <summary>
/// 安全间隙(米)
/// </summary>
public double SafetyMargin { get; set; }
/// <summary>
/// 有效宽度(含间隙)
/// </summary>
public double EffectiveWidth => Width + SafetyMargin * 2;
/// <summary>
/// 有效高度(含间隙)
/// </summary>
public double EffectiveHeight => Height + SafetyMargin;
}
}

View File

@ -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
{
/// <summary>
/// 路径分析引擎
/// 负责计算路径的各项指标和评分
/// </summary>
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
/// <summary>
/// 获取终点分组阈值(转换为模型单位用于距离比较)
/// </summary>
private double EndpointThreshold => NavisworksTransport.Utils.UnitsConverter.ConvertFromMeters(ENDPOINT_THRESHOLD_METERS);
/// <summary>
/// 构造函数
/// </summary>
public PathAnalysisEngine(PathDatabase database)
{
_database = database ?? throw new ArgumentNullException(nameof(database));
}
#region
/// <summary>
/// 分析单条路径的所有指标
/// </summary>
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;
}
/// <summary>
/// 对多条路径进行分组(按终点)
/// </summary>
public List<EndpointGroup> GroupPathsByEndpoint(List<PathRoute> routes)
{
if (routes == null || routes.Count == 0)
return new List<EndpointGroup>();
var groups = new List<EndpointGroup>();
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;
}
/// <summary>
/// 分析多条路径并分组,包含完整的上下文计算
/// </summary>
public List<EndpointGroup> AnalyzeAndGroupPaths(List<PathRoute> routes, string strategy)
{
if (routes == null || routes.Count == 0)
return new List<EndpointGroup>();
// 第一步:初步分析所有路径(获取基础数据)
var analyses = new List<PathDetailedAnalysis>();
foreach (var route in routes)
{
var analysis = AnalyzePath(route, new AnalysisContext { Strategy = strategy });
analyses.Add(analysis);
}
// 第二步:按终点分组
var groups = new List<EndpointGroup>();
var processed = new HashSet<string>();
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<PathDetailedAnalysis>();
// 首先添加当前路径
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;
}
/// <summary>
/// 检测碰撞热点
/// </summary>
public List<CollisionHotspot> DetectHotspots(List<CollisionResult> collisions, double radius = HOTSPOT_RADIUS)
{
var hotspots = new List<CollisionHotspot>();
if (collisions == null || collisions.Count < 2)
return hotspots;
var processed = new HashSet<int>();
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<CollisionResult> { 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();
}
/// <summary>
/// 计算加权综合评分
/// </summary>
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);
}
/// <summary>
/// 查找组内最佳路径
/// </summary>
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];
}
/// <summary>
/// 生成优化建议
/// </summary>
public List<CategorizedSuggestion> GenerateSuggestions(PathDetailedAnalysis analysis, EndpointGroup group)
{
var suggestions = new List<CategorizedSuggestion>();
// 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
/// <summary>
/// 计算安全性分数
/// </summary>
private double CalculateSafetyScore(int collisionCount, int hotspotCount)
{
// 简单扣分制每个碰撞扣1分每个热点扣1分
double penalty = collisionCount * 1.0 + hotspotCount * 1.0;
return Math.Max(0, 100 - penalty);
}
/// <summary>
/// 计算效率分数 - 基于路径长度(越短越高效)
/// </summary>
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);
}
/// <summary>
/// 计算转弯难度分数
/// </summary>
private double CalculateTurnDifficultyScore(List<PathEdge> 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);
}
/// <summary>
/// 计算路径曲折度分数
/// </summary>
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;
}
/// <summary>
/// 计算冗余度分数
/// </summary>
private double CalculateRedundancyScore(PathRoute route, List<ChannelInfo> 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
/// <summary>
/// 获取路径的碰撞数据
/// </summary>
private List<CollisionResult> GetCollisionsForRoute(string routeId)
{
try
{
// 获取最新的ClashDetective结果
var clashResult = _database.GetLatestClashDetectiveResultByRouteId(routeId);
if (clashResult == null)
return new List<CollisionResult>();
// 获取碰撞对象
var collisionObjects = _database.GetClashDetectiveCollisionObjects(clashResult.Id);
// 转换为CollisionResult列表
var collisions = new List<CollisionResult>();
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<CollisionResult>();
}
}
/// <summary>
/// 计算两点距离
/// </summary>
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);
}
/// <summary>
/// 计算碰撞中心点
/// </summary>
private Point3D CalculateCenter(List<CollisionResult> 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);
}
/// <summary>
/// 计算组中心点(取所有路径终点的平均值)
/// </summary>
private Point3D CalculateGroupCenter(List<PathDetailedAnalysis> analyses, List<PathRoute> 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);
}
/// <summary>
/// 更新组内统计信息
/// </summary>
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}");
}
/// <summary>
/// 设置默认分数(错误恢复)
/// </summary>
private void SetDefaultScores(PathDetailedAnalysis analysis)
{
analysis.SafetyScore = 50;
analysis.EfficiencyScore = 50;
analysis.TurnDifficultyScore = 50;
analysis.TortuosityScore = 50;
analysis.RedundancyScore = 50;
analysis.WeightedScore = 50;
}
/// <summary>
/// 获取安全的显示名称
/// </summary>
private string GetSafeDisplayName(ModelItem item)
{
if (item == null)
return "未知对象";
try
{
return item.DisplayName ?? "未命名对象";
}
catch
{
return "未知对象";
}
}
#endregion
}
/// <summary>
/// ModelItem代理类用于当没有实际ModelItem时
/// </summary>
internal static class ModelItemProxyHelper
{
public static ModelItem CreateProxy(string displayName)
{
// 这是一个简化实现,实际可能需要更复杂的处理
// 这里仅用于存储显示名称
return null;
}
}
}

View File

@ -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
{
/// <summary>
/// 路径分析报告HTML生成器
/// 生成包含图表的专业HTML报告
/// </summary>
public class PathAnalysisReportGenerator
{
private readonly string _chartJsPath;
public PathAnalysisReportGenerator()
{
// Chart.js本地路径
_chartJsPath = Path.Combine(
PathHelper.GetPluginDirectory(),
"resources", "js", "chart.min.js"
);
}
/// <summary>
/// 生成完整HTML报告
/// </summary>
public string GenerateHtmlReport(PathAnalysisReportData data)
{
var html = new StringBuilder();
// HTML头部
html.AppendLine(GenerateHtmlHead(data));
// 报告主体
html.AppendLine("<body>");
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("</body>");
html.AppendLine("</html>");
return html.ToString();
}
/// <summary>
/// 保存报告到指定文件路径
/// </summary>
/// <param name="data">报告数据</param>
/// <param name="filePath">完整的文件路径</param>
/// <returns>是否保存成功</returns>
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("<!DOCTYPE html>");
sb.AppendLine("<html lang=\"zh-CN\">");
sb.AppendLine("<head>");
sb.AppendLine(" <meta charset=\"UTF-8\">");
sb.AppendLine(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
sb.AppendLine($" <title>路径分析报告 - {data.DocumentName}</title>");
sb.AppendLine(GenerateStyles());
sb.AppendLine(GenerateChartJsInclude());
sb.AppendLine("</head>");
return sb.ToString();
}
private string GenerateStyles()
{
return @"
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Microsoft YaHei', 'Segoe UI', Arial, sans-serif;
background: #f5f5f5;
line-height: 1.6;
color: #333;
}
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
/* 头部样式 */
.header {
background: linear-gradient(135deg, #2C5AA0 0%, #1e3d6f 100%);
color: white;
padding: 40px;
border-radius: 8px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.header h1 { font-size: 32px; margin-bottom: 10px; }
.header .meta { opacity: 0.9; font-size: 14px; }
/* 卡片样式 */
.card {
background: white;
border-radius: 8px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card-title {
font-size: 20px;
font-weight: bold;
color: #2C5AA0;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #e9ecef;
}
/* 最佳路径卡片 */
.best-path-card {
background: linear-gradient(135deg, #28A745 0%, #1e7e34 100%);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 30px;
}
.best-path-card h2 { font-size: 24px; margin-bottom: 15px; }
.best-path-card .path-name { font-size: 28px; font-weight: bold; margin-bottom: 10px; }
.best-path-card .path-stats { font-size: 16px; opacity: 0.95; }
/* 表格样式 */
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.data-table th {
background: #2C5AA0;
color: white;
padding: 12px;
text-align: left;
font-weight: 600;
}
.data-table td {
padding: 12px;
border-bottom: 1px solid #e9ecef;
}
.data-table tr:hover { background: #f8f9fa; }
.data-table .best-row { background: #d4edda !important; font-weight: bold; }
/* 分数条 */
.score-bar-container {
background: #e9ecef;
height: 24px;
border-radius: 12px;
overflow: hidden;
position: relative;
}
.score-bar {
height: 100%;
border-radius: 12px;
transition: width 0.3s ease;
}
.score-bar.high { background: #28A745; }
.score-bar.medium { background: #FFC107; }
.score-bar.low { background: #DC3545; }
.score-text {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-weight: bold;
font-size: 12px;
}
/* 建议样式 */
.suggestion-item {
border-left: 4px solid #2C5AA0;
padding: 15px 20px;
margin-bottom: 15px;
background: #f8f9fa;
border-radius: 0 4px 4px 0;
}
.suggestion-item.high-priority { border-left-color: #DC3545; }
.suggestion-item.medium-priority { border-left-color: #FFC107; }
.suggestion-item.low-priority { border-left-color: #28A745; }
.suggestion-category {
display: inline-block;
padding: 3px 10px;
border-radius: 3px;
font-size: 12px;
color: white;
margin-bottom: 8px;
}
/* 图表容器 */
.chart-container {
position: relative;
height: 350px;
margin: 20px 0;
}
/* 组分析样式 */
.group-section {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
}
.group-header {
font-size: 18px;
font-weight: bold;
color: #2C5AA0;
margin-bottom: 15px;
}
/* 页脚 */
.footer {
text-align: center;
padding: 30px;
color: #6c757d;
font-size: 12px;
border-top: 1px solid #e9ecef;
margin-top: 40px;
}
/* 网格布局 */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; }
/* 指标卡片 */
.metric-card {
background: white;
padding: 20px;
border-radius: 8px;
text-align: center;
border: 1px solid #e9ecef;
}
.metric-value {
font-size: 32px;
font-weight: bold;
color: #2C5AA0;
margin: 10px 0;
}
.metric-label { color: #6c757d; font-size: 14px; }
</style>";
}
private string GenerateChartJsInclude()
{
// 使用内联Chart.js避免外部依赖
// 如果本地存在则使用本地否则使用CDN
if (File.Exists(_chartJsPath))
{
string chartJs = File.ReadAllText(_chartJsPath);
return $"<script>{chartJs}</script>";
}
// 使用Chart.js CDN作为备选
return "<script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>";
}
private string GenerateHeader(PathAnalysisReportData data)
{
var sb = new StringBuilder();
sb.AppendLine("<div class=\"container\">");
sb.AppendLine(" <div class=\"header\">");
sb.AppendLine(" <h1>📊 物流路径规划分析报告</h1>");
sb.AppendLine(" <div class=\"meta\">");
sb.AppendLine($" <p>文档: {EscapeHtml(data.DocumentName)}</p>");
sb.AppendLine($" <p>生成时间: {data.ReportTime:yyyy年MM月dd日 HH:mm:ss}</p>");
sb.AppendLine($" <p>分析策略: {data.Strategy} | 路径数量: {data.TotalPathCount} | 组数量: {data.GroupCount}</p>");
sb.AppendLine(" </div>");
sb.AppendLine(" </div>");
return sb.ToString();
}
private string GenerateExecutiveSummary(PathAnalysisReportData data)
{
var sb = new StringBuilder();
sb.AppendLine(" <div class=\"best-path-card\">");
sb.AppendLine(" <h2>🏆 推荐最佳路径</h2>");
if (data.OverallBestPath != null)
{
var best = data.OverallBestPath;
sb.AppendLine($" <div class=\"path-name\">{EscapeHtml(best.RouteName)}</div>");
sb.AppendLine(" <div class=\"path-stats\">");
sb.AppendLine($" 长度: {best.TotalLength:F1}米 | ");
sb.AppendLine($" 碰撞: {best.CollisionCount}次 | ");
sb.AppendLine($" 综合评分: {best.WeightedScore:F1}分");
sb.AppendLine(" </div>");
}
else
{
sb.AppendLine(" <div class=\"path-name\">未找到推荐路径</div>");
}
sb.AppendLine(" </div>");
return sb.ToString();
}
private string GenerateGroupAnalysis(EndpointGroup group)
{
var sb = new StringBuilder();
sb.AppendLine(" <div class=\"group-section\">");
sb.AppendLine($" <div class=\"group-header\">🎯 {EscapeHtml(group.GroupName)} - {group.EndPointDescription}</div>");
sb.AppendLine($" <p>共 {group.RouteCount} 条路径到达此终点</p>");
// 生成雷达图
if (group.BestPath != null)
{
string chartId = $"radarChart_{group.GroupId}";
sb.AppendLine(" <div class=\"chart-container\">");
sb.AppendLine($" <canvas id=\"{chartId}\"></canvas>");
sb.AppendLine(" </div>");
sb.AppendLine(GenerateRadarChartScript(chartId, group.BestPath));
}
sb.AppendLine(" </div>");
return sb.ToString();
}
private string GenerateAllPathsTable(PathAnalysisReportData data)
{
var sb = new StringBuilder();
sb.AppendLine(" <div class=\"card\">");
sb.AppendLine(" <div class=\"card-title\">📋 所有路径详细数据</div>");
sb.AppendLine(" <table class=\"data-table\">");
sb.AppendLine(" <thead>");
sb.AppendLine(" <tr>");
sb.AppendLine(" <th>路径名称</th>");
sb.AppendLine(" <th>长度(m)</th>");
sb.AppendLine(" <th>时间(s)</th>");
sb.AppendLine(" <th>碰撞</th>");
sb.AppendLine(" <th>安全分</th>");
sb.AppendLine(" <th>效率分</th>");
sb.AppendLine(" <th>转弯分</th>");
sb.AppendLine(" <th>直达分</th>");
sb.AppendLine(" <th>综合分</th>");
sb.AppendLine(" </tr>");
sb.AppendLine(" </thead>");
sb.AppendLine(" <tbody>");
foreach (var analysis in data.AllPathAnalyses.OrderByDescending(a => a.WeightedScore))
{
string rowClass = analysis.IsBestInGroup ? "best-row" : "";
sb.AppendLine($" <tr class=\"{rowClass}\">");
sb.AppendLine($" <td>{EscapeHtml(analysis.RouteName)}</td>");
sb.AppendLine($" <td>{analysis.TotalLength:F1}</td>");
sb.AppendLine($" <td>{analysis.CollisionCount}</td>");
sb.AppendLine($" <td>{GenerateScoreBar(analysis.SafetyScore)}</td>");
sb.AppendLine($" <td>{GenerateScoreBar(analysis.EfficiencyScore)}</td>");
sb.AppendLine($" <td>{GenerateScoreBar(analysis.TurnDifficultyScore)}</td>");
sb.AppendLine($" <td>{GenerateScoreBar(analysis.TortuosityScore)}</td>");
sb.AppendLine($" <td><strong>{analysis.WeightedScore:F1}</strong></td>");
sb.AppendLine(" </tr>");
}
sb.AppendLine(" </tbody>");
sb.AppendLine(" </table>");
sb.AppendLine(" </div>");
return sb.ToString();
}
private string GenerateSuggestions(PathAnalysisReportData data)
{
var sb = new StringBuilder();
sb.AppendLine(" <div class=\"card\">");
sb.AppendLine(" <div class=\"card-title\">💡 优化建议</div>");
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($" <div class=\"suggestion-item {priorityClass}\">");
sb.AppendLine($" <span class=\"suggestion-category\" style=\"background: {categoryColor}\">{suggestion.CategoryName}</span>");
sb.AppendLine($" <h4>{EscapeHtml(suggestion.Title)}</h4>");
sb.AppendLine($" <p>{EscapeHtml(suggestion.Description)}</p>");
if (!string.IsNullOrEmpty(suggestion.RelatedRouteName))
{
sb.AppendLine($" <small>相关路径: {EscapeHtml(suggestion.RelatedRouteName)}</small>");
}
sb.AppendLine(" </div>");
}
}
else
{
sb.AppendLine(" <p>暂无优化建议</p>");
}
sb.AppendLine(" </div>");
return sb.ToString();
}
private string GenerateTechnicalParameters(PathAnalysisReportData data)
{
var sb = new StringBuilder();
sb.AppendLine(" <div class=\"card\">");
sb.AppendLine(" <div class=\"card-title\">⚙️ 技术参数</div>");
if (data.VehicleParams != null)
{
sb.AppendLine(" <div class=\"grid-3\">");
sb.AppendLine(" <div class=\"metric-card\">");
sb.AppendLine(" <div class=\"metric-label\">车辆长度</div>");
sb.AppendLine($" <div class=\"metric-value\">{data.VehicleParams.Length:F2}m</div>");
sb.AppendLine(" </div>");
sb.AppendLine(" <div class=\"metric-card\">");
sb.AppendLine(" <div class=\"metric-label\">车辆宽度</div>");
sb.AppendLine($" <div class=\"metric-value\">{data.VehicleParams.Width:F2}m</div>");
sb.AppendLine(" </div>");
sb.AppendLine(" <div class=\"metric-card\">");
sb.AppendLine(" <div class=\"metric-label\">车辆高度</div>");
sb.AppendLine($" <div class=\"metric-value\">{data.VehicleParams.Height:F2}m</div>");
sb.AppendLine(" </div>");
sb.AppendLine(" </div>");
}
sb.AppendLine(" <div style=\"margin-top: 20px;\">");
sb.AppendLine(" <p><strong>评分标准:</strong></p>");
sb.AppendLine(" <ul style=\"margin-left: 20px; margin-top: 10px;\">");
sb.AppendLine(" <li>安全性: 基于碰撞数量和热点检测</li>");
sb.AppendLine(" <li>效率: 基于路径长度比较</li>");
sb.AppendLine(" <li>转弯难度: 基于转弯次数</li>");
sb.AppendLine(" <li>直达性: 越高表示路径越直接</li>");
sb.AppendLine(" </ul>");
sb.AppendLine(" </div>");
sb.AppendLine(" </div>");
return sb.ToString();
}
private string GenerateFooter(PathAnalysisReportData data)
{
var sb = new StringBuilder();
sb.AppendLine(" <div class=\"footer\">");
sb.AppendLine($" <p>NavisworksTransport 路径规划分析系统</p>");
sb.AppendLine($" <p>报告生成时间: {data.ReportTime:yyyy-MM-dd HH:mm:ss}</p>");
sb.AppendLine(" </div>");
sb.AppendLine("</div>"); // 关闭container
return sb.ToString();
}
#endregion
#region
private string GenerateScoreBar(double score)
{
string barClass = score >= 80 ? "high" : score >= 60 ? "medium" : "low";
return $"<div class=\"score-bar-container\"><div class=\"score-bar {barClass}\" style=\"width: {score}%\"></div><span class=\"score-text\">{score:F0}</span></div>";
}
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("<script>");
sb.AppendLine($" new Chart(document.getElementById('{chartId}'), {{");
sb.AppendLine(" type: 'radar',");
sb.AppendLine(" data: {");
sb.AppendLine($" labels: [{string.Join(", ", labels)}],");
sb.AppendLine(" datasets: [{");
sb.AppendLine($" label: '{EscapeHtml(analysis.RouteName)}',");
sb.AppendLine($" data: [{string.Join(", ", data)}],");
sb.AppendLine(" backgroundColor: 'rgba(44, 90, 160, 0.2)',");
sb.AppendLine(" borderColor: 'rgba(44, 90, 160, 1)',");
sb.AppendLine(" pointBackgroundColor: 'rgba(44, 90, 160, 1)',");
sb.AppendLine(" borderWidth: 2");
sb.AppendLine(" }]");
sb.AppendLine(" },");
sb.AppendLine(" options: {");
sb.AppendLine(" responsive: true,");
sb.AppendLine(" maintainAspectRatio: false,");
sb.AppendLine(" scales: {");
sb.AppendLine(" r: {");
sb.AppendLine(" beginAtZero: true,");
sb.AppendLine(" max: 100,");
sb.AppendLine(" ticks: { stepSize: 20 }");
sb.AppendLine(" }");
sb.AppendLine(" },");
sb.AppendLine(" plugins: {");
sb.AppendLine(" legend: { display: false }");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine(" });");
sb.AppendLine("</script>");
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("&", "&amp;")
.Replace("<", "&lt;")
.Replace(">", "&gt;")
.Replace("\"", "&quot;")
.Replace("'", "&#39;");
}
#endregion
}
/// <summary>
/// 分析报告数据与ViewModel数据兼容
/// </summary>
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<EndpointGroup> Groups { get; set; }
public List<PathDetailedAnalysis> AllPathAnalyses { get; set; }
public PathDetailedAnalysis OverallBestPath { get; set; }
public List<CategorizedSuggestion> AllSuggestions { get; set; }
public string DocumentName { get; set; }
public VehicleParameters VehicleParams { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// 路径分析服务 - 简化
/// 负责路径碰撞分析和评分计算
/// 路径分析服务 - 扩展
/// 负责路径碰撞分析、多维度评分计算和同终点组分析
/// </summary>
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();
}
/// <summary>
@ -264,4 +272,135 @@ namespace NavisworksTransport
/// <summary>优化建议列表</summary>
public List<string> 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);
}
/// <summary>
/// 执行完整的多维度路径分析
/// </summary>
public PathDetailedAnalysis AnalyzePathDetailed(PathRoute route, string strategy)
{
var context = new AnalysisContext { Strategy = strategy };
return _engine.AnalyzePath(route, context);
}
/// <summary>
/// 分析多条路径并按终点分组
/// </summary>
public List<EndpointGroup> AnalyzePathsWithGrouping(List<PathRoute> routes, string strategy)
{
return _engine.AnalyzeAndGroupPaths(routes, strategy);
}
/// <summary>
/// 获取同终点组的详细分析
/// </summary>
public EndpointGroupAnalysis GetEndpointGroupAnalysis(EndpointGroup group, string strategy)
{
var analysis = new EndpointGroupAnalysis
{
Group = group,
BestPath = _engine.FindBestPathInGroup(group, strategy),
Suggestions = new List<CategorizedSuggestion>()
};
// 为组内每条路径生成建议
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;
}
/// <summary>
/// 保存详细分析结果到数据库
/// </summary>
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);
}
/// <summary>
/// 生成建议供View使用
/// </summary>
public List<CategorizedSuggestion> GenerateSuggestionsForView(PathDetailedAnalysis analysis, EndpointGroup group)
{
return _engine.GenerateSuggestions(analysis, group);
}
}
/// <summary>
/// 同终点组详细分析结果
/// </summary>
public class EndpointGroupAnalysis
{
/// <summary>组信息</summary>
public EndpointGroup Group { get; set; }
/// <summary>最佳路径</summary>
public PathDetailedAnalysis BestPath { get; set; }
/// <summary>组内路径对比</summary>
public List<PathComparisonDetail> Comparisons { get; set; } = new List<PathComparisonDetail>();
/// <summary>优化建议</summary>
public List<CategorizedSuggestion> Suggestions { get; set; } = new List<CategorizedSuggestion>();
/// <summary>分析策略</summary>
public string Strategy { get; set; }
}
/// <summary>
/// 路径对比详情
/// </summary>
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
}

View File

@ -493,18 +493,17 @@ namespace NavisworksTransport
}
/// <summary>
/// 重新计算路径总长度
/// 【已废弃】重新计算路径总长度
///
/// TotalLength 现在是计算属性,自动从 Edges/Points 实时计算,无需手动调用此方法。
/// 保留此方法用于兼容,但实际不执行任何操作。
/// </summary>
/// <param name="route">路径</param>
[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 计算
}
}
}

View File

@ -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
/// <summary>
/// 保存分析评分结果
/// </summary>
@ -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
/// <summary>
/// 获取路径最新的ClashDetective检测结果
/// </summary>
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;
}
/// <summary>
/// 保存详细分析结果(扩展字段)
/// </summary>
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;
}
}
/// <summary>
/// 检查并扩展 AnalysisResults 表结构(用于升级现有数据库)
/// </summary>
public void EnsureAnalysisResultsTableExtended()
{
try
{
// 检查表是否存在扩展字段
var checkSql = "PRAGMA table_info(AnalysisResults)";
var existingColumns = new HashSet<string>();
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<string, string>
{
["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
}
/// <summary>
@ -2654,7 +2819,7 @@ namespace NavisworksTransport
}
/// <summary>
/// 路径分析结果数据模型
/// 路径分析结果数据模型(扩展版)
/// </summary>
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
}

View File

@ -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;

View File

@ -517,9 +517,67 @@ namespace NavisworksTransport
public List<string> AssociatedChannelIds { get; set; }
/// <summary>
/// 路径总长度(米)
/// 路径总长度(米)- 只读计算属性
///
/// 【重要】此属性实时从几何数据计算,不从数据库存储或加载。
/// 数据库中的 TotalLength 字段已废弃,保留仅用于兼容旧版本。
///
/// 计算方式:
/// - 地面路径(Ground):从 Edges.PhysicalLength 累加(支持圆弧)
/// - 空轨路径(Rail):从 Points 计算直线距离
/// - 吊装路径(Hoisting):从 Points 计算直线距离
/// </summary>
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();
}
}
/// <summary>
/// 从路径点计算直线距离总长度(辅助方法)
/// </summary>
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;
}
/// <summary>
/// 创建时间
@ -640,7 +698,7 @@ namespace NavisworksTransport
Id = Guid.NewGuid().ToString();
EstimatedTime = 0.0;
AssociatedChannelIds = new List<string>();
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<string>();
TotalLength = 0.0;
// TotalLength计算属性自动从几何数据计算无需初始化
CreatedTime = DateTime.Now;
TurnRadius = 0.0; // 0.0 表示未设置,实际使用时从配置获取默认值
IsCurved = false;
@ -762,38 +820,6 @@ namespace NavisworksTransport
return sortedPoints;
}
/// <summary>
/// 重新计算路径长度
/// </summary>
public void RecalculateLength()
{
UpdateTotalLength();
}
/// <summary>
/// 更新路径总长度
/// </summary>
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);
}
}
/// <summary>
/// 重新计算路径数据并保存到数据库
/// 统一的路径数据更新入口:重新设置索引、更新长度、计算曲线、保存到数据库
@ -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,

View File

@ -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
{
/// <summary>
/// 路径分析视图模型
/// 用于路径规划分析对话框的数据绑定和业务逻辑
/// 路径分析视图模型 - 扩展版
/// 支持多维度评分和同终点组分析
/// </summary>
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
/// <summary>
/// 可用策略列表
/// </summary>
public List<string> AvailableStrategies => AnalysisStrategies.GetAllStrategies();
/// <summary>
/// 可用路径列表
/// </summary>
public ObservableCollection<PathAnalysisItem> AvailablePaths { get; set; }
/// <summary>
/// 选中的路径列表(用于对比分析和结果显示)
/// 选中的路径列表
/// </summary>
public ObservableCollection<PathAnalysisItem> SelectedPaths { get; set; }
/// <summary>
/// 分析策略(安全优先/效率优先)
/// 分析策略
/// </summary>
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
}
/// <summary>
/// 优化建议列表
/// 同终点组列表
/// </summary>
public ObservableCollection<OptimizationSuggestion> OptimizationSuggestions { get; set; }
public ObservableCollection<EndpointGroupViewModel> EndpointGroups { get; set; }
/// <summary>
/// 瓶颈位置列表
/// 当前选中的组
/// </summary>
public ObservableCollection<BottleneckLocation> BottleneckLocations { get; set; }
public EndpointGroupViewModel SelectedGroup
{
get => _selectedGroup;
set
{
_selectedGroup = value;
OnPropertyChanged();
UpdateSelectedGroupDetails();
}
}
/// <summary>
/// 开始分析命令
/// 当前选中的路径详细分析
/// </summary>
public PathDetailedAnalysis SelectedPathAnalysis
{
get => _selectedPathAnalysis;
set
{
_selectedPathAnalysis = value;
OnPropertyChanged();
}
}
/// <summary>
/// 优化建议列表(分类)
/// </summary>
public ObservableCollection<CategorizedSuggestionViewModel> CategorizedSuggestions { get; set; }
/// <summary>
/// 四维度分数
/// </summary>
public ObservableCollection<DimensionScoreViewModel> DimensionScores { get; set; }
/// <summary>
/// 命令
/// </summary>
public ICommand StartAnalysisCommand { get; private set; }
/// <summary>
/// 全选命令
/// </summary>
public ICommand SelectAllCommand { get; private set; }
/// <summary>
/// 清空选择命令
/// </summary>
public ICommand ClearSelectionCommand { get; private set; }
/// <summary>
/// 导出报告命令
/// </summary>
public ICommand ExportReportCommand { get; private set; }
public ICommand SelectGroupCommand { get; private set; }
public ICommand HighlightBestPathCommand { get; private set; }
#endregion
#region
/// <summary>
/// 初始化路径分析视图模型
/// </summary>
public PathAnalysisViewModel()
{
try
{
InitializeCollections();
InitializeCommands();
// 确保PathPlanningManager的数据库已初始化
EnsurePathDatabaseInitialized();
LoadPathData();
LogManager.Info("PathAnalysisViewModel初始化完成");
}
@ -144,30 +170,25 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
/// <summary>
/// 初始化命令
/// </summary>
private void InitializeCollections()
{
AvailablePaths = new ObservableCollection<PathAnalysisItem>();
SelectedPaths = new ObservableCollection<PathAnalysisItem>();
EndpointGroups = new ObservableCollection<EndpointGroupViewModel>();
CategorizedSuggestions = new ObservableCollection<CategorizedSuggestionViewModel>();
DimensionScores = new ObservableCollection<DimensionScoreViewModel>();
}
private void InitializeCommands()
{
StartAnalysisCommand = new RelayCommand(ExecuteStartAnalysis, CanExecuteStartAnalysis);
SelectAllCommand = new RelayCommand(SelectAll);
ClearSelectionCommand = new RelayCommand(ClearSelection);
ExportReportCommand = new RelayCommand(ExportReport, CanExportReport);
SelectGroupCommand = new RelayCommand<EndpointGroupViewModel>(SelectGroup);
HighlightBestPathCommand = new RelayCommand(HighlightBestPath, CanHighlightBestPath);
}
private bool CanExecuteStartAnalysis()
{
return !IsAnalyzing && AvailablePaths.Any(p => p.IsSelected);
}
private void ExecuteStartAnalysis()
{
PerformAnalysis();
}
/// <summary>
/// 确保PathPlanningManager的路径数据库已初始化
/// </summary>
private void EnsurePathDatabaseInitialized()
{
try
@ -183,40 +204,20 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#endregion
#region
#region
/// <summary>
/// 初始化集合
/// </summary>
private void InitializeCollections()
{
AvailablePaths = new ObservableCollection<PathAnalysisItem>();
SelectedPaths = new ObservableCollection<PathAnalysisItem>();
OptimizationSuggestions = new ObservableCollection<OptimizationSuggestion>();
BottleneckLocations = new ObservableCollection<BottleneckLocation>();
}
/// <summary>
/// 加载真实路径数据
/// </summary>
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
/// <summary>
/// 全选所有路径
/// </summary>
private void SelectAll()
private bool CanExecuteStartAnalysis()
{
foreach (var path in AvailablePaths)
{
path.IsSelected = true;
}
CommandManager.InvalidateRequerySuggested();
return !IsAnalyzing && AvailablePaths.Any(p => p.IsSelected);
}
/// <summary>
/// 清空所有选择
/// </summary>
private void ClearSelection()
{
foreach (var path in AvailablePaths)
{
path.IsSelected = false;
}
CommandManager.InvalidateRequerySuggested();
}
/// <summary>
/// 更新推荐路径
/// </summary>
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);
}
}
/// <summary>
/// 执行路径分析
/// </summary>
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
}
}
/// <summary>
/// 导出报告
/// </summary>
private void ExportReport()
private void UpdateAnalysisResults(List<EndpointGroup> groups, List<PathAnalysisItem> 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<PathDetailedAnalysis>(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}");
}
RecommendedPath = bestItem;
// 写入文件
System.IO.File.WriteAllText(filePath, report.ToString(), System.Text.Encoding.UTF8);
AnalysisStatus = $"报告已导出到:{filePath}";
LogManager.Info($"分析报告已导出:{filePath}");
// 打开文件所在目录并选中文件
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{filePath}\"");
// 更新雷达图数据
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;
}
}
/// <summary>
/// 判断是否可以导出报告
/// </summary>
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;
}
/// <summary>
/// 更新分析结果到UI
/// </summary>
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<PathDetailedAnalysis>(),
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<PathDetailedAnalysis> 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
/// <summary>
/// 路径分析项目
/// </summary>
public class PathAnalysisItem : INotifyPropertyChanged
{
private bool _isSelected;
@ -563,74 +628,40 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private double _averageSpeed;
private int _score;
/// <summary>路径ID</summary>
public string RouteId { get; set; }
/// <summary>路径名称</summary>
public string Name { get; set; }
/// <summary>路径类型</summary>
public string Type { get; set; }
/// <summary>路径长度(米)</summary>
public double Length { get; set; }
/// <summary>估算时间(秒)</summary>
public int EstimatedTime { get; set; }
/// <summary>碰撞次数</summary>
public int CollisionCount
{
get => _collisionCount;
set
{
_collisionCount = value;
OnPropertyChanged();
}
set { _collisionCount = value; OnPropertyChanged(); }
}
/// <summary>受影响对象数</summary>
public int AffectedObjects
{
get => _affectedObjects;
set
{
_affectedObjects = value;
OnPropertyChanged();
}
set { _affectedObjects = value; OnPropertyChanged(); }
}
/// <summary>平均速度(m/s)</summary>
public double AverageSpeed
{
get => _averageSpeed;
set
{
_averageSpeed = value;
OnPropertyChanged();
}
set { _averageSpeed = value; OnPropertyChanged(); }
}
/// <summary>综合评分</summary>
public int Score
{
get => _score;
set
{
_score = value;
OnPropertyChanged();
}
set { _score = value; OnPropertyChanged(); }
}
/// <summary>是否被选中用于分析</summary>
public bool IsSelected
{
get => _isSelected;
set
{
_isSelected = value;
OnPropertyChanged();
}
set { _isSelected = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
@ -641,39 +672,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
/// <summary>
/// 优化建议
/// </summary>
public class OptimizationSuggestion
{
/// <summary>建议标题</summary>
public string Title { get; set; }
/// <summary>建议描述</summary>
public string Description { get; set; }
/// <summary>优先级</summary>
public string Priority { get; set; }
/// <summary>状态</summary>
public string Status { get; set; }
}
/// <summary>
/// 瓶颈位置
/// </summary>
public class BottleneckLocation
{
/// <summary>位置名称</summary>
public string Name { get; set; }
/// <summary>影响路径数</summary>
public int AffectedPaths { get; set; }
/// <summary>严重程度</summary>
public string Severity { get; set; }
/// <summary>改进建议</summary>
public string Suggestion { get; set; }
}

View File

@ -1,286 +1,337 @@
<!--
NavisworksTransport 路径规划分析对话框 - 采用与主界面一致的Navisworks 2026风格
功能说明:
1. 多路径对比分析,支持路径选择和维度对比
2. 图文化展示分析结果(条形图/表格)
3. 根据业务目标(安全优先/效率优先)提供调整建议
4. 最佳路径推荐和导出功能
设计原则参考CollisionReportDialog的布局和样式使用统一的蓝色主题
NavisworksTransport 路径规划分析对话框 V2
支持多维度评分和同终点组分析
-->
<Window x:Class="NavisworksTransport.UI.WPF.Views.PathAnalysisDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:NavisworksTransport.UI.WPF.Views"
xmlns:vm="clr-namespace:NavisworksTransport.UI.WPF.ViewModels"
xmlns:cmd="clr-namespace:NavisworksTransport.UI.WPF.Commands"
mc:Ignorable="d"
Title="路径规划分析 - 多路径对比"
Height="800"
Width="1200"
Height="850"
Width="1300"
ResizeMode="CanResize"
WindowStartupLocation="CenterOwner"
MinHeight="600"
MinWidth="900">
MinHeight="700"
MinWidth="1100">
<Window.Resources>
<!-- 引用共享的Navisworks 2026样式资源 -->
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 转换器定义 -->
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<!-- 颜色定义 -->
<SolidColorBrush x:Key="PrimaryBrush" Color="#2C5AA0"/>
<SolidColorBrush x:Key="SuccessBrush" Color="#28A745"/>
<SolidColorBrush x:Key="WarningBrush" Color="#FFC107"/>
<SolidColorBrush x:Key="DangerBrush" Color="#DC3545"/>
<SolidColorBrush x:Key="InfoBrush" Color="#17A2B8"/>
<!-- 路径分析专用样式 -->
<Style x:Key="AnalysisCardStyle" TargetType="Border">
<!-- 卡片样式 -->
<Style x:Key="CardStyle" TargetType="Border">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#FFD4E7FF"/>
<Setter Property="BorderBrush" Value="#FFE0E0E0"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="CornerRadius" Value="6"/>
<Setter Property="Padding" Value="15"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="Gray" Direction="315" ShadowDepth="2" Opacity="0.3" BlurRadius="4"/>
<DropShadowEffect Color="Gray" Direction="315" ShadowDepth="1" Opacity="0.2" BlurRadius="3"/>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SectionTitleStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{StaticResource NavisworksPrimaryBrush}"/>
<Setter Property="Margin" Value="0,0,0,10"/>
</Style>
<Style x:Key="MetricLabelStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="11"/>
<Setter Property="Foreground" Value="#FF666666"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="MetricValueStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="{StaticResource NavisworksPrimaryBrush}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style x:Key="ChartBarStyle" TargetType="Rectangle">
<Setter Property="Fill" Value="{StaticResource NavisworksPrimaryBrush}"/>
<Setter Property="Height" Value="25"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="5,2"/>
</Style>
<Style x:Key="RecommendationItemStyle" TargetType="Border">
<Setter Property="Background" Value="#FFF0F8FF"/>
<Setter Property="BorderBrush" Value="#FFB0D4F1"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="3"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="Margin" Value="0,3"/>
</Style>
<Style x:Key="BestPathStyle" TargetType="Border">
<!-- 最佳路径卡片样式 -->
<Style x:Key="BestPathCardStyle" TargetType="Border">
<Setter Property="Background" Value="#FFF0F8FF"/>
<Setter Property="BorderBrush" Value="#FF28A745"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="CornerRadius" Value="5"/>
<Setter Property="Padding" Value="15"/>
<Setter Property="Margin" Value="0,10"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="20"/>
</Style>
<!-- 维度分数条样式 -->
<Style x:Key="ScoreBarStyle" TargetType="ProgressBar">
<Setter Property="Height" Value="20"/>
<Setter Property="Maximum" Value="100"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="#FFE9ECEF"/>
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}"/>
</Style>
<!-- 建议项样式 -->
<Style x:Key="SuggestionItemStyle" TargetType="Border">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#FFE0E0E0"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Padding" Value="12"/>
<Setter Property="Margin" Value="0,4"/>
</Style>
<!-- 组项样式 -->
<Style x:Key="GroupItemStyle" TargetType="Border">
<Setter Property="Background" Value="#FFF8F9FA"/>
<Setter Property="BorderBrush" Value="#FFDEE2E6"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Padding" Value="12"/>
<Setter Property="Margin" Value="0,4"/>
</Style>
<!-- 值转换器 -->
<local:ScoreToColorConverter x:Key="ScoreToColorConverter"/>
<local:PriorityToColorConverter x:Key="PriorityToColorConverter"/>
<local:StrategyDescriptionConverter x:Key="StrategyDescriptionConverter"/>
<local:BoolToBackgroundConverter x:Key="BoolToBackgroundConverter"/>
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter"/>
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid Background="#FFF5F5F5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 头部标题 -->
<Border Grid.Row="0" Background="{StaticResource PrimaryBrush}" Padding="20,15">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<TextBlock Text="📊 路径规划分析" FontSize="20" FontWeight="Bold" Foreground="White"/>
<TextBlock Text="| 多维度对比分析" FontSize="14" Foreground="#FFCCCCCC" Margin="15,3,0,0"/>
</StackPanel>
<TextBlock Grid.Column="1" Text="{Binding AnalysisStatus}" FontSize="12" Foreground="#FFCCCCCC" VerticalAlignment="Center"/>
</Grid>
</Border>
<!-- 主内容区域 -->
<Grid Grid.Row="0" Margin="15">
<Grid Grid.Row="1" Margin="15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300"/>
<ColumnDefinition Width="280"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="320"/>
<ColumnDefinition Width="350"/>
</Grid.ColumnDefinitions>
<!-- 左侧:路径选择面板 -->
<Border Grid.Column="0" Style="{StaticResource AnalysisCardStyle}">
<StackPanel>
<TextBlock Text="📋 路径选择" Style="{StaticResource SectionTitleStyle}"/>
<!-- 左侧面板:路径选择和策略 -->
<StackPanel Grid.Column="0">
<!-- 路径选择卡片 -->
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock Text="📋 路径选择" FontSize="14" FontWeight="Bold" Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,10"/>
<!-- 路径列表 -->
<ScrollViewer Height="200" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding AvailablePaths}">
<ScrollViewer Height="250" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding AvailablePaths}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#FFF8F9FA" BorderBrush="#FFDEE2E6" BorderThickness="1"
CornerRadius="4" Padding="10" Margin="0,3">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected, Mode=TwoWay}"
VerticalAlignment="Center" Margin="0,0,8,0"/>
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" FontSize="12"/>
<TextBlock FontSize="10" Foreground="#FF6C757D">
<Run Text="长度: "/>
<Run Text="{Binding Length, StringFormat={}{0:F1}m}"/>
<Run Text=" | 碰撞: "/>
<Run Text="{Binding CollisionCount}"/>
</TextBlock>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<Button Content="全选" Command="{Binding SelectAllCommand}"
Style="{StaticResource SecondaryButtonStyle}" Width="65" Margin="0,0,8,0"/>
<Button Content="清空" Command="{Binding ClearSelectionCommand}"
Style="{StaticResource SecondaryButtonStyle}" Width="65"/>
</StackPanel>
</StackPanel>
</Border>
<!-- 策略选择卡片 -->
<Border Style="{StaticResource CardStyle}" Margin="0,10,0,0">
<StackPanel>
<TextBlock Text="🎯 分析策略" FontSize="14" FontWeight="Bold" Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,10"/>
<ComboBox ItemsSource="{Binding AvailableStrategies}"
SelectedItem="{Binding AnalysisStrategy}"
Height="32" Margin="0,5"/>
<TextBlock Text="{Binding AnalysisStrategy, Converter={StaticResource StrategyDescriptionConverter}}"
FontSize="11" Foreground="#FF6C757D" TextWrapping="Wrap" Margin="0,5,0,0"/>
<Button Content="开始分析" Command="{Binding StartAnalysisCommand}"
Style="{StaticResource ActionButtonStyle}" Height="38" Margin="0,15,0,0"/>
</StackPanel>
</Border>
</StackPanel>
<!-- 中间面板:分析结果和组列表 -->
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- 最佳路径卡片 -->
<Border Style="{StaticResource BestPathCardStyle}"
Visibility="{Binding RecommendedPath, Converter={StaticResource NullToVisibilityConverter}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock FontSize="12" Foreground="#FF28A745" FontWeight="SemiBold">
🏆 推荐最佳路径
</TextBlock>
<TextBlock Text="{Binding RecommendedPath.Name}" FontSize="18" FontWeight="Bold"
Foreground="#FF2C3E50" Margin="0,5"/>
<TextBlock FontSize="12" Foreground="#FF6C757D">
<Run Text="长度: "/>
<Run Text="{Binding RecommendedPath.Length, StringFormat={}{0:F1}m}"/>
<Run Text=" | 评分: "/>
<Run Text="{Binding RecommendedPath.Score, StringFormat={}{0:F1}}"/>
</TextBlock>
</StackPanel>
<Button Grid.Column="1" Content="高亮显示" Command="{Binding HighlightBestPathCommand}"
Style="{StaticResource ActionButtonStyle}" Width="90" Height="32"/>
</Grid>
</Border>
<!-- 同终点组列表 -->
<Border Style="{StaticResource CardStyle}" Margin="0,10,0,0">
<StackPanel>
<TextBlock Text="🎯 同终点路径组" FontSize="14" FontWeight="Bold"
Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,10"/>
<ItemsControl ItemsSource="{Binding EndpointGroups}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource GroupItemStyle}"
Background="{Binding IsSelected, Converter={StaticResource BoolToBackgroundConverter}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock Text="{Binding GroupName}" FontWeight="SemiBold" FontSize="12"/>
<TextBlock Text="{Binding RouteCount, StringFormat=({0}条路径)}"
FontSize="11" Foreground="#FF6C757D" Margin="8,0,0,0"/>
<Border Background="#FF28A745" CornerRadius="3" Padding="5,2" Margin="8,0,0,0"
Visibility="{Binding BestPath.IsBestInGroup, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock Text="最佳" FontSize="9" Foreground="White"/>
</Border>
</StackPanel>
<TextBlock Grid.Row="1" Text="{Binding EndPointDescription}"
FontSize="10" Foreground="#FF6C757D" Margin="0,3,0,0"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<!-- 五维度分数展示 -->
<Border Style="{StaticResource CardStyle}" Margin="0,10,0,0">
<StackPanel>
<TextBlock Text="📊 四维度评分" FontSize="14" FontWeight="Bold"
Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,10"/>
<ItemsControl ItemsSource="{Binding DimensionScores}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Name}" FontSize="11"
VerticalAlignment="Center"/>
<ProgressBar Grid.Column="1" Value="{Binding Score}"
Maximum="{Binding MaxScore}" Style="{StaticResource ScoreBarStyle}"/>
<TextBlock Grid.Column="2" Text="{Binding Score, StringFormat={}{0:F1}}"
FontSize="11" FontWeight="Bold" HorizontalAlignment="Right"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<!-- 右侧面板:建议列表 -->
<Border Grid.Column="2" Style="{StaticResource CardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="💡 优化建议" FontSize="14" FontWeight="Bold"
Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,10"/>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding CategorizedSuggestions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#FFF8FBFF" BorderBrush="#FFD4E7FF" BorderThickness="1" CornerRadius="3" Padding="8" Margin="0,2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected, Mode=TwoWay}" VerticalAlignment="Center" Margin="0,0,8,0"/>
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" FontSize="11"/>
<TextBlock FontSize="9" Foreground="#FF666666">
<Run Text="长度: "/>
<Run Text="{Binding Length, StringFormat={}{0:F1}m}"/>
<Run Text=" | 时间: "/>
<Run Text="{Binding EstimatedTime}"/>
<Run Text="s"/>
</TextBlock>
<Border Style="{StaticResource SuggestionItemStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
<Border Background="{Binding Priority, Converter={StaticResource PriorityToColorConverter}}"
CornerRadius="3" Padding="4,2">
<TextBlock Text="{Binding CategoryName}" FontSize="9" Foreground="White"/>
</Border>
<TextBlock Text="{Binding RelatedRouteName}" FontSize="10"
Foreground="#FF6C757D" Margin="8,0,0,0" FontStyle="Italic"/>
</StackPanel>
</Grid>
<TextBlock Text="{Binding Title}" FontSize="12" FontWeight="SemiBold"
TextWrapping="Wrap"/>
<TextBlock Text="{Binding Description}" FontSize="11" Foreground="#FF6C757D"
TextWrapping="Wrap" Margin="0,3,0,0"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<!-- 操作按钮 -->
<StackPanel Orientation="Horizontal" Margin="0,15,0,0">
<Button Content="全选" Command="{Binding SelectAllCommand}" Style="{StaticResource SecondaryButtonStyle}" Width="60" Margin="0,0,8,0"/>
<Button Content="清空" Command="{Binding ClearSelectionCommand}" Style="{StaticResource SecondaryButtonStyle}" Width="60"/>
</StackPanel>
<!-- 分析策略 -->
<TextBlock Text="🎯 分析策略" Style="{StaticResource SectionTitleStyle}" Margin="0,20,0,10"/>
<RadioButton Content="安全优先(避开碰撞)"
GroupName="AnalysisStrategy"
Tag="安全优先"
Checked="StrategyRadioButton_Checked"
IsChecked="True"
Margin="0,3"/>
<RadioButton Content="效率优先(最短路径)"
GroupName="AnalysisStrategy"
Tag="效率优先"
Checked="StrategyRadioButton_Checked"
Margin="0,3"/>
<!-- 分析按钮 -->
<Button Content="开始分析"
Command="{Binding StartAnalysisCommand}"
Style="{StaticResource ActionButtonStyle}"
Height="35"
Margin="0,20,0,0"/>
</StackPanel>
</Border>
<!-- 中央:数据对比展示 -->
<Border Grid.Column="1" Style="{StaticResource AnalysisCardStyle}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<TextBlock Text="📊 对比分析" Style="{StaticResource SectionTitleStyle}"/>
<!-- 对比表格 -->
<DataGrid ItemsSource="{Binding SelectedPaths}"
AutoGenerateColumns="False" HeadersVisibility="Column" CanUserReorderColumns="False"
CanUserResizeColumns="True" CanUserSortColumns="False" IsReadOnly="True"
BorderBrush="#FFD4E7FF" BorderThickness="1" GridLinesVisibility="Horizontal"
HorizontalGridLinesBrush="#FFEEEEEE" RowBackground="White" AlternatingRowBackground="#FFF8FBFF"
Height="120">
<DataGrid.Columns>
<DataGridTextColumn Header="路径名称" Binding="{Binding Name}" Width="180"/>
<DataGridTextColumn Header="长度(m)" Binding="{Binding Length, StringFormat={}{0:F1}}" Width="80"/>
<DataGridTextColumn Header="时间(s)" Binding="{Binding EstimatedTime}" Width="80"/>
<DataGridTextColumn Header="碰撞" Binding="{Binding CollisionCount}" Width="60"/>
<DataGridTextColumn Header="评分" Binding="{Binding Score}" Width="60"/>
</DataGrid.Columns>
</DataGrid>
<!-- 推荐路径 -->
<TextBlock Text="🏆 推荐路径" Style="{StaticResource SectionTitleStyle}" Margin="0,25,0,15"/>
<Border Background="#FFF0F8FF" BorderBrush="#FFD4E7FF" BorderThickness="1" CornerRadius="3" Padding="15" Margin="0,0,0,15">
<StackPanel>
<TextBlock FontSize="14" FontWeight="SemiBold" Foreground="#FF2C3E50">
<TextBlock.Text>
<MultiBinding StringFormat="推荐路径: {0}">
<Binding Path="RecommendedPath.Name" FallbackValue="未选择"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock FontSize="11" Foreground="#FF7F8C8D" Margin="0,5,0,0">
<TextBlock.Text>
<MultiBinding StringFormat="长度: {0:F1}米 | 时间: {1}秒 | 碰撞: {2}次 | 评分: {3}">
<Binding Path="RecommendedPath.Length" FallbackValue="0"/>
<Binding Path="RecommendedPath.EstimatedTime" FallbackValue="0"/>
<Binding Path="RecommendedPath.CollisionCount" FallbackValue="0"/>
<Binding Path="RecommendedPath.Score" FallbackValue="0"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Border>
<!-- 分析状态 -->
<TextBlock Text="📋 分析状态" Style="{StaticResource SectionTitleStyle}" Margin="0,15,0,10"/>
<Border Background="#FFFAFAFA" BorderBrush="#FFE0E0E0" BorderThickness="1" CornerRadius="3" Padding="10">
<TextBlock Text="{Binding AnalysisStatus}" FontSize="12" Foreground="#FF666666"/>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
<!-- 右侧:分析结果面板 -->
<Border Grid.Column="2" Style="{StaticResource AnalysisCardStyle}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<TextBlock Text="🏆 分析结果" Style="{StaticResource SectionTitleStyle}"/>
<!-- 优化建议 -->
<TextBlock Text="💡 优化建议" FontWeight="SemiBold" FontSize="12" Margin="0,20,0,10"/>
<ItemsControl ItemsSource="{Binding OptimizationSuggestions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource RecommendationItemStyle}">
<TextBlock Text="{Binding Title}" FontSize="10" TextWrapping="Wrap"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 导出按钮 -->
<Button Content="导出分析报告"
Command="{Binding ExportReportCommand}"
Style="{StaticResource ActionButtonStyle}"
Height="35"
Margin="0,20,0,0"/>
</StackPanel>
</ScrollViewer>
<Button Grid.Row="2" Content="导出分析报告" Command="{Binding ExportReportCommand}"
Style="{StaticResource ActionButtonStyle}" Height="38" Margin="0,10,0,0"/>
</Grid>
</Border>
</Grid>
<!-- 底部按钮区域 -->
<Border Grid.Row="1"
BorderBrush="#FFD4E7FF"
BorderThickness="1"
CornerRadius="0"
Background="#FFF8FBFF"
Margin="15,0,15,15"
Padding="15,10">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Content="重新分析"
Style="{StaticResource SecondaryButtonStyle}"
Margin="0,0,10,0"/>
<Button Content="应用推荐"
Style="{StaticResource ActionButtonStyle}"
Margin="0,0,10,0"/>
<Button Content="关闭"
Click="CloseButton_Click"
Style="{StaticResource SecondaryButtonStyle}"/>
<!-- 底部按钮 -->
<Border Grid.Row="2" Background="White" BorderBrush="#FFE0E0E0" BorderThickness="0,1,0,0"
Padding="20,12">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="重新分析" Style="{StaticResource SecondaryButtonStyle}" Width="100" Margin="0,0,10,0"/>
<Button Content="应用推荐" Style="{StaticResource ActionButtonStyle}" Width="100" Margin="0,0,10,0"/>
<Button Content="关闭" Click="CloseButton_Click" Style="{StaticResource SecondaryButtonStyle}" Width="80"/>
</StackPanel>
</Border>
</Grid>

View File

@ -1,31 +1,33 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using NavisworksTransport.UI.WPF.ViewModels;
namespace NavisworksTransport.UI.WPF.Views
{
/// <summary>
/// PathAnalysisDialog.xaml 的交互逻辑
/// 路径规划分析对话框 - 用于多路径对比分析和最佳路径推荐
/// 路径规划分析对话框 - 支持多维度评分和同终点组分析(非模态窗口)
/// </summary>
public partial class PathAnalysisDialog : Window
{
/// <summary>
/// 初始化路径分析对话框
/// </summary>
#region
private static PathAnalysisDialog _currentInstance = null;
private static readonly object _lock = new object();
#endregion
public PathAnalysisDialog()
{
try
{
InitializeComponent();
// 绑定ViewModel
DataContext = new ViewModels.PathAnalysisViewModel();
LogManager.Info("PathAnalysisDialog初始化完成已绑定PathAnalysisViewModel");
// 设置窗口标题包含当前时间
LogManager.Info("PathAnalysisDialog初始化完成");
Title = $"路径规划分析 - 多路径对比 [{DateTime.Now:MM-dd HH:mm}]";
}
catch (Exception ex)
@ -35,25 +37,59 @@ namespace NavisworksTransport.UI.WPF.Views
}
/// <summary>
/// 分析策略 RadioButton 选中事件
/// 显示路径分析窗口(单例模式,非模态)
/// </summary>
private void StrategyRadioButton_Checked(object sender, RoutedEventArgs e)
public static PathAnalysisDialog ShowAnalysis(Window owner = null)
{
if (sender is RadioButton rb && rb.Tag is string strategy)
try
{
if (DataContext is PathAnalysisViewModel viewModel)
lock (_lock)
{
viewModel.AnalysisStrategy = strategy;
LogManager.Info($"用户切换分析策略: {strategy}");
// 如果当前已有实例且仍然打开,则激活窗口
if (_currentInstance != null)
{
try
{
if (_currentInstance.IsLoaded)
{
_currentInstance.Activate();
_currentInstance.Focus();
LogManager.Info("PathAnalysisDialog已激活现有窗口");
return _currentInstance;
}
}
catch (Exception)
{
// 如果访问失败,认为窗口已关闭,创建新实例
_currentInstance = null;
}
}
// 创建新实例
var dialog = new PathAnalysisDialog();
if (owner != null)
{
dialog.Owner = owner;
}
// 设置为当前实例
_currentInstance = dialog;
// 使用Show()非模态显示,允许与主窗口同时操作
dialog.Show();
LogManager.Info("PathAnalysisDialog已以非模态方式显示");
return dialog;
}
}
catch (Exception ex)
{
LogManager.Error($"显示路径分析窗口失败: {ex.Message}");
return null;
}
}
/// <summary>
/// 关闭按钮点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
try
@ -67,16 +103,20 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
/// <summary>
/// 窗口关闭时的清理工作
/// </summary>
/// <param name="e"></param>
protected override void OnClosed(EventArgs e)
{
try
{
// 执行清理工作(如果需要)
LogManager.Info("PathAnalysisDialog已关闭执行清理");
LogManager.Info("PathAnalysisDialog已关闭");
// 清理单例引用
lock (_lock)
{
if (_currentInstance == this)
{
_currentInstance = null;
}
}
base.OnClosed(e);
}
@ -86,4 +126,118 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
}
#region
/// <summary>
/// 分数转颜色转换器
/// </summary>
public class ScoreToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double score)
{
if (score >= 80) return new SolidColorBrush(Colors.Green);
if (score >= 60) return new SolidColorBrush(Colors.Orange);
return new SolidColorBrush(Colors.Red);
}
return new SolidColorBrush(Colors.Gray);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// 优先级转颜色转换器
/// </summary>
public class PriorityToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is int priority)
{
if (priority >= 4) return new SolidColorBrush(Color.FromRgb(220, 53, 69)); // 红色 - 高优先级
if (priority >= 3) return new SolidColorBrush(Color.FromRgb(255, 193, 7)); // 黄色 - 中优先级
return new SolidColorBrush(Color.FromRgb(40, 167, 69)); // 绿色 - 低优先级
}
return new SolidColorBrush(Colors.Gray);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// 策略描述转换器
/// </summary>
public class StrategyDescriptionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string strategy)
{
switch (strategy)
{
case "安全优先":
return "优先选择无碰撞路径适合精密组件运输安全权重50%";
case "效率优先":
return "优先选择最短路径适合紧急任务效率权重40%";
case "平衡模式":
return "综合考虑安全与效率,通用场景推荐(默认)";
default:
return "请选择分析策略";
}
}
return "";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// 布尔转背景色转换器
/// </summary>
public class BoolToBackgroundConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool isSelected && isSelected)
{
return new SolidColorBrush(Color.FromRgb(230, 240, 255)); // 选中时的浅蓝色
}
return new SolidColorBrush(Colors.Transparent);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// Null转Visibility转换器
/// </summary>
public class NullToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value == null ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
#endregion
}

View File

@ -93,7 +93,7 @@ namespace NavisworksTransport.UI.WPF.Views
}
/// <summary>
/// 路径分析按钮点击事件,打开路径规划分析对话框
/// 路径分析按钮点击事件,打开路径规划分析对话框(非模态)
/// </summary>
private void OnPathAnalysisButtonClick(object sender, RoutedEventArgs e)
{
@ -101,30 +101,9 @@ namespace NavisworksTransport.UI.WPF.Views
{
LogManager.Info("路径分析按钮被点击准备打开PathAnalysisDialog窗口");
var dlg = new PathAnalysisDialog();
// 在Navisworks环境中尝试找到合适的父窗口
try
{
// 尝试设置Owner为当前WPF窗口的顶级父窗口
var parentWindow = Window.GetWindow(this);
if (parentWindow != null)
{
dlg.Owner = parentWindow;
LogManager.Info("设置PathAnalysisDialog的Owner为当前窗口");
}
else
{
LogManager.Info("未找到父窗口将以独立窗口方式显示PathAnalysisDialog");
}
}
catch (Exception ownerEx)
{
LogManager.Warning($"设置Owner失败将以独立窗口显示: {ownerEx.Message}");
}
dlg.ShowDialog();
LogManager.Info("PathAnalysisDialog窗口显示完成");
// 使用静态方法显示(单例模式,非模态)
var parentWindow = Window.GetWindow(this);
PathAnalysisDialog.ShowAnalysis(parentWindow);
}
catch (Exception ex)
{