实现路径分析功能
This commit is contained in:
parent
b36d20933e
commit
77b9da40fc
88
AGENTS.md
88
AGENTS.md
@ -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)
|
||||
|
||||
@ -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>
|
||||
@ -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" />
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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")]
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
594
doc/working/path_analysis_design.md
Normal file
594
doc/working/path_analysis_design.md
Normal 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`
|
||||
402
doc/working/path_analysis_implementation_plan.md
Normal file
402
doc/working/path_analysis_implementation_plan.md
Normal 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)
|
||||
- 策略选择(RadioButton,3种)
|
||||
- [开始分析] 按钮
|
||||
|
||||
中间面板(可变):
|
||||
- 数据对比表格(5维度分数)
|
||||
- 雷达图(选中路径)
|
||||
- 同终点组列表
|
||||
- 组标题(终点位置)
|
||||
- 组内路径(最佳路径高亮)
|
||||
|
||||
右侧面板(300px):
|
||||
- 最佳路径卡片
|
||||
- 优化建议列表(分类)
|
||||
- [导出报告] 按钮
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 布局合理,信息清晰
|
||||
- [ ] 最佳路径视觉突出
|
||||
- [ ] 表格数据完整
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.3: 图表可视化(3小时)
|
||||
|
||||
**实现方式**: 使用 WPF 原生图表或简单自定义控件
|
||||
|
||||
**图表类型**:
|
||||
1. **雷达图**: 展示5维度分数
|
||||
- 使用 WPF Polygon 实现
|
||||
- 或集成简单图表库
|
||||
|
||||
2. **柱状图**: 组内路径对比
|
||||
- 使用 WPF Rectangle 实现
|
||||
- 颜色区分不同路径
|
||||
|
||||
**文件**: 可在 `PathAnalysisDialog.xaml` 中直接实现,或新建 `PathAnalysisCharts.xaml`
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 雷达图正确显示5维度
|
||||
- [ ] 柱状图对比清晰
|
||||
- [ ] 图表随选择更新
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: HTML报告(Day 3-4)
|
||||
|
||||
### 任务 3.1: 报告生成器(4小时)
|
||||
|
||||
**文件**: `src/Core/PathAnalysisReportGenerator.cs` (新建)
|
||||
|
||||
**核心方法**:
|
||||
```csharp
|
||||
public class PathAnalysisReportGenerator
|
||||
{
|
||||
// 生成完整报告
|
||||
public string GenerateHtmlReport(PathAnalysisReportData data);
|
||||
|
||||
// 生成雷达图数据(纯CSS实现)
|
||||
private string GenerateRadarChartCss(PathDetailedAnalysis analysis);
|
||||
|
||||
// 生成柱状图数据(纯CSS实现)
|
||||
private string GenerateBarChartCss(EndpointGroup group);
|
||||
|
||||
// 生成表格HTML
|
||||
private string GenerateComparisonTable(List<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: 数据模型**
|
||||
@ -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
|
||||
@ -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}米");
|
||||
|
||||
|
||||
650
src/Core/Models/PathAnalysisModels.cs
Normal file
650
src/Core/Models/PathAnalysisModels.cs
Normal 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-5,5最高)
|
||||
/// </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;
|
||||
}
|
||||
}
|
||||
811
src/Core/PathAnalysisEngine.cs
Normal file
811
src/Core/PathAnalysisEngine.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
574
src/Core/PathAnalysisReportGenerator.cs
Normal file
574
src/Core/PathAnalysisReportGenerator.cs
Normal 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("&", "&")
|
||||
.Replace("<", "<")
|
||||
.Replace(">", ">")
|
||||
.Replace("\"", """)
|
||||
.Replace("'", "'");
|
||||
}
|
||||
|
||||
#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; }
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 计算
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}");
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
System.IO.File.WriteAllText(filePath, report.ToString(), System.Text.Encoding.UTF8);
|
||||
|
||||
AnalysisStatus = $"报告已导出到:{filePath}";
|
||||
LogManager.Info($"分析报告已导出:{filePath}");
|
||||
|
||||
// 打开文件所在目录并选中文件
|
||||
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{filePath}\"");
|
||||
RecommendedPath = bestItem;
|
||||
|
||||
// 更新雷达图数据
|
||||
UpdateDimensionScores(overallBest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
LogManager.Error($"导出报告失败:{ex.Message}", ex);
|
||||
AnalysisStatus = "导出报告失败";
|
||||
LogManager.Warning("[路径分析UI] 未找到 overallBest");
|
||||
}
|
||||
|
||||
// 默认选中第一个组
|
||||
if (EndpointGroups.Count > 0)
|
||||
{
|
||||
SelectedGroup = EndpointGroups[0];
|
||||
LogManager.Info($"[路径分析UI] 选中第一个组: {SelectedGroup.GroupName}");
|
||||
}
|
||||
|
||||
LogManager.Info($"[路径分析UI] 更新完成: EndpointGroups={EndpointGroups.Count}, Suggestions={CategorizedSuggestions.Count}");
|
||||
}
|
||||
|
||||
private void UpdateDimensionScores(PathDetailedAnalysis analysis)
|
||||
{
|
||||
DimensionScores.Clear();
|
||||
DimensionScores.Add(new DimensionScoreViewModel { Name = "安全性", Score = analysis.SafetyScore, MaxScore = 100 });
|
||||
DimensionScores.Add(new DimensionScoreViewModel { Name = "效率", Score = analysis.EfficiencyScore, MaxScore = 100 });
|
||||
DimensionScores.Add(new DimensionScoreViewModel { Name = "转弯难度", Score = analysis.TurnDifficultyScore, MaxScore = 100 });
|
||||
DimensionScores.Add(new DimensionScoreViewModel { Name = "直达性", Score = analysis.TortuosityScore, MaxScore = 100 });
|
||||
}
|
||||
|
||||
private void SelectGroup(EndpointGroupViewModel group)
|
||||
{
|
||||
if (group != null)
|
||||
{
|
||||
SelectedGroup = group;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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,41 +672,21 @@ 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; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,287 +1,338 @@
|
||||
<!--
|
||||
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 Grid.Column="0">
|
||||
<!-- 路径选择卡片 -->
|
||||
<Border Style="{StaticResource CardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="📋 路径选择" FontSize="14" FontWeight="Bold" Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,10"/>
|
||||
|
||||
<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>
|
||||
<TextBlock Text="📋 路径选择" Style="{StaticResource SectionTitleStyle}"/>
|
||||
<!-- 最佳路径卡片 -->
|
||||
<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>
|
||||
|
||||
<!-- 路径列表 -->
|
||||
<ScrollViewer Height="200" VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl ItemsSource="{Binding AvailablePaths}">
|
||||
<!-- 同终点组列表 -->
|
||||
<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>
|
||||
</Window>
|
||||
</Window>
|
||||
|
||||
@ -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
|
||||
@ -66,17 +102,21 @@ namespace NavisworksTransport.UI.WPF.Views
|
||||
LogManager.Error($"关闭PathAnalysisDialog时发生错误: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user