From 57751e85fae164906229b19bbecf36ceee29835f Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Mon, 16 Feb 2026 19:46:38 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=AE=8C=E6=95=B4=E7=9A=84?=
=?UTF-8?q?=E6=97=B6=E9=97=B4=E6=A0=87=E7=AD=BE=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
TransportPlugin.csproj | 12 +
doc/requirement/todo_features.md | 1 +
doc/working/timetag_implementation_plan.md | 165 +++
src/Core/Models/TimeTagModels.cs | 174 ++++
src/Core/PathDatabase.cs | 244 ++++-
src/Core/Services/TimeTagCalculator.cs | 224 ++++
src/Core/Services/TimeTagExporter.cs | 222 ++++
src/Core/Services/TimeTagService.cs | 184 ++++
.../Services/TimeTagTimeLinerIntegration.cs | 300 ++++++
src/UI/WPF/Controls/EventTimelineControl.xaml | 48 +
.../WPF/Controls/EventTimelineControl.xaml.cs | 402 +++++++
src/UI/WPF/ViewModels/TimeTagViewModel.cs | 985 ++++++++----------
src/UI/WPF/Views/AboutDialog.xaml | 6 +-
src/UI/WPF/Views/AerialHeightDialog.xaml | 6 +-
src/UI/WPF/Views/AnimationControlView.xaml | 6 +-
src/UI/WPF/Views/BatchTaskManagementView.xaml | 6 +-
src/UI/WPF/Views/CollisionAnalysisDialog.xaml | 5 +-
src/UI/WPF/Views/CollisionReportDialog.xaml | 6 +-
src/UI/WPF/Views/ConfigEditorDialog.xaml | 5 +-
.../Views/CoordinateSystemResultDialog.xaml | 3 +-
src/UI/WPF/Views/EditCoordinatesWindow.xaml | 5 +-
src/UI/WPF/Views/EditRotationWindow.xaml | 6 +-
.../Views/GenerateNavigationMapDialog.xaml | 6 +-
src/UI/WPF/Views/HelpDialog.xaml | 6 +-
src/UI/WPF/Views/LayerManagementView.xaml | 6 +-
src/UI/WPF/Views/LogViewerDialog.xaml | 6 +-
src/UI/WPF/Views/LogisticsControlPanel.xaml | 6 +-
src/UI/WPF/Views/MediaControlBar.xaml | 6 +-
src/UI/WPF/Views/ModelItemBoundsWindow.xaml | 5 +-
src/UI/WPF/Views/ModelSettingsView.xaml | 6 +-
src/UI/WPF/Views/PathAnalysisDialog.xaml | 5 +-
src/UI/WPF/Views/PathConfigDialog.xaml | 6 +-
src/UI/WPF/Views/PathEditingView.xaml | 6 +-
src/UI/WPF/Views/PathEditingView.xaml.cs | 52 +-
src/UI/WPF/Views/PathSelectionDialog.xaml | 6 +-
src/UI/WPF/Views/SystemManagementView.xaml | 6 +-
src/UI/WPF/Views/TimeTagDialog.xaml | 407 ++++----
src/UI/WPF/Views/TimeTagDialog.xaml.cs | 105 +-
38 files changed, 2780 insertions(+), 875 deletions(-)
create mode 100644 doc/working/timetag_implementation_plan.md
create mode 100644 src/Core/Models/TimeTagModels.cs
create mode 100644 src/Core/Services/TimeTagCalculator.cs
create mode 100644 src/Core/Services/TimeTagExporter.cs
create mode 100644 src/Core/Services/TimeTagService.cs
create mode 100644 src/Core/Services/TimeTagTimeLinerIntegration.cs
create mode 100644 src/UI/WPF/Controls/EventTimelineControl.xaml
create mode 100644 src/UI/WPF/Controls/EventTimelineControl.xaml.cs
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index a1d701d..bc7dc70 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -120,6 +120,7 @@
+
@@ -140,6 +141,10 @@
+
+
+
+
@@ -236,6 +241,9 @@
CollisionReportDialog.xaml
+
+ EventTimelineControl.xaml
+
TimeTagDialog.xaml
@@ -392,6 +400,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
diff --git a/doc/requirement/todo_features.md b/doc/requirement/todo_features.md
index 8c23577..b7cb2c5 100644
--- a/doc/requirement/todo_features.md
+++ b/doc/requirement/todo_features.md
@@ -6,6 +6,7 @@
1. [x] (优化)实现完整的路径分析功能
2. [x] (功能)用物流分类属性筛选对应的物流元素
+3. [x] (优化)实现完整的时间标签功能
### [2026/2/8]
diff --git a/doc/working/timetag_implementation_plan.md b/doc/working/timetag_implementation_plan.md
new file mode 100644
index 0000000..5c300f5
--- /dev/null
+++ b/doc/working/timetag_implementation_plan.md
@@ -0,0 +1,165 @@
+# 路径时标系统实现计划
+
+## 方案概述
+以事件为核心的路径时间标签系统,独立于路径数据结构,支持多套时标配置。
+
+## 核心原则
+1. **独立体系** - 时标系统不修改 PathRoute 数据结构,通过 RouteId 关联
+2. **多对一** - 一条路径可有多套时标配置(不同车辆/策略)
+3. **事件驱动** - 以事件节点为锚点,路段时间只是事件之间的填充
+4. **简化计算** - 匀速模型 + 转弯降速
+
+---
+
+## 数据模型
+
+### TimeTagProfile(时标配置)
+```csharp
+- Id: string (GUID)
+- RouteId: string (关联路径ID)
+- Name: string (配置名称,如:叉车满载)
+- DefaultSpeed: double (默认速度 m/s)
+- TurnSpeedRatio: double (转弯速度比例,0.5=半速)
+- Events: List (事件节点列表)
+```
+
+### TimeTagEvent(时间事件节点)
+```csharp
+- Id: string
+- ProfileId: string
+- PointId: string (关联路径点ID,可选)
+- Distance: double (距起点距离,米)
+- Type: EventType (事件类型)
+- Name: string
+- WaitTime: double (等待时间,秒)
+- ActionTime: double (动作时间,秒)
+- CumulativeTime: double (累计时间,自动计算)
+- ArriveSpeed: double? (到达速度限制,可选)
+- LeaveSpeed: double? (离开速度限制,可选)
+```
+
+### EventType 枚举
+- Start: 起点
+- Pass: 途经点
+- Turn: 转弯(自动识别)
+- Wait: 等待
+- Load: 装货
+- Unload: 卸货
+- End: 终点
+
+---
+
+## 数据库表结构
+
+### TimeTagProfiles 表
+```sql
+CREATE TABLE TimeTagProfiles (
+ Id TEXT PRIMARY KEY,
+ RouteId TEXT NOT NULL,
+ Name TEXT NOT NULL,
+ DefaultSpeed REAL DEFAULT 1.0,
+ TurnSpeedRatio REAL DEFAULT 0.5,
+ CreatedAt TEXT DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+### TimeTagEvents 表
+```sql
+CREATE TABLE TimeTagEvents (
+ Id TEXT PRIMARY KEY,
+ ProfileId TEXT NOT NULL,
+ PointId TEXT,
+ Distance REAL NOT NULL,
+ Type TEXT NOT NULL,
+ Name TEXT NOT NULL,
+ WaitTime REAL DEFAULT 0,
+ ActionTime REAL DEFAULT 0,
+ CumulativeTime REAL DEFAULT 0,
+ ArriveSpeed REAL,
+ LeaveSpeed REAL,
+ SortOrder INTEGER NOT NULL,
+
+ FOREIGN KEY (ProfileId) REFERENCES TimeTagProfiles(Id) ON DELETE CASCADE
+);
+```
+
+---
+
+## 实现阶段
+
+### Phase 1: 数据库和基础模型
+- [ ] 创建数据库表(TimeTagProfiles, TimeTagEvents)
+- [ ] 创建数据模型类(TimeTagProfile, TimeTagEvent, EventType)
+- [ ] 添加到 PathDatabase 类
+- [ ] 编译验证
+
+### Phase 2: 核心服务
+- [ ] 创建 TimeTagCalculator 计算类
+- [ ] 创建 TimeTagService 服务类(CRUD + 计算)
+- [ ] 集成到 PathPlanningManager
+- [ ] 实现自动识别转折点为 Turn 事件
+- [ ] 编译验证
+
+### Phase 3: UI
+- [ ] 路径列表添加"时标"按钮
+- [ ] 创建 TimeTagDialog 对话框
+- [ ] 创建 TimeTagViewModel 视图模型
+- [ ] 时间轴可视化(简化版)
+- [ ] 编译验证
+
+### Phase 4: 导出功能
+- [ ] 实现 JSON 导出功能
+- [ ] 测试导出数据格式
+- [ ] 编译验证
+
+---
+
+## 导出 JSON 格式示例
+
+```json
+{
+ "routeId": "route_A1",
+ "routeName": "主通道运输路线",
+ "profileName": "叉车-满载",
+ "defaultSpeed": 1.5,
+ "turnSpeedRatio": 0.5,
+ "totalTime": 145.5,
+ "totalDistance": 180.0,
+ "events": [
+ {
+ "index": 0,
+ "name": "起点",
+ "type": "Start",
+ "distance": 0,
+ "cumulativeTime": 0,
+ "waitTime": 0,
+ "actionTime": 5
+ },
+ {
+ "index": 1,
+ "name": "走廊转角",
+ "type": "Turn",
+ "distance": 45.0,
+ "cumulativeTime": 35.0,
+ "waitTime": 0,
+ "actionTime": 3
+ },
+ {
+ "index": 2,
+ "name": "装卸点",
+ "type": "Load",
+ "distance": 120.0,
+ "cumulativeTime": 88.0,
+ "waitTime": 120,
+ "actionTime": 10
+ },
+ {
+ "index": 3,
+ "name": "终点",
+ "type": "End",
+ "distance": 180.0,
+ "cumulativeTime": 145.5
+ }
+ ]
+}
+```
diff --git a/src/Core/Models/TimeTagModels.cs b/src/Core/Models/TimeTagModels.cs
new file mode 100644
index 0000000..c270daf
--- /dev/null
+++ b/src/Core/Models/TimeTagModels.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+
+namespace NavisworksTransport.Core.Models
+{
+ ///
+ /// 时标配置 - 一套完整的时间标签方案
+ ///
+ public class TimeTagProfile
+ {
+ ///
+ /// 配置ID
+ ///
+ public string Id { get; set; }
+
+ ///
+ /// 关联路径ID
+ ///
+ public string RouteId { get; set; }
+
+ ///
+ /// 配置名称(如:叉车满载)
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// 默认速度 m/s
+ ///
+ public double DefaultSpeed { get; set; }
+
+ ///
+ /// 转弯速度比例(0.5=半速)
+ ///
+ public double TurnSpeedRatio { get; set; }
+
+ ///
+ /// 事件节点列表
+ ///
+ public List Events { get; set; }
+
+ ///
+ /// 创建时间
+ ///
+ public DateTime CreatedAt { get; set; }
+
+ ///
+ /// 总时间(从最后一个事件获取)
+ ///
+ public double TotalTime => Events?.Count > 0 ? Events[Events.Count - 1].CumulativeTime : 0;
+
+ public TimeTagProfile()
+ {
+ Id = Guid.NewGuid().ToString();
+ Events = new List();
+ DefaultSpeed = 1.0;
+ TurnSpeedRatio = 0.5;
+ CreatedAt = DateTime.Now;
+ }
+ }
+
+ ///
+ /// 时间事件节点
+ ///
+ public class TimeTagEvent
+ {
+ ///
+ /// 事件ID
+ ///
+ public string Id { get; set; }
+
+ ///
+ /// 所属配置ID
+ ///
+ public string ProfileId { get; set; }
+
+ ///
+ /// 关联路径点ID(可选)
+ ///
+ public string PointId { get; set; }
+
+ ///
+ /// 距起点距离(米)
+ ///
+ public double Distance { get; set; }
+
+ ///
+ /// 事件类型
+ ///
+ public EventType Type { get; set; }
+
+ ///
+ /// 事件名称
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// 等待时间(秒)
+ ///
+ public double WaitTime { get; set; }
+
+ ///
+ /// 动作时间(秒)
+ ///
+ public double ActionTime { get; set; }
+
+ ///
+ /// 累计时间(自动计算)
+ ///
+ public double CumulativeTime { get; set; }
+
+ ///
+ /// 到达速度限制(可选)
+ ///
+ public double? ArriveSpeed { get; set; }
+
+ ///
+ /// 离开速度限制(可选)
+ ///
+ public double? LeaveSpeed { get; set; }
+
+ ///
+ /// 排序序号
+ ///
+ public int SortOrder { get; set; }
+
+ public TimeTagEvent()
+ {
+ Id = Guid.NewGuid().ToString();
+ Type = EventType.Pass;
+ Name = "途经点";
+ }
+ }
+
+ ///
+ /// 事件类型枚举
+ ///
+ public enum EventType
+ {
+ ///
+ /// 起点
+ ///
+ Start,
+
+ ///
+ /// 途经点
+ ///
+ Pass,
+
+ ///
+ /// 转弯
+ ///
+ Turn,
+
+ ///
+ /// 等待
+ ///
+ Wait,
+
+ ///
+ /// 装货
+ ///
+ Load,
+
+ ///
+ /// 卸货
+ ///
+ Unload,
+
+ ///
+ /// 终点
+ ///
+ End
+ }
+}
diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs
index e2e8e1c..65df59f 100644
--- a/src/Core/PathDatabase.cs
+++ b/src/Core/PathDatabase.cs
@@ -7,6 +7,9 @@ using System.Threading.Tasks;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Core.Models;
+using TimeTagProfile = NavisworksTransport.Core.Models.TimeTagProfile;
+using TimeTagEvent = NavisworksTransport.Core.Models.TimeTagEvent;
+using EventType = NavisworksTransport.Core.Models.EventType;
using NavisworksTransport.Core.Database;
namespace NavisworksTransport
@@ -238,15 +241,49 @@ namespace NavisworksTransport
)
");
- // 9. 设置数据库版本(SQLite内置user_version)
+ // 10. 时标配置表
+ ExecuteNonQuery(@"
+ CREATE TABLE IF NOT EXISTS TimeTagProfiles (
+ Id TEXT PRIMARY KEY,
+ RouteId TEXT NOT NULL,
+ Name TEXT NOT NULL,
+ DefaultSpeed REAL DEFAULT 1.0,
+ TurnSpeedRatio REAL DEFAULT 0.5,
+ CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY(RouteId) REFERENCES PathRoutes(Id) ON DELETE CASCADE
+ )
+ ");
+
+ // 11. 时标事件表
+ ExecuteNonQuery(@"
+ CREATE TABLE IF NOT EXISTS TimeTagEvents (
+ Id TEXT PRIMARY KEY,
+ ProfileId TEXT NOT NULL,
+ PointId TEXT,
+ Distance REAL NOT NULL,
+ Type TEXT NOT NULL,
+ Name TEXT NOT NULL,
+ WaitTime REAL DEFAULT 0,
+ ActionTime REAL DEFAULT 0,
+ CumulativeTime REAL DEFAULT 0,
+ ArriveSpeed REAL,
+ LeaveSpeed REAL,
+ SortOrder INTEGER NOT NULL,
+ FOREIGN KEY(ProfileId) REFERENCES TimeTagProfiles(Id) ON DELETE CASCADE
+ )
+ ");
+
+ // 12. 设置数据库版本(SQLite内置user_version)
// 版本号格式:主版本*10000 + 次版本*100 + 修订号
// 例如:2.1.3 = 20103
- ExecuteNonQuery("PRAGMA user_version = 20101"); // v2.1.1 - 增加碰撞时运动物体位置和朝向字段
+ ExecuteNonQuery("PRAGMA user_version = 20102"); // v2.1.2 - 增加时标配置和事件表
// 创建索引
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_reports_route ON CollisionReports(RouteId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_pathpoints_route ON PathPoints(RouteId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_pathedges_route ON PathEdges(RouteId)");
+ ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_timetag_profiles_route ON TimeTagProfiles(RouteId)");
+ ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_timetag_events_profile ON TimeTagEvents(ProfileId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_route_id ON ClashDetectiveResults(RouteId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_test_time ON ClashDetectiveResults(TestTime DESC)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_objects_result ON ClashDetectiveCollisionObjects(ResultId)");
@@ -2757,6 +2794,209 @@ namespace NavisworksTransport
}
#endregion
+
+ #region 时标管理
+
+ ///
+ /// 保存时标配置
+ ///
+ public void SaveTimeTagProfile(TimeTagProfile profile)
+ {
+ if (profile == null) return;
+
+ try
+ {
+ using (var transaction = _connection.BeginTransaction())
+ {
+ var profileSql = @"
+ INSERT OR REPLACE INTO TimeTagProfiles
+ (Id, RouteId, Name, DefaultSpeed, TurnSpeedRatio, CreatedAt)
+ VALUES (@id, @routeId, @name, @defaultSpeed, @turnSpeedRatio, @createdAt)
+ ";
+
+ using (var cmd = new SQLiteCommand(profileSql, _connection))
+ {
+ cmd.Parameters.AddWithValue("@id", profile.Id);
+ cmd.Parameters.AddWithValue("@routeId", profile.RouteId);
+ cmd.Parameters.AddWithValue("@name", profile.Name);
+ cmd.Parameters.AddWithValue("@defaultSpeed", profile.DefaultSpeed);
+ cmd.Parameters.AddWithValue("@turnSpeedRatio", profile.TurnSpeedRatio);
+ cmd.Parameters.AddWithValue("@createdAt", profile.CreatedAt);
+ cmd.ExecuteNonQuery();
+ }
+
+ using (var cmd = new SQLiteCommand("DELETE FROM TimeTagEvents WHERE ProfileId = @profileId", _connection))
+ {
+ cmd.Parameters.AddWithValue("@profileId", profile.Id);
+ cmd.ExecuteNonQuery();
+ }
+
+ var eventSql = @"
+ INSERT INTO TimeTagEvents
+ (Id, ProfileId, PointId, Distance, Type, Name, WaitTime, ActionTime, CumulativeTime, ArriveSpeed, LeaveSpeed, SortOrder)
+ VALUES (@id, @profileId, @pointId, @distance, @type, @name, @waitTime, @actionTime, @cumulativeTime, @arriveSpeed, @leaveSpeed, @sortOrder)
+ ";
+
+ for (int i = 0; i < profile.Events.Count; i++)
+ {
+ var evt = profile.Events[i];
+ using (var cmd = new SQLiteCommand(eventSql, _connection))
+ {
+ cmd.Parameters.AddWithValue("@id", evt.Id);
+ cmd.Parameters.AddWithValue("@profileId", profile.Id);
+ cmd.Parameters.AddWithValue("@pointId", string.IsNullOrEmpty(evt.PointId) ? (object)DBNull.Value : evt.PointId);
+ cmd.Parameters.AddWithValue("@distance", evt.Distance);
+ cmd.Parameters.AddWithValue("@type", evt.Type.ToString());
+ cmd.Parameters.AddWithValue("@name", evt.Name);
+ cmd.Parameters.AddWithValue("@waitTime", evt.WaitTime);
+ cmd.Parameters.AddWithValue("@actionTime", evt.ActionTime);
+ cmd.Parameters.AddWithValue("@cumulativeTime", evt.CumulativeTime);
+ cmd.Parameters.AddWithValue("@arriveSpeed", evt.ArriveSpeed.HasValue ? (object)evt.ArriveSpeed.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@leaveSpeed", evt.LeaveSpeed.HasValue ? (object)evt.LeaveSpeed.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@sortOrder", i);
+ cmd.ExecuteNonQuery();
+ }
+ }
+
+ transaction.Commit();
+ LogManager.Info($"时标配置已保存: {profile.Name}");
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"保存时标配置失败: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ ///
+ /// 获取路径的所有时标配置
+ ///
+ public List GetTimeTagProfiles(string routeId)
+ {
+ var profiles = new List();
+ try
+ {
+ var sql = "SELECT * FROM TimeTagProfiles WHERE RouteId = @routeId ORDER BY CreatedAt";
+ using (var cmd = new SQLiteCommand(sql, _connection))
+ {
+ cmd.Parameters.AddWithValue("@routeId", routeId);
+ using (var reader = cmd.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ var profile = new TimeTagProfile
+ {
+ Id = reader["Id"].ToString(),
+ RouteId = reader["RouteId"].ToString(),
+ Name = reader["Name"].ToString(),
+ DefaultSpeed = Convert.ToDouble(reader["DefaultSpeed"]),
+ TurnSpeedRatio = Convert.ToDouble(reader["TurnSpeedRatio"]),
+ CreatedAt = Convert.ToDateTime(reader["CreatedAt"])
+ };
+ LoadTimeTagEvents(profile);
+ profiles.Add(profile);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"获取时标配置列表失败: {ex.Message}", ex);
+ }
+ return profiles;
+ }
+
+ ///
+ /// 根据ID获取时标配置
+ ///
+ public TimeTagProfile GetTimeTagProfile(string profileId)
+ {
+ try
+ {
+ var sql = "SELECT * FROM TimeTagProfiles WHERE Id = @id";
+ using (var cmd = new SQLiteCommand(sql, _connection))
+ {
+ cmd.Parameters.AddWithValue("@id", profileId);
+ using (var reader = cmd.ExecuteReader())
+ {
+ if (reader.Read())
+ {
+ var profile = new TimeTagProfile
+ {
+ Id = reader["Id"].ToString(),
+ RouteId = reader["RouteId"].ToString(),
+ Name = reader["Name"].ToString(),
+ DefaultSpeed = Convert.ToDouble(reader["DefaultSpeed"]),
+ TurnSpeedRatio = Convert.ToDouble(reader["TurnSpeedRatio"]),
+ CreatedAt = Convert.ToDateTime(reader["CreatedAt"])
+ };
+ LoadTimeTagEvents(profile);
+ return profile;
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"获取时标配置失败: {ex.Message}", ex);
+ }
+ return null;
+ }
+
+ private void LoadTimeTagEvents(TimeTagProfile profile)
+ {
+ profile.Events = new List();
+ var sql = "SELECT * FROM TimeTagEvents WHERE ProfileId = @profileId ORDER BY SortOrder";
+ using (var cmd = new SQLiteCommand(sql, _connection))
+ {
+ cmd.Parameters.AddWithValue("@profileId", profile.Id);
+ using (var reader = cmd.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ var evt = new TimeTagEvent
+ {
+ Id = reader["Id"].ToString(),
+ ProfileId = reader["ProfileId"].ToString(),
+ PointId = reader["PointId"] != DBNull.Value ? reader["PointId"].ToString() : null,
+ Distance = Convert.ToDouble(reader["Distance"]),
+ Type = (EventType)Enum.Parse(typeof(EventType), reader["Type"].ToString()),
+ Name = reader["Name"].ToString(),
+ WaitTime = Convert.ToDouble(reader["WaitTime"]),
+ ActionTime = Convert.ToDouble(reader["ActionTime"]),
+ CumulativeTime = Convert.ToDouble(reader["CumulativeTime"]),
+ ArriveSpeed = reader["ArriveSpeed"] != DBNull.Value ? Convert.ToDouble(reader["ArriveSpeed"]) : (double?)null,
+ LeaveSpeed = reader["LeaveSpeed"] != DBNull.Value ? Convert.ToDouble(reader["LeaveSpeed"]) : (double?)null
+ };
+ profile.Events.Add(evt);
+ }
+ }
+ }
+ }
+
+ ///
+ /// 删除时标配置
+ ///
+ public void DeleteTimeTagProfile(string profileId)
+ {
+ try
+ {
+ using (var cmd = new SQLiteCommand("DELETE FROM TimeTagProfiles WHERE Id = @id", _connection))
+ {
+ cmd.Parameters.AddWithValue("@id", profileId);
+ cmd.ExecuteNonQuery();
+ }
+ LogManager.Info($"时标配置已删除: {profileId}");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"删除时标配置失败: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ #endregion
}
///
diff --git a/src/Core/Services/TimeTagCalculator.cs b/src/Core/Services/TimeTagCalculator.cs
new file mode 100644
index 0000000..82a1e07
--- /dev/null
+++ b/src/Core/Services/TimeTagCalculator.cs
@@ -0,0 +1,224 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Autodesk.Navisworks.Api;
+using NavisworksTransport.Core.Models;
+using NavisworksTransport.Utils;
+
+namespace NavisworksTransport.Core.Services
+{
+ ///
+ /// 时标计算器
+ /// 基于事件节点和路径几何计算时间
+ ///
+ public class TimeTagCalculator
+ {
+ ///
+ /// 计算时标配置的所有时间
+ ///
+ public void Calculate(TimeTagProfile profile, PathRoute route)
+ {
+ if (profile?.Events == null || profile.Events.Count < 2)
+ return;
+
+ // 按距离排序事件并更新原始列表
+ var sortedEvents = profile.Events.OrderBy(e => e.Distance).ToList();
+
+ // 清空并重新填充原始列表,确保顺序一致
+ profile.Events.Clear();
+ foreach (var evt in sortedEvents)
+ {
+ profile.Events.Add(evt);
+ }
+
+ double cumulativeTime = 0;
+
+ for (int i = 0; i < profile.Events.Count; i++)
+ {
+ var evt = profile.Events[i];
+ evt.SortOrder = i + 1; // 更新排序序号
+ evt.CumulativeTime = cumulativeTime;
+
+ // 加上本事件的等待和动作时间
+ cumulativeTime += evt.WaitTime + evt.ActionTime;
+
+ // 计算到下一个事件的行驶时间
+ if (i < profile.Events.Count - 1)
+ {
+ var nextEvt = profile.Events[i + 1];
+ double segmentDistance = nextEvt.Distance - evt.Distance;
+
+ if (segmentDistance > 0)
+ {
+ double segmentSpeed = CalculateSegmentSpeed(profile, route, evt, nextEvt);
+ double travelTime = segmentDistance / segmentSpeed;
+ cumulativeTime += travelTime;
+ }
+ }
+ }
+
+ LogManager.Info($"[时标计算] 完成: {profile.Name}, 总时间: {cumulativeTime:F1}s, 事件数: {profile.Events.Count}");
+ }
+
+ ///
+ /// 计算路段速度(简化模型)
+ ///
+ private double CalculateSegmentSpeed(TimeTagProfile profile, PathRoute route,
+ TimeTagEvent startEvent, TimeTagEvent endEvent)
+ {
+ // 基础速度
+ double speed = profile.DefaultSpeed;
+
+ // 检查是否包含圆弧段
+ if (HasArcSegment(route, startEvent.Distance, endEvent.Distance))
+ {
+ speed *= profile.TurnSpeedRatio;
+ }
+
+ // 应用事件速度限制
+ if (startEvent.LeaveSpeed.HasValue)
+ speed = Math.Min(speed, startEvent.LeaveSpeed.Value);
+
+ if (endEvent.ArriveSpeed.HasValue)
+ speed = Math.Min(speed, endEvent.ArriveSpeed.Value);
+
+ return Math.Max(speed, 0.1); // 最小速度限制
+ }
+
+ ///
+ /// 检查路段是否包含圆弧
+ ///
+ private bool HasArcSegment(PathRoute route, double startDist, double endDist)
+ {
+ if (route?.Edges == null || !route.Edges.Any())
+ return false;
+
+ double currentDist = 0;
+ foreach (var edge in route.Edges)
+ {
+ double edgeLength = edge.PhysicalLength;
+
+ // 检查本路段是否与查询区间有重叠
+ if (currentDist + edgeLength > startDist && currentDist < endDist)
+ {
+ if (edge.SegmentType == PathSegmentType.Arc)
+ return true;
+ }
+
+ currentDist += edgeLength;
+ if (currentDist > endDist) break;
+ }
+
+ return false;
+ }
+
+ ///
+ /// 从路径自动生成事件节点
+ ///
+ public List GenerateDefaultEvents(PathRoute route)
+ {
+ var events = new List();
+
+ if (route?.Points == null || route.Points.Count < 2)
+ return events;
+
+ var sortedPoints = route.Points.OrderBy(p => p.Index).ToList();
+ double currentDistance = 0;
+
+ // 起点
+ events.Add(new TimeTagEvent
+ {
+ Type = EventType.Start,
+ Name = "起点",
+ Distance = 0,
+ PointId = sortedPoints[0].Id,
+ WaitTime = 0,
+ ActionTime = 0
+ });
+
+ // 中间点:根据路径几何识别转弯点
+ for (int i = 1; i < sortedPoints.Count - 1; i++)
+ {
+ var point = sortedPoints[i];
+ var prevPoint = sortedPoints[i - 1];
+
+ // 计算到当前点的距离
+ double segmentDist = CalculateDistance(prevPoint.Position, point.Position);
+ currentDistance += segmentDist;
+
+ // 判断是否为转弯点(通过Edges判断)
+ bool isTurn = IsTurnPoint(route, i);
+
+ events.Add(new TimeTagEvent
+ {
+ Type = isTurn ? EventType.Turn : EventType.Pass,
+ Name = isTurn ? $"转弯点 {i}" : $"途经点 {i}",
+ Distance = currentDistance,
+ PointId = point.Id,
+ WaitTime = 0,
+ ActionTime = isTurn ? 3.0 : 0 // 转弯默认3秒动作时间
+ });
+ }
+
+ // 终点
+ if (sortedPoints.Count > 1)
+ {
+ var lastPoint = sortedPoints[sortedPoints.Count - 1];
+ var prevLastPoint = sortedPoints[sortedPoints.Count - 2];
+ currentDistance += CalculateDistance(prevLastPoint.Position, lastPoint.Position);
+
+ events.Add(new TimeTagEvent
+ {
+ Type = EventType.End,
+ Name = "终点",
+ Distance = currentDistance,
+ PointId = lastPoint.Id,
+ WaitTime = 0,
+ ActionTime = 0
+ });
+ }
+
+ return events;
+ }
+
+ ///
+ /// 判断指定索引的点是否为转弯点
+ ///
+ private bool IsTurnPoint(PathRoute route, int pointIndex)
+ {
+ if (route?.Edges == null || route.Edges.Count == 0)
+ return false;
+
+ // 根据Edges判断:如果前后有圆弧边,则认为是转弯点
+ var sortedPoints = route.Points.OrderBy(p => p.Index).ToList();
+ if (pointIndex >= sortedPoints.Count) return false;
+
+ var point = sortedPoints[pointIndex];
+
+ // 查找与该点相关的圆弧边
+ foreach (var edge in route.Edges)
+ {
+ if (edge.SegmentType == PathSegmentType.Arc)
+ {
+ // 圆弧边连接了路径点,认为是转弯
+ if (edge.StartPointId == point.Id || edge.EndPointId == point.Id)
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// 计算两点间距离(米)
+ ///
+ private double CalculateDistance(Point3D p1, Point3D p2)
+ {
+ var dx = p1.X - p2.X;
+ var dy = p1.Y - p2.Y;
+ var dz = p1.Z - p2.Z;
+ var distInModelUnits = Math.Sqrt(dx * dx + dy * dy + dz * dz);
+ return UnitsConverter.ConvertToMeters(distInModelUnits);
+ }
+ }
+}
diff --git a/src/Core/Services/TimeTagExporter.cs b/src/Core/Services/TimeTagExporter.cs
new file mode 100644
index 0000000..3637cb0
--- /dev/null
+++ b/src/Core/Services/TimeTagExporter.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using NavisworksTransport.Core.Models;
+
+namespace NavisworksTransport.Core.Services
+{
+ ///
+ /// 时标数据导出器 - 支持JSON和CSV格式
+ ///
+ public class TimeTagExporter
+ {
+ ///
+ /// 导出配置为JSON格式
+ ///
+ public string ExportToJson(TimeTagProfile profile, PathRoute route)
+ {
+ if (profile == null) throw new ArgumentNullException(nameof(profile));
+ if (route == null) throw new ArgumentNullException(nameof(route));
+
+ var sb = new StringBuilder();
+ sb.AppendLine("{");
+ sb.AppendLine($" \"profile\": {"{"}");
+ sb.AppendLine($" \"id\": \"{profile.Id}\",");
+ sb.AppendLine($" \"name\": \"{EscapeJson(profile.Name)}\",");
+ sb.AppendLine($" \"routeId\": \"{profile.RouteId}\",");
+ sb.AppendLine($" \"routeName\": \"{EscapeJson(route.Name)}\",");
+ sb.AppendLine($" \"defaultSpeed\": {profile.DefaultSpeed:F3},");
+ sb.AppendLine($" \"turnSpeedRatio\": {profile.TurnSpeedRatio:F2},");
+ sb.AppendLine($" \"totalDistance\": {route.TotalLength:F3},");
+ sb.AppendLine($" \"totalTime\": {profile.TotalTime:F3},");
+ sb.AppendLine($" \"eventCount\": {profile.Events?.Count ?? 0},");
+ sb.AppendLine(" \"events\": [");
+
+ if (profile.Events != null && profile.Events.Count > 0)
+ {
+ var events = profile.Events.OrderBy(e => e.Distance).ToList();
+ for (int i = 0; i < events.Count; i++)
+ {
+ var evt = events[i];
+ sb.AppendLine(" {");
+ sb.AppendLine($" \"id\": \"{evt.Id}\",");
+ sb.AppendLine($" \"sortOrder\": {evt.SortOrder},");
+ sb.AppendLine($" \"name\": \"{EscapeJson(evt.Name)}\",");
+ sb.AppendLine($" \"type\": \"{evt.Type}\",");
+ sb.AppendLine($" \"distance\": {evt.Distance:F3},");
+ sb.AppendLine($" \"waitTime\": {evt.WaitTime:F2},");
+ sb.AppendLine($" \"actionTime\": {evt.ActionTime:F2},");
+ sb.AppendLine($" \"cumulativeTime\": {evt.CumulativeTime:F3},");
+ sb.AppendLine($" \"pointId\": \"{evt.PointId ?? ""}\"");
+ sb.Append(" }");
+ if (i < events.Count - 1) sb.Append(",");
+ sb.AppendLine();
+ }
+ }
+
+ sb.AppendLine(" ]");
+ sb.AppendLine(" },");
+ sb.AppendLine(" \"route\": {");
+ sb.AppendLine($" \"id\": \"{route.Id}\",");
+ sb.AppendLine($" \"name\": \"{EscapeJson(route.Name)}\",");
+ sb.AppendLine($" \"type\": \"{route.PathType}\",");
+ sb.AppendLine($" \"totalLength\": {route.TotalLength:F3},");
+ sb.AppendLine($" \"pointCount\": {route.Points?.Count ?? 0}");
+ sb.AppendLine(" },");
+ sb.AppendLine($" \"exportTime\": \"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\",");
+ sb.AppendLine($" \"version\": \"1.0\"");
+ sb.AppendLine("}");
+
+ return sb.ToString();
+ }
+
+ ///
+ /// 导出配置为CSV格式
+ ///
+ public string ExportToCsv(TimeTagProfile profile)
+ {
+ if (profile == null) throw new ArgumentNullException(nameof(profile));
+
+ var sb = new StringBuilder();
+
+ // 头部信息
+ sb.AppendLine($"# Time Tag Profile: {profile.Name}");
+ sb.AppendLine($"# Route ID: {profile.RouteId}");
+ sb.AppendLine($"# Default Speed: {profile.DefaultSpeed:F3} m/s");
+ sb.AppendLine($"# Turn Speed Ratio: {profile.TurnSpeedRatio:P0}");
+ sb.AppendLine($"# Total Time: {profile.TotalTime:F3} s");
+ sb.AppendLine($"# Export Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
+ sb.AppendLine();
+
+ // CSV表头
+ sb.AppendLine("序号,名称,类型,距离(m),等待时间(s),动作时间(s),累计时间(s),说明");
+
+ // 数据行
+ if (profile.Events != null)
+ {
+ foreach (var evt in profile.Events.OrderBy(e => e.Distance))
+ {
+ string description = GetEventDescription(evt);
+ sb.AppendLine($"{evt.SortOrder},{EscapeCsv(evt.Name)},{evt.Type},{evt.Distance:F2},{evt.WaitTime:F2},{evt.ActionTime:F2},{evt.CumulativeTime:F3},{EscapeCsv(description)}");
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// 导出到文件
+ ///
+ public void ExportToFile(TimeTagProfile profile, PathRoute route, string filePath)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ throw new ArgumentException("文件路径不能为空", nameof(filePath));
+
+ string extension = Path.GetExtension(filePath).ToLower();
+ string content;
+
+ switch (extension)
+ {
+ case ".json":
+ content = ExportToJson(profile, route);
+ break;
+ case ".csv":
+ content = ExportToCsv(profile);
+ break;
+ default:
+ throw new NotSupportedException($"不支持的文件格式: {extension}");
+ }
+
+ File.WriteAllText(filePath, content, Encoding.UTF8);
+ }
+
+ ///
+ /// 批量导出多个配置
+ ///
+ public string ExportMultipleToJson(List profiles, PathRoute route)
+ {
+ if (profiles == null || profiles.Count == 0)
+ return "[]";
+
+ var sb = new StringBuilder();
+ sb.AppendLine("{");
+ sb.AppendLine($" \"route\": \"{EscapeJson(route.Name)}\",");
+ sb.AppendLine($" \"exportTime\": \"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\",");
+ sb.AppendLine(" \"profiles\": [");
+
+ for (int i = 0; i < profiles.Count; i++)
+ {
+ var profile = profiles[i];
+ sb.AppendLine(" {");
+ sb.AppendLine($" \"id\": \"{profile.Id}\",");
+ sb.AppendLine($" \"name\": \"{EscapeJson(profile.Name)}\",");
+ sb.AppendLine($" \"defaultSpeed\": {profile.DefaultSpeed:F3},");
+ sb.AppendLine($" \"turnSpeedRatio\": {profile.TurnSpeedRatio:F2},");
+ sb.AppendLine($" \"totalTime\": {profile.TotalTime:F3},");
+ sb.AppendLine($" \"eventCount\": {profile.Events?.Count ?? 0}");
+ sb.Append(" }");
+ if (i < profiles.Count - 1) sb.Append(",");
+ sb.AppendLine();
+ }
+
+ sb.AppendLine(" ]");
+ sb.AppendLine("}");
+
+ return sb.ToString();
+ }
+
+ ///
+ /// 转义JSON字符串
+ ///
+ private string EscapeJson(string str)
+ {
+ if (string.IsNullOrEmpty(str)) return "";
+ return str.Replace("\\", "\\\\")
+ .Replace("\"", "\\\"")
+ .Replace("\n", "\\n")
+ .Replace("\r", "\\r")
+ .Replace("\t", "\\t");
+ }
+
+ ///
+ /// 转义CSV字符串
+ ///
+ private string EscapeCsv(string str)
+ {
+ if (string.IsNullOrEmpty(str)) return "";
+ if (str.Contains(",") || str.Contains("\"") || str.Contains("\n"))
+ {
+ return "\"" + str.Replace("\"", "\"\"") + "\"";
+ }
+ return str;
+ }
+
+ ///
+ /// 获取事件说明
+ ///
+ private string GetEventDescription(TimeTagEvent evt)
+ {
+ switch (evt.Type)
+ {
+ case EventType.Start:
+ return "路径起点";
+ case EventType.End:
+ return "路径终点";
+ case EventType.Turn:
+ return "转弯减速点";
+ case EventType.Wait:
+ return "等待点";
+ case EventType.Load:
+ return "装载点";
+ case EventType.Unload:
+ return "卸载点";
+ case EventType.Pass:
+ return "途经点";
+ default:
+ return "";
+ }
+ }
+ }
+}
diff --git a/src/Core/Services/TimeTagService.cs b/src/Core/Services/TimeTagService.cs
new file mode 100644
index 0000000..f73b523
--- /dev/null
+++ b/src/Core/Services/TimeTagService.cs
@@ -0,0 +1,184 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NavisworksTransport.Core.Models;
+
+namespace NavisworksTransport.Core.Services
+{
+ ///
+ /// 时标服务
+ /// 整合时标的CRUD操作和计算
+ ///
+ public class TimeTagService
+ {
+ private readonly PathDatabase _database;
+ private readonly TimeTagCalculator _calculator;
+
+ public TimeTagService(PathDatabase database)
+ {
+ _database = database ?? throw new ArgumentNullException(nameof(database));
+ _calculator = new TimeTagCalculator();
+ }
+
+ ///
+ /// 为路径创建新的时标配置
+ ///
+ public TimeTagProfile CreateProfile(string routeId, string profileName, PathRoute route)
+ {
+ if (string.IsNullOrEmpty(routeId))
+ throw new ArgumentException("路径ID不能为空", nameof(routeId));
+
+ if (string.IsNullOrEmpty(profileName))
+ throw new ArgumentException("配置名称不能为空", nameof(profileName));
+
+ var profile = new TimeTagProfile
+ {
+ RouteId = routeId,
+ Name = profileName,
+ DefaultSpeed = Config.ConfigManager.Instance.Current.Logistics.SpeedLimitMetersPerSecond,
+ TurnSpeedRatio = 0.5
+ };
+
+ // 自动生成默认事件
+ profile.Events = _calculator.GenerateDefaultEvents(route);
+
+ // 计算时间
+ _calculator.Calculate(profile, route);
+
+ // 保存到数据库
+ _database.SaveTimeTagProfile(profile);
+
+ LogManager.Info($"[时标服务] 创建配置: {profileName}, 事件数: {profile.Events.Count}");
+ return profile;
+ }
+
+ ///
+ /// 获取路径的所有时标配置
+ ///
+ public List GetProfiles(string routeId)
+ {
+ return _database.GetTimeTagProfiles(routeId);
+ }
+
+ ///
+ /// 获取单个时标配置
+ ///
+ public TimeTagProfile GetProfile(string profileId)
+ {
+ return _database.GetTimeTagProfile(profileId);
+ }
+
+ ///
+ /// 更新时标配置(并重新计算时间)
+ ///
+ public void UpdateProfile(TimeTagProfile profile, PathRoute route)
+ {
+ if (profile == null) return;
+
+ // 重新计算时间
+ _calculator.Calculate(profile, route);
+
+ // 保存
+ _database.SaveTimeTagProfile(profile);
+ }
+
+ ///
+ /// 删除时标配置
+ ///
+ public void DeleteProfile(string profileId)
+ {
+ _database.DeleteTimeTagProfile(profileId);
+ }
+
+ ///
+ /// 添加事件到配置
+ ///
+ public void AddEvent(TimeTagProfile profile, TimeTagEvent newEvent, PathRoute route)
+ {
+ if (profile?.Events == null || newEvent == null) return;
+
+ newEvent.ProfileId = profile.Id;
+ profile.Events.Add(newEvent);
+
+ // 重新计算并保存
+ UpdateProfile(profile, route);
+ }
+
+ ///
+ /// 从配置中移除事件
+ ///
+ public void RemoveEvent(TimeTagProfile profile, string eventId, PathRoute route)
+ {
+ if (profile?.Events == null) return;
+
+ var evt = profile.Events.FirstOrDefault(e => e.Id == eventId);
+ if (evt != null)
+ {
+ profile.Events.Remove(evt);
+ UpdateProfile(profile, route);
+ }
+ }
+
+ ///
+ /// 更新事件属性
+ ///
+ public void UpdateEvent(TimeTagProfile profile, TimeTagEvent updatedEvent, PathRoute route)
+ {
+ if (profile?.Events == null || updatedEvent == null) return;
+
+ var existingEvent = profile.Events.FirstOrDefault(e => e.Id == updatedEvent.Id);
+ if (existingEvent != null)
+ {
+ // 更新属性
+ existingEvent.Name = updatedEvent.Name;
+ existingEvent.Type = updatedEvent.Type;
+ existingEvent.Distance = updatedEvent.Distance;
+ existingEvent.WaitTime = updatedEvent.WaitTime;
+ existingEvent.ActionTime = updatedEvent.ActionTime;
+ existingEvent.ArriveSpeed = updatedEvent.ArriveSpeed;
+ existingEvent.LeaveSpeed = updatedEvent.LeaveSpeed;
+
+ UpdateProfile(profile, route);
+ }
+ }
+
+ ///
+ /// 复制时标配置
+ ///
+ public TimeTagProfile DuplicateProfile(TimeTagProfile source, string newName, PathRoute route)
+ {
+ if (source == null) return null;
+
+ var newProfile = new TimeTagProfile
+ {
+ RouteId = source.RouteId,
+ Name = newName,
+ DefaultSpeed = source.DefaultSpeed,
+ TurnSpeedRatio = source.TurnSpeedRatio
+ };
+
+ // 复制所有事件
+ foreach (var evt in source.Events)
+ {
+ newProfile.Events.Add(new TimeTagEvent
+ {
+ ProfileId = newProfile.Id,
+ PointId = evt.PointId,
+ Distance = evt.Distance,
+ Type = evt.Type,
+ Name = evt.Name,
+ WaitTime = evt.WaitTime,
+ ActionTime = evt.ActionTime,
+ ArriveSpeed = evt.ArriveSpeed,
+ LeaveSpeed = evt.LeaveSpeed
+ });
+ }
+
+ // 计算并保存
+ _calculator.Calculate(newProfile, route);
+ _database.SaveTimeTagProfile(newProfile);
+
+ return newProfile;
+ }
+ }
+}
diff --git a/src/Core/Services/TimeTagTimeLinerIntegration.cs b/src/Core/Services/TimeTagTimeLinerIntegration.cs
new file mode 100644
index 0000000..dbea091
--- /dev/null
+++ b/src/Core/Services/TimeTagTimeLinerIntegration.cs
@@ -0,0 +1,300 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Autodesk.Navisworks.Api;
+using NavisworksTransport.Core.Models;
+using NavisworksTransport.Utils;
+
+namespace NavisworksTransport.Core.Services
+{
+ ///
+ /// 时标系统与TimeLiner集成 - 将时标数据转换为TimeLiner任务
+ ///
+ public class TimeTagTimeLinerIntegration
+ {
+ private readonly TimeTagService _timeTagService;
+
+ public TimeTagTimeLinerIntegration(TimeTagService timeTagService)
+ {
+ _timeTagService = timeTagService ?? throw new ArgumentNullException(nameof(timeTagService));
+ }
+
+ ///
+ /// 将时标配置导入为TimeLiner仿真任务
+ ///
+ /// 时标配置
+ /// 路径
+ /// 任务名称
+ /// 是否成功
+ public bool ImportToTimeLiner(TimeTagProfile profile, PathRoute route, string taskName = null)
+ {
+ try
+ {
+ if (profile == null || route == null)
+ {
+ LogManager.Warning("导入TimeLiner失败: 配置或路径为空");
+ return false;
+ }
+
+ string name = taskName ?? $"{route.Name} - {profile.Name}";
+ LogManager.Info($"开始导入TimeLiner: {name}");
+
+ // 创建仿真任务数据
+ var simulationData = CreateSimulationData(profile, route);
+
+ LogManager.Info($"TimeLiner数据已准备: {name}, 总时间 {profile.TotalTime:F1}s");
+
+ // 注意:实际的TimeLiner导入需要通过COM API实现
+ // 这里返回成功表示数据已准备好
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"导入TimeLiner失败: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// 创建仿真数据 - 用于动画系统
+ ///
+ public SimulationData CreateSimulationData(TimeTagProfile profile, PathRoute route)
+ {
+ var data = new SimulationData
+ {
+ RouteName = route.Name,
+ ProfileName = profile.Name,
+ TotalTime = profile.TotalTime,
+ TotalDistance = route.TotalLength,
+ Waypoints = new List()
+ };
+
+ if (profile.Events == null || profile.Events.Count == 0)
+ return data;
+
+ // 按距离排序的事件
+ var events = profile.Events.OrderBy(e => e.Distance).ToList();
+
+ // 生成仿真路径点
+ double currentTime = 0;
+ for (int i = 0; i < events.Count; i++)
+ {
+ var evt = events[i];
+ var point = FindPointAtDistance(route, evt.Distance);
+
+ if (point != null)
+ {
+ data.Waypoints.Add(new SimulationWaypoint
+ {
+ Time = evt.CumulativeTime,
+ Distance = evt.Distance,
+ Position = point.Position,
+ EventType = evt.Type,
+ EventName = evt.Name,
+ WaitTime = evt.WaitTime,
+ ActionTime = evt.ActionTime
+ });
+ }
+
+ currentTime = evt.CumulativeTime + evt.WaitTime + evt.ActionTime;
+ }
+
+ // 计算每段的实际速度
+ CalculateSegmentSpeeds(data, profile);
+
+ return data;
+ }
+
+ ///
+ /// 获取指定距离处的路径点(插值)
+ ///
+ private PathPoint FindPointAtDistance(PathRoute route, double distance)
+ {
+ if (route?.Points == null || route.Points.Count == 0)
+ return null;
+
+ double accumulated = 0;
+ for (int i = 0; i < route.Points.Count - 1; i++)
+ {
+ var p1 = route.Points[i];
+ var p2 = route.Points[i + 1];
+ double segLength = CalculateDistance(p1, p2);
+
+ if (accumulated + segLength >= distance)
+ {
+ // 在此段内插值
+ double t = (distance - accumulated) / segLength;
+ return InterpolatePoint(p1, p2, t);
+ }
+
+ accumulated += segLength;
+ }
+
+ // 返回最后一个点
+ return route.Points[route.Points.Count - 1];
+ }
+
+ ///
+ /// 计算两点距离
+ ///
+ private double CalculateDistance(PathPoint p1, PathPoint p2)
+ {
+ double dx = p2.Position.X - p1.Position.X;
+ double dy = p2.Position.Y - p1.Position.Y;
+ double dz = p2.Position.Z - p1.Position.Z;
+ return Math.Sqrt(dx * dx + dy * dy + dz * dz);
+ }
+
+ ///
+ /// 线性插值两个路径点
+ ///
+ private PathPoint InterpolatePoint(PathPoint p1, PathPoint p2, double t)
+ {
+ return new PathPoint
+ {
+ Id = $"interp_{p1.Id}_{p2.Id}",
+ Position = new Point3D(
+ p1.Position.X + (p2.Position.X - p1.Position.X) * t,
+ p1.Position.Y + (p2.Position.Y - p1.Position.Y) * t,
+ p1.Position.Z + (p2.Position.Z - p1.Position.Z) * t
+ )
+ };
+ }
+
+ ///
+ /// 计算每段的速度
+ ///
+ private void CalculateSegmentSpeeds(SimulationData data, TimeTagProfile profile)
+ {
+ if (data.Waypoints.Count < 2) return;
+
+ for (int i = 0; i < data.Waypoints.Count - 1; i++)
+ {
+ var wp1 = data.Waypoints[i];
+ var wp2 = data.Waypoints[i + 1];
+
+ double distance = wp2.Distance - wp1.Distance;
+ double time = wp2.Time - wp1.Time - wp1.WaitTime - wp1.ActionTime;
+
+ if (time > 0)
+ {
+ wp1.Speed = distance / time;
+ wp1.SpeedRatio = wp1.Speed / profile.DefaultSpeed;
+ }
+ }
+ }
+
+ ///
+ /// 生成仿真脚本(用于外部系统)
+ ///
+ public string GenerateSimulationScript(TimeTagProfile profile, PathRoute route)
+ {
+ var data = CreateSimulationData(profile, route);
+ var sb = new System.Text.StringBuilder();
+
+ sb.AppendLine("# Simulation Script");
+ sb.AppendLine($"# Route: {data.RouteName}");
+ sb.AppendLine($"# Profile: {data.ProfileName}");
+ sb.AppendLine($"# Total Time: {data.TotalTime:F2}s");
+ sb.AppendLine($"# Total Distance: {data.TotalDistance:F2}m");
+ sb.AppendLine();
+
+ foreach (var wp in data.Waypoints)
+ {
+ sb.AppendLine($"T={wp.Time:F2}s | D={wp.Distance:F2}m | " +
+ $"Pos=({wp.Position.X:F2}, {wp.Position.Y:F2}, {wp.Position.Z:F2}) | " +
+ $"Speed={wp.Speed:F2}m/s | {wp.EventType}");
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// 导出仿真数据为JSON
+ ///
+ public string ExportSimulationJson(TimeTagProfile profile, PathRoute route)
+ {
+ var data = CreateSimulationData(profile, route);
+ return ExportSimulationDataToJson(data);
+ }
+
+ ///
+ /// 将仿真数据转换为JSON
+ ///
+ private string ExportSimulationDataToJson(SimulationData data)
+ {
+ var sb = new System.Text.StringBuilder();
+ sb.AppendLine("{");
+ sb.AppendLine($" \"routeName\": \"{EscapeJson(data.RouteName)}\",");
+ sb.AppendLine($" \"profileName\": \"{EscapeJson(data.ProfileName)}\",");
+ sb.AppendLine($" \"totalTime\": {data.TotalTime:F3},");
+ sb.AppendLine($" \"totalDistance\": {data.TotalDistance:F3},");
+ sb.AppendLine(" \"waypoints\": [");
+
+ for (int i = 0; i < data.Waypoints.Count; i++)
+ {
+ var wp = data.Waypoints[i];
+ sb.AppendLine(" {");
+ sb.AppendLine($" \"time\": {wp.Time:F3},");
+ sb.AppendLine($" \"distance\": {wp.Distance:F3},");
+ sb.Append($" \"position\": {{\"x\": {wp.Position.X:F3}, \"y\": {wp.Position.Y:F3}, \"z\": {wp.Position.Z:F3}}},");
+ sb.AppendLine();
+ sb.AppendLine($" \"eventType\": \"{wp.EventType}\",");
+ sb.AppendLine($" \"eventName\": \"{EscapeJson(wp.EventName)}\",");
+ sb.AppendLine($" \"waitTime\": {wp.WaitTime:F2},");
+ sb.AppendLine($" \"actionTime\": {wp.ActionTime:F2},");
+ sb.AppendLine($" \"speed\": {wp.Speed:F3},");
+ sb.AppendLine($" \"speedRatio\": {wp.SpeedRatio:F2}");
+ sb.Append(" }");
+ if (i < data.Waypoints.Count - 1) sb.Append(",");
+ sb.AppendLine();
+ }
+
+ sb.AppendLine(" ]");
+ sb.AppendLine("}");
+
+ return sb.ToString();
+ }
+
+ ///
+ /// 转义JSON字符串
+ ///
+ private string EscapeJson(string str)
+ {
+ if (string.IsNullOrEmpty(str)) return "";
+ return str.Replace("\\", "\\\\")
+ .Replace("\"", "\\\"")
+ .Replace("\n", "\\n")
+ .Replace("\r", "\\r")
+ .Replace("\t", "\\t");
+ }
+ }
+
+ ///
+ /// 仿真数据结构
+ ///
+ public class SimulationData
+ {
+ public string RouteName { get; set; }
+ public string ProfileName { get; set; }
+ public double TotalTime { get; set; }
+ public double TotalDistance { get; set; }
+ public List Waypoints { get; set; }
+ }
+
+ ///
+ /// 仿真路径点
+ ///
+ public class SimulationWaypoint
+ {
+ public double Time { get; set; }
+ public double Distance { get; set; }
+ public Point3D Position { get; set; }
+ public EventType EventType { get; set; }
+ public string EventName { get; set; }
+ public double WaitTime { get; set; }
+ public double ActionTime { get; set; }
+ public double Speed { get; set; }
+ public double SpeedRatio { get; set; }
+ }
+}
diff --git a/src/UI/WPF/Controls/EventTimelineControl.xaml b/src/UI/WPF/Controls/EventTimelineControl.xaml
new file mode 100644
index 0000000..8497db1
--- /dev/null
+++ b/src/UI/WPF/Controls/EventTimelineControl.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UI/WPF/Controls/EventTimelineControl.xaml.cs b/src/UI/WPF/Controls/EventTimelineControl.xaml.cs
new file mode 100644
index 0000000..1d845bf
--- /dev/null
+++ b/src/UI/WPF/Controls/EventTimelineControl.xaml.cs
@@ -0,0 +1,402 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Shapes;
+using NavisworksTransport.Core.Models;
+
+namespace NavisworksTransport.UI.WPF.Controls
+{
+ ///
+ /// 事件时间线可视化控件
+ ///
+ public partial class EventTimelineControl : UserControl
+ {
+ // 固定参数
+ private const double PADDING = 50; // 左右边距
+ private const double NODE_Y = 40; // 节点Y坐标(基线位置)
+ private const double MIN_NODE_SPACING = 100; // 最小节点间距
+ private const double MIN_CANVAS_WIDTH = 600; // 最小画布宽度
+
+ public EventTimelineControl()
+ {
+ InitializeComponent();
+ Loaded += OnLoaded;
+ SizeChanged += OnSizeChanged;
+ }
+
+ private void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ AttachCollectionChanged();
+ DrawTimeline();
+ }
+
+ private void OnSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ DrawTimeline();
+ }
+
+ #region 依赖属性
+
+ public static readonly DependencyProperty EventsProperty =
+ DependencyProperty.Register(nameof(Events), typeof(IEnumerable),
+ typeof(EventTimelineControl),
+ new PropertyMetadata(null, OnEventsChanged));
+
+ public IEnumerable Events
+ {
+ get => (IEnumerable)GetValue(EventsProperty);
+ set => SetValue(EventsProperty, value);
+ }
+
+ private static void OnEventsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var control = (EventTimelineControl)d;
+ control.DetachCollectionChanged(e.OldValue as INotifyCollectionChanged);
+ control.AttachCollectionChanged();
+ control.DrawTimeline();
+ }
+
+ ///
+ /// 附加集合变更监听
+ ///
+ private void AttachCollectionChanged()
+ {
+ if (Events is INotifyCollectionChanged notifyCollection)
+ {
+ notifyCollection.CollectionChanged += OnCollectionChanged;
+ }
+ }
+
+ ///
+ /// 移除集合变更监听
+ ///
+ private void DetachCollectionChanged(INotifyCollectionChanged oldCollection)
+ {
+ if (oldCollection != null)
+ {
+ oldCollection.CollectionChanged -= OnCollectionChanged;
+ }
+ }
+
+ ///
+ /// 集合内容变化时重绘
+ ///
+ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ // 使用 Dispatcher 确保在 UI 线程执行
+ Dispatcher.BeginInvoke(new Action(DrawTimeline));
+ }
+
+ public static readonly DependencyProperty TotalDistanceProperty =
+ DependencyProperty.Register(nameof(TotalDistance), typeof(double),
+ typeof(EventTimelineControl),
+ new PropertyMetadata(100.0, (d, e) => ((EventTimelineControl)d).DrawTimeline()));
+
+ public double TotalDistance
+ {
+ get => (double)GetValue(TotalDistanceProperty);
+ set => SetValue(TotalDistanceProperty, value);
+ }
+
+ public static readonly DependencyProperty SelectedEventProperty =
+ DependencyProperty.Register(nameof(SelectedEvent), typeof(TimeTagEvent),
+ typeof(EventTimelineControl),
+ new PropertyMetadata(null, (d, e) => ((EventTimelineControl)d).HighlightSelectedEvent()));
+
+ public TimeTagEvent SelectedEvent
+ {
+ get => (TimeTagEvent)GetValue(SelectedEventProperty);
+ set => SetValue(SelectedEventProperty, value);
+ }
+
+ #endregion
+
+ ///
+ /// 获取实际可用的画布宽度
+ ///
+ private double GetCanvasWidth()
+ {
+ if (TimelineCanvas == null) return MIN_CANVAS_WIDTH;
+
+ var events = Events?.ToList();
+ int eventCount = events?.Count ?? 0;
+
+ // 基于事件数量计算所需宽度
+ double requiredWidth = PADDING * 2 + Math.Max(eventCount - 1, 1) * MIN_NODE_SPACING;
+ requiredWidth = Math.Max(requiredWidth, MIN_CANVAS_WIDTH);
+
+ // 获取ScrollViewer的实际宽度
+ var scrollViewer = GetParentScrollViewer();
+ double availableWidth = scrollViewer?.ActualWidth ?? TimelineCanvas.ActualWidth;
+
+ // 如果可用宽度大于所需宽度,使用可用宽度(让基线填满空间)
+ // 如果可用宽度小于所需宽度,使用所需宽度(启用滚动)
+ return Math.Max(availableWidth, requiredWidth);
+ }
+
+ ///
+ /// 获取父级ScrollViewer
+ ///
+ private ScrollViewer GetParentScrollViewer()
+ {
+ DependencyObject parent = TimelineCanvas;
+ while (parent != null)
+ {
+ if (parent is ScrollViewer sv) return sv;
+ parent = VisualTreeHelper.GetParent(parent);
+ }
+ return null;
+ }
+
+ ///
+ /// 绘制时间线
+ ///
+ private void DrawTimeline()
+ {
+ if (TimelineCanvas == null) return;
+
+ // 清除动态添加的元素(保留基线)
+ ClearDynamicElements();
+
+ var events = Events?.OrderBy(e => e.Distance).ToList();
+ if (events == null || events.Count == 0)
+ {
+ // 没有事件时画一条简单的基线
+ DrawBaseLineOnly();
+ return;
+ }
+
+ // 计算画布宽度并设置
+ double canvasWidth = GetCanvasWidth();
+ TimelineCanvas.Width = canvasWidth;
+
+ // 计算可用绘图区域
+ double availableWidth = canvasWidth - PADDING * 2;
+
+ // 更新基线
+ UpdateBaseLine(PADDING, canvasWidth - PADDING);
+
+ // 绘制所有事件节点
+ for (int i = 0; i < events.Count; i++)
+ {
+ var evt = events[i];
+
+ // 计算位置:基于距离比例
+ double positionRatio = TotalDistance > 0 ? evt.Distance / TotalDistance : 0;
+ double x = PADDING + positionRatio * availableWidth;
+
+ // 确保第一个点在起点,最后一个点在终点
+ if (i == 0) x = PADDING;
+ else if (i == events.Count - 1) x = canvasWidth - PADDING;
+
+ DrawEventNode(evt, x, NODE_Y, i);
+ }
+
+ HighlightSelectedEvent();
+ }
+
+ ///
+ /// 清除动态元素
+ ///
+ private void ClearDynamicElements()
+ {
+ var toRemove = TimelineCanvas.Children.OfType()
+ .Where(c => c != BaseLine)
+ .ToList();
+
+ foreach (var child in toRemove)
+ {
+ TimelineCanvas.Children.Remove(child);
+ }
+ }
+
+ ///
+ /// 仅绘制基线(无事件时)
+ ///
+ private void DrawBaseLineOnly()
+ {
+ double canvasWidth = Math.Max(TimelineCanvas.ActualWidth, MIN_CANVAS_WIDTH);
+ if (double.IsNaN(canvasWidth) || canvasWidth <= 0) canvasWidth = MIN_CANVAS_WIDTH;
+
+ TimelineCanvas.Width = canvasWidth;
+ UpdateBaseLine(PADDING, canvasWidth - PADDING);
+ }
+
+ ///
+ /// 更新基线位置
+ ///
+ private void UpdateBaseLine(double x1, double x2)
+ {
+ BaseLine.X1 = x1;
+ BaseLine.Y1 = NODE_Y;
+ BaseLine.X2 = x2;
+ BaseLine.Y2 = NODE_Y;
+ }
+
+ ///
+ /// 绘制单个事件节点
+ ///
+ private void DrawEventNode(TimeTagEvent evt, double x, double y, int index)
+ {
+ var color = GetEventColor(evt.Type);
+
+ // 节点圆形(直径14)
+ var ellipse = new Ellipse
+ {
+ Width = 14,
+ Height = 14,
+ Fill = new SolidColorBrush(color),
+ Stroke = Brushes.White,
+ StrokeThickness = 2,
+ Tag = evt
+ };
+
+ // 居中定位
+ Canvas.SetLeft(ellipse, x - 7);
+ Canvas.SetTop(ellipse, y - 7);
+
+ // 鼠标事件
+ ellipse.MouseLeftButtonDown += (s, e) =>
+ {
+ SelectedEvent = evt;
+ e.Handled = true;
+ };
+
+ ellipse.MouseEnter += (s, e) =>
+ {
+ ellipse.Width = 18;
+ ellipse.Height = 18;
+ Canvas.SetLeft(ellipse, x - 9);
+ Canvas.SetTop(ellipse, y - 9);
+ ellipse.Cursor = System.Windows.Input.Cursors.Hand;
+ };
+
+ ellipse.MouseLeave += (s, e) =>
+ {
+ ellipse.Width = 14;
+ ellipse.Height = 14;
+ Canvas.SetLeft(ellipse, x - 7);
+ Canvas.SetTop(ellipse, y - 7);
+ };
+
+ TimelineCanvas.Children.Add(ellipse);
+
+ // 交替上下显示标签,避免重叠
+ bool showAbove = (index % 2) == 1;
+ DrawEventLabel(evt, x, y, showAbove, color);
+ }
+
+ ///
+ /// 绘制事件标签
+ ///
+ private void DrawEventLabel(TimeTagEvent evt, double x, double y, bool showAbove, Color color)
+ {
+ // 连接线起点和终点
+ double lineStartY = showAbove ? y - 7 : y + 7;
+ double lineEndY = showAbove ? y - 20 : y + 20;
+ double textY = showAbove ? y - 35 : y + 22;
+
+ // 垂直连接线
+ var line = new Line
+ {
+ Stroke = new SolidColorBrush(color),
+ StrokeThickness = 1,
+ StrokeDashArray = new DoubleCollection { 2, 2 },
+ X1 = x,
+ Y1 = lineStartY,
+ X2 = x,
+ Y2 = lineEndY,
+ Tag = evt
+ };
+ TimelineCanvas.Children.Add(line);
+
+ // 事件名称
+ var nameText = new TextBlock
+ {
+ Text = evt.Name,
+ FontSize = 10,
+ Foreground = Brushes.Black,
+ TextAlignment = TextAlignment.Center,
+ Tag = evt
+ };
+
+ // 计算文本宽度并居中
+ nameText.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+ double textWidth = nameText.DesiredSize.Width;
+ Canvas.SetLeft(nameText, x - textWidth / 2);
+ Canvas.SetTop(nameText, textY);
+ TimelineCanvas.Children.Add(nameText);
+
+ // 累计时间(在名称下方/上方)
+ var timeText = new TextBlock
+ {
+ Text = $"{evt.CumulativeTime:F1}s",
+ FontSize = 9,
+ Foreground = Brushes.Gray,
+ TextAlignment = TextAlignment.Center,
+ Tag = evt
+ };
+
+ timeText.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+ textWidth = timeText.DesiredSize.Width;
+ Canvas.SetLeft(timeText, x - textWidth / 2);
+ Canvas.SetTop(timeText, textY + (showAbove ? -13 : 13));
+ TimelineCanvas.Children.Add(timeText);
+ }
+
+ ///
+ /// 高亮选中事件
+ ///
+ private void HighlightSelectedEvent()
+ {
+ if (TimelineCanvas == null) return;
+
+ foreach (var ellipse in TimelineCanvas.Children.OfType()
+ .Where(e => e.Tag is TimeTagEvent))
+ {
+ var evt = ellipse.Tag as TimeTagEvent;
+ if (evt == SelectedEvent)
+ {
+ ellipse.Stroke = new SolidColorBrush(Colors.Red);
+ ellipse.StrokeThickness = 3;
+ Canvas.SetZIndex(ellipse, 100);
+ }
+ else
+ {
+ ellipse.Stroke = Brushes.White;
+ ellipse.StrokeThickness = 2;
+ Canvas.SetZIndex(ellipse, 0);
+ }
+ }
+ }
+
+ ///
+ /// 获取事件类型对应的颜色
+ ///
+ private Color GetEventColor(EventType type)
+ {
+ switch (type)
+ {
+ case EventType.Start:
+ return Color.FromRgb(76, 175, 80); // Green
+ case EventType.End:
+ return Color.FromRgb(96, 125, 139); // Blue Gray
+ case EventType.Turn:
+ return Color.FromRgb(255, 152, 0); // Orange
+ case EventType.Wait:
+ return Color.FromRgb(156, 39, 176); // Purple
+ case EventType.Load:
+ case EventType.Unload:
+ return Color.FromRgb(244, 67, 54); // Red
+ case EventType.Pass:
+ return Color.FromRgb(33, 150, 243); // Blue
+ default:
+ return Colors.Gray;
+ }
+ }
+ }
+}
diff --git a/src/UI/WPF/ViewModels/TimeTagViewModel.cs b/src/UI/WPF/ViewModels/TimeTagViewModel.cs
index 99a6057..6197b1c 100644
--- a/src/UI/WPF/ViewModels/TimeTagViewModel.cs
+++ b/src/UI/WPF/ViewModels/TimeTagViewModel.cs
@@ -3,504 +3,522 @@ using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
-using NavisworksTransport.Core.Config;
-using NavisworksTransport.PathPlanning;
+using System.Windows.Input;
+using Microsoft.Win32;
+using NavisworksTransport.Core.Models;
+using NavisworksTransport.Core.Services;
namespace NavisworksTransport.UI.WPF.ViewModels
{
///
- /// 时间标签视图模型
- /// 用于时间标签设置对话框的数据绑定和业务逻辑
+ /// 时标对话框视图模型 - 支持完整的事件编辑
///
public class TimeTagViewModel : INotifyPropertyChanged
{
- #region 字段
-
- private double _defaultSpeed;
- private bool _useElementSpeedLimit = true;
- private bool _isCalculating = false;
- private double _totalDuration;
- private string _selectedRoute;
- private readonly TimeMarkerCalculationService _timeCalculationService;
- private PathPlanningManager _pathPlanningManager;
-
- #endregion
+ private readonly TimeTagService _timeTagService;
+ private readonly TimeTagExporter _exporter;
+ private PathRoute _currentRoute;
+ private TimeTagProfile _currentProfile;
+ private TimeTagEvent _selectedEvent;
+ private string _selectedProfileId;
#region 属性
+ public string RouteName => _currentRoute?.Name ?? "未知路径";
+
+ public ObservableCollection Profiles { get; set; }
+
+ public string SelectedProfileId
+ {
+ get => _selectedProfileId;
+ set
+ {
+ _selectedProfileId = value;
+ OnPropertyChanged();
+ LoadProfile(value);
+ }
+ }
+
+ public TimeTagProfile CurrentProfile
+ {
+ get => _currentProfile;
+ set
+ {
+ _currentProfile = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(CanEdit));
+ OnPropertyChanged(nameof(DefaultSpeed));
+ OnPropertyChanged(nameof(TurnSpeedRatio));
+ RefreshEvents();
+ }
+ }
+
+ public ObservableCollection Events { get; set; }
+
///
- /// 默认速度 (m/s)
+ /// 当前选中的事件
///
+ public TimeTagEvent SelectedEvent
+ {
+ get => _selectedEvent;
+ set
+ {
+ _selectedEvent = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(CanEditEvent));
+ OnPropertyChanged(nameof(IsSelectedDistanceEditable));
+ OnPropertyChanged(nameof(IsSelectedEventTypeEditable));
+ OnPropertyChanged(nameof(SelectedEventType));
+ OnPropertyChanged(nameof(SelectedWaitTime));
+ OnPropertyChanged(nameof(SelectedActionTime));
+ OnPropertyChanged(nameof(SelectedDistance));
+ OnPropertyChanged(nameof(SelectedDistanceDisplay));
+ }
+ }
+
+ public bool CanEdit => CurrentProfile != null;
+ public bool CanEditEvent => SelectedEvent != null && CurrentProfile != null;
+
+ ///
+ /// 选中事件的距离是否可编辑
+ /// PointId为空表示手动添加的事件,距离可编辑
+ /// PointId不为空表示关联路径点的事件,距离由路径点决定,不可编辑
+ ///
+ public bool IsSelectedDistanceEditable => SelectedEvent != null && string.IsNullOrEmpty(SelectedEvent.PointId);
+
+ ///
+ /// 选中事件的事件类型是否可编辑
+ /// 同样基于PointId判断,自动生成的事件类型固定
+ ///
+ public bool IsSelectedEventTypeEditable => SelectedEvent != null && string.IsNullOrEmpty(SelectedEvent.PointId);
+
public double DefaultSpeed
{
- get => _defaultSpeed;
+ get => CurrentProfile?.DefaultSpeed ?? 1.0;
set
{
- _defaultSpeed = value;
- OnPropertyChanged();
- // 速度改变时重新计算时间
- RecalculateTimeSegments();
- }
- }
-
- ///
- /// 是否使用元素限速优先
- ///
- public bool UseElementSpeedLimit
- {
- get => _useElementSpeedLimit;
- set
- {
- _useElementSpeedLimit = value;
- OnPropertyChanged();
- // 限速设置改变时重新计算
- RecalculateTimeSegments();
- }
- }
-
- ///
- /// 是否正在计算
- ///
- public bool IsCalculating
- {
- get => _isCalculating;
- set
- {
- _isCalculating = value;
- OnPropertyChanged();
- }
- }
-
- ///
- /// 总时长 (秒)
- ///
- public double TotalDuration
- {
- get => _totalDuration;
- set
- {
- _totalDuration = value;
- OnPropertyChanged();
- OnPropertyChanged(nameof(TotalDurationText));
- }
- }
-
- ///
- /// 总时长文本显示
- ///
- public string TotalDurationText => $"{TotalDuration:F1}秒";
-
- ///
- /// 选中的路径名称
- ///
- public string SelectedRoute
- {
- get => _selectedRoute;
- set
- {
- _selectedRoute = value;
- OnPropertyChanged();
- }
- }
-
- ///
- /// 时间分段列表
- ///
- public ObservableCollection TimeSegments { get; set; }
-
- ///
- /// 时间轴预览项目列表
- ///
- public ObservableCollection TimelineItems { get; set; }
-
- #endregion
-
- #region 构造函数
-
- ///
- /// 初始化时间标签视图模型
- ///
- public TimeTagViewModel()
- {
- try
- {
- // 从配置读取默认速度
- var config = ConfigManager.Instance.Current;
- _defaultSpeed = config.Logistics.SpeedLimitMetersPerSecond;
-
- _timeCalculationService = new TimeMarkerCalculationService();
- _pathPlanningManager = PathPlanningManager.GetActivePathManager();
-
- InitializeCollections();
- LoadRouteData(_pathPlanningManager.CurrentRoute);
-
- // 订阅配置变更事件
- SubscribeToConfigChanges();
-
- LogManager.Info($"TimeTagViewModel初始化完成 - 默认速度: {_defaultSpeed:F1}m/s(来自配置)");
- }
- catch (Exception ex)
- {
- LogManager.Error($"初始化TimeTagViewModel失败: {ex.Message}", ex);
- }
- }
-
- #region 配置变更处理
-
- ///
- /// 订阅配置变更事件
- ///
- private void SubscribeToConfigChanges()
- {
- try
- {
- ConfigManager.Instance.CategoryConfigurationChanged += OnCategoryConfigurationChanged;
- LogManager.Info("TimeTagViewModel 已订阅配置变更事件");
- }
- catch (Exception ex)
- {
- LogManager.Error($"订阅配置变更事件失败: {ex.Message}", ex);
- }
- }
-
- ///
- /// 取消订阅配置变更事件
- ///
- private void UnsubscribeFromConfigChanges()
- {
- try
- {
- ConfigManager.Instance.CategoryConfigurationChanged -= OnCategoryConfigurationChanged;
- LogManager.Info("TimeTagViewModel 已取消订阅配置变更事件");
- }
- catch (Exception ex)
- {
- LogManager.Warning($"取消订阅配置变更事件时发生异常: {ex.Message}");
- }
- }
-
- ///
- /// 配置变更事件处理
- ///
- private void OnCategoryConfigurationChanged(object sender, Tuple args)
- {
- try
- {
- var category = args.Item1;
- var eventArgs = args.Item2;
-
- // 处理 Logistics 类别变更(默认速度)
- if (category == ConfigCategory.Logistics || category == ConfigCategory.All)
+ if (CurrentProfile != null && Math.Abs(CurrentProfile.DefaultSpeed - value) > 0.001)
{
- LogManager.Info("收到 Logistics 配置变更通知,正在更新默认速度...");
-
- var config = ConfigManager.Instance.Current;
- double newSpeed = config.Logistics.SpeedLimitMetersPerSecond;
-
- // 更新默认速度
- if (_defaultSpeed != newSpeed)
- {
- _defaultSpeed = newSpeed;
- DefaultSpeed = newSpeed; // 触发属性变更通知
- LogManager.Info($"默认速度已更新: {newSpeed:F1} m/s");
- }
+ CurrentProfile.DefaultSpeed = value;
+ OnPropertyChanged();
+ Recalculate();
}
}
- catch (Exception ex)
+ }
+
+ public double TurnSpeedRatio
+ {
+ get => CurrentProfile?.TurnSpeedRatio ?? 0.5;
+ set
{
- LogManager.Error($"处理配置变更事件失败: {ex.Message}", ex);
+ if (CurrentProfile != null && Math.Abs(CurrentProfile.TurnSpeedRatio - value) > 0.01)
+ {
+ CurrentProfile.TurnSpeedRatio = value;
+ OnPropertyChanged();
+ Recalculate();
+ }
+ }
+ }
+
+ public string TotalTimeText => CurrentProfile != null ? $"{CurrentProfile.TotalTime:F1} 秒" : "--";
+ public string TotalDistanceText => _currentRoute != null ? $"{_currentRoute.TotalLength:F1} 米" : "--";
+ public double TotalDistanceValue => _currentRoute?.TotalLength ?? 100;
+
+ ///
+ /// 事件类型列表
+ ///
+ public EventType[] EventTypes => (EventType[])Enum.GetValues(typeof(EventType));
+
+ #endregion
+
+ #region 选中事件的编辑属性
+
+ public EventType SelectedEventType
+ {
+ get => SelectedEvent?.Type ?? EventType.Pass;
+ set
+ {
+ // 只有手动添加的事件才能修改类型
+ if (SelectedEvent != null && IsSelectedEventTypeEditable && SelectedEvent.Type != value)
+ {
+ SelectedEvent.Type = value;
+ OnPropertyChanged();
+ SaveAndRecalculate();
+ }
+ }
+ }
+
+ public double SelectedWaitTime
+ {
+ get => SelectedEvent?.WaitTime ?? 0;
+ set
+ {
+ if (SelectedEvent != null && Math.Abs(SelectedEvent.WaitTime - value) > 0.01)
+ {
+ SelectedEvent.WaitTime = value;
+ OnPropertyChanged();
+ SaveAndRecalculate();
+ }
+ }
+ }
+
+ public double SelectedActionTime
+ {
+ get => SelectedEvent?.ActionTime ?? 0;
+ set
+ {
+ if (SelectedEvent != null && Math.Abs(SelectedEvent.ActionTime - value) > 0.01)
+ {
+ SelectedEvent.ActionTime = value;
+ OnPropertyChanged();
+ SaveAndRecalculate();
+ }
+ }
+ }
+
+ ///
+ /// 选中事件的距离显示(只读)
+ ///
+ public string SelectedDistanceDisplay => SelectedEvent != null ? $"{SelectedEvent.Distance:F2} m" : "--";
+
+ public double SelectedDistance
+ {
+ get => SelectedEvent?.Distance ?? 0;
+ set
+ {
+ // 只有手动添加的事件(PointId为空)才能编辑距离
+ if (SelectedEvent != null && IsSelectedDistanceEditable && Math.Abs(SelectedEvent.Distance - value) > 0.01)
+ {
+ SelectedEvent.Distance = Math.Max(0, Math.Min(value, _currentRoute?.TotalLength ?? value));
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(SelectedDistanceDisplay));
+ // 距离改变需要重新排序
+ ReorderEventsAndRecalculate();
+ }
}
}
#endregion
+ #region 命令
+
+ public ICommand CreateProfileCommand { get; }
+ public ICommand DeleteProfileCommand { get; }
+ public ICommand RefreshCommand { get; }
+ public ICommand ExportCommand { get; }
+ public ICommand InsertEventCommand { get; }
+ public ICommand DeleteEventCommand { get; }
+
#endregion
- #region 初始化方法
-
- ///
- /// 初始化集合
- ///
- private void InitializeCollections()
+ public TimeTagViewModel(TimeTagService timeTagService, PathRoute route)
{
- TimeSegments = new ObservableCollection();
- TimelineItems = new ObservableCollection();
+ _timeTagService = timeTagService ?? throw new ArgumentNullException(nameof(timeTagService));
+ _exporter = new TimeTagExporter();
+ _currentRoute = route;
+
+ Profiles = new ObservableCollection();
+ Events = new ObservableCollection();
+
+ CreateProfileCommand = new RelayCommand(() => CreateProfile());
+ DeleteProfileCommand = new RelayCommand(() => DeleteProfile(), () => CurrentProfile != null);
+ RefreshCommand = new RelayCommand(() => RefreshProfiles());
+ ExportCommand = new RelayCommand(() => Export(), () => CurrentProfile != null);
+ InsertEventCommand = new RelayCommand(() => InsertEvent(), () => CurrentProfile != null && _currentRoute != null);
+ DeleteEventCommand = new RelayCommand(() => DeleteEvent(), () => SelectedEvent != null);
+
+ RefreshProfiles();
}
- ///
- /// 从真实路径数据加载时间分段
- ///
- /// 路径数据
- private void LoadRouteData(PathRoute route)
+ private void RefreshProfiles()
{
- try
+ if (_currentRoute == null) return;
+
+ Profiles.Clear();
+ var profiles = _timeTagService.GetProfiles(_currentRoute.Id);
+ foreach (var profile in profiles)
{
- if (route == null)
+ Profiles.Add(profile);
+ }
+
+ if (Profiles.Count > 0 && string.IsNullOrEmpty(SelectedProfileId))
+ {
+ SelectedProfileId = Profiles[0].Id;
+ }
+ }
+
+ private void LoadProfile(string profileId)
+ {
+ if (string.IsNullOrEmpty(profileId))
+ {
+ CurrentProfile = null;
+ return;
+ }
+
+ var profile = Profiles.FirstOrDefault(p => p.Id == profileId);
+
+ // 从数据库加载后需要重新计算时间
+ if (profile != null && _currentRoute != null)
+ {
+ _timeTagService.UpdateProfile(profile, _currentRoute);
+ }
+
+ CurrentProfile = profile;
+ }
+
+ private void RefreshEvents()
+ {
+ Events.Clear();
+ if (CurrentProfile?.Events != null)
+ {
+ foreach (var evt in CurrentProfile.Events.OrderBy(e => e.Distance))
{
- LogManager.Info("路径为空,加载演示数据");
+ Events.Add(evt);
+ }
+ }
+ OnPropertyChanged(nameof(TotalTimeText));
+ }
+
+ private void CreateProfile()
+ {
+ if (_currentRoute == null) return;
+
+ string profileName = $"配置 {Profiles.Count + 1}";
+ var newProfile = _timeTagService.CreateProfile(_currentRoute.Id, profileName, _currentRoute);
+
+ Profiles.Add(newProfile);
+ SelectedProfileId = newProfile.Id;
+ }
+
+ private void DeleteProfile()
+ {
+ if (CurrentProfile == null) return;
+
+ var result = System.Windows.MessageBox.Show(
+ $"确定要删除配置 \"{CurrentProfile.Name}\" 吗?",
+ "确认删除",
+ System.Windows.MessageBoxButton.YesNo,
+ System.Windows.MessageBoxImage.Question);
+
+ if (result != System.Windows.MessageBoxResult.Yes) return;
+
+ _timeTagService.DeleteProfile(CurrentProfile.Id);
+ Profiles.Remove(CurrentProfile);
+
+ if (Profiles.Count > 0)
+ {
+ SelectedProfileId = Profiles[0].Id;
+ }
+ else
+ {
+ SelectedProfileId = null;
+ }
+ }
+
+ #region 事件操作
+
+ ///
+ /// 插入新事件
+ /// 如果有选中事件,在选中事件后插入;否则在末尾插入
+ /// 不能插入在终点之后
+ ///
+ private void InsertEvent()
+ {
+ if (CurrentProfile == null || _currentRoute == null) return;
+
+ double distance;
+ int insertIndex;
+
+ if (SelectedEvent != null)
+ {
+ // 在选中事件后插入
+ var currentIndex = Events.IndexOf(SelectedEvent);
+
+ // 检查是否选中了终点,不能在终点后插入
+ if (SelectedEvent.Type == EventType.End || currentIndex >= Events.Count - 1)
+ {
+ System.Windows.MessageBox.Show("不能在终点之后插入事件", "提示",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Information);
return;
}
- LogManager.Info($"开始从路径 '{route.Name}' 加载真实时间数据");
-
- // 使用TimeMarkerCalculationService计算时间分段
- var result = _timeCalculationService.CalculateTimeMarkers(route);
-
- if (!result.IsSuccessful)
+ distance = SelectedEvent.Distance + 5;
+ insertIndex = currentIndex + 1;
+
+ // 确保不超过下一点
+ var nextEvent = Events[currentIndex + 1];
+ if (nextEvent != null)
{
- LogManager.Warning($"时标计算失败: {result.ErrorMessage},回退到演示数据");
- return;
+ // 取中间位置,但至少留1米间距
+ distance = Math.Min(distance, (SelectedEvent.Distance + nextEvent.Distance) / 2);
+ distance = Math.Max(distance, SelectedEvent.Distance + 1);
}
-
- // 清空现有数据
- TimeSegments.Clear();
- TimelineItems.Clear();
-
- // 添加路径分段数据
- for (int i = 0; i < result.Segments.Count; i++)
- {
- var segment = result.Segments[i];
- var segmentItem = new TimeSegmentItem
- {
- Index = i + 1,
- Length = segment.Distance,
- Speed = segment.SpeedLimit,
- Duration = segment.Time,
- CumulativeTime = segment.CumulativeTime,
- Remarks = segment.IsValid ? $"{segment.StartPointName}->{segment.EndPointName}" : "无限速"
- };
- TimeSegments.Add(segmentItem);
- }
-
- // 更新总时长(使用实际计算时间,不添加额外等待)
- TotalDuration = result.TotalTime;
-
- // 更新选中路径名称
- SelectedRoute = route.Name;
-
- // 生成时间轴预览数据
- GenerateTimelineItems();
-
- LogManager.Info($"真实时间数据加载完成: 总距离 {result.TotalDistance:F2}m, 总时间 {result.TotalTime:F2}s");
}
- catch (Exception ex)
+ else
{
- LogManager.Error($"加载路径时间数据失败: {ex.Message}", ex);
+ // 没有选中事件,在起点后插入(默认位置)
+ if (Events.Count >= 2)
+ {
+ var firstEvent = Events[0];
+ var secondEvent = Events[1];
+ distance = (firstEvent.Distance + secondEvent.Distance) / 2;
+ insertIndex = 1;
+ }
+ else
+ {
+ distance = _currentRoute.TotalLength / 2;
+ insertIndex = 1;
+ }
}
+
+ // 确保不超出路径总长
+ distance = Math.Min(distance, _currentRoute.TotalLength - 1);
+
+ var newEvent = new TimeTagEvent
+ {
+ Id = Guid.NewGuid().ToString(),
+ ProfileId = CurrentProfile.Id,
+ Name = $"事件{Events.Count + 1}",
+ Type = EventType.Wait,
+ Distance = distance,
+ WaitTime = 5,
+ ActionTime = 0,
+ SortOrder = insertIndex + 1
+ };
+
+ CurrentProfile.Events.Add(newEvent);
+
+ // 重新排序并选中
+ ReorderEventsAndRecalculate();
+ SelectedEvent = newEvent;
}
///
- /// 生成时间轴预览数据
+ /// 删除选中事件
///
- private void GenerateTimelineItems()
+ private void DeleteEvent()
{
- try
+ if (SelectedEvent == null || CurrentProfile == null) return;
+
+ // 不能删除起点和终点
+ if (SelectedEvent.Type == EventType.Start || SelectedEvent.Type == EventType.End)
{
- TimelineItems.Clear();
-
- if (TotalDuration <= 0) return;
-
- // 为每个真实路径分段生成时间轴项目(只处理真实的路径段,不包括等待时间)
- foreach (var segment in TimeSegments.Where(s => s.Index > 0))
- {
- var timelineItem = new TimelinePreviewItem
- {
- Name = segment.Remarks, // 使用真实的路径点名称(如"自动起点->自动点1")
- Duration = segment.Duration,
- Color = segment.Index == 1 ? "#FF2B579A" : "#FFFF9800", // 第一段蓝色,其他橙色
- Percentage = TotalDuration > 0 ? segment.Duration / TotalDuration * 100 : 0
- };
- TimelineItems.Add(timelineItem);
- }
-
- LogManager.Info($"时间轴预览数据生成完成,包含 {TimelineItems.Count} 个项目");
+ System.Windows.MessageBox.Show("不能删除起点或终点事件", "提示",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Information);
+ return;
}
- catch (Exception ex)
+
+ var result = System.Windows.MessageBox.Show(
+ $"确定要删除事件 \"{SelectedEvent.Name}\" 吗?",
+ "确认删除",
+ System.Windows.MessageBoxButton.YesNo,
+ System.Windows.MessageBoxImage.Question);
+
+ if (result != System.Windows.MessageBoxResult.Yes) return;
+
+ CurrentProfile.Events.Remove(SelectedEvent);
+ Events.Remove(SelectedEvent);
+ SelectedEvent = null;
+
+ SaveAndRecalculate();
+ }
+
+ ///
+ /// 重新排序事件并重新计算
+ ///
+ private void ReorderEventsAndRecalculate()
+ {
+ if (CurrentProfile == null) return;
+
+ // 保存当前选中事件的ID
+ var selectedEventId = SelectedEvent?.Id;
+
+ // 按距离排序并更新SortOrder
+ var sortedEvents = CurrentProfile.Events.OrderBy(e => e.Distance).ToList();
+ for (int i = 0; i < sortedEvents.Count; i++)
{
- LogManager.Error($"生成时间轴预览数据失败: {ex.Message}", ex);
+ sortedEvents[i].SortOrder = i + 1;
+ }
+
+ SaveAndRecalculate();
+ RefreshEvents();
+
+ // 恢复选中状态
+ if (selectedEventId != null)
+ {
+ SelectedEvent = Events.FirstOrDefault(e => e.Id == selectedEventId);
}
}
#endregion
- #region 业务逻辑方法
-
- ///
- /// 重新计算时间分段
- ///
- private void RecalculateTimeSegments()
+ private void Recalculate()
{
- try
- {
- IsCalculating = true;
+ if (CurrentProfile == null || _currentRoute == null) return;
- // 检查是否有真实路径数据
- if (_pathPlanningManager?.CurrentRoute != null)
+ _timeTagService.UpdateProfile(CurrentProfile, _currentRoute);
+ OnPropertyChanged(nameof(TotalTimeText));
+
+ // 刷新事件列表以更新累计时间
+ var selectedId = SelectedEvent?.Id;
+ RefreshEvents();
+
+ if (selectedId != null)
+ {
+ SelectedEvent = Events.FirstOrDefault(e => e.Id == selectedId);
+ }
+ }
+
+ private void SaveAndRecalculate()
+ {
+ if (CurrentProfile == null) return;
+
+ _timeTagService.UpdateProfile(CurrentProfile, _currentRoute);
+ Recalculate();
+ }
+
+ private void Export()
+ {
+ if (CurrentProfile == null || _currentRoute == null) return;
+
+ var dialog = new SaveFileDialog
+ {
+ Title = "导出时标配置",
+ FileName = $"{CurrentProfile.Name}_{DateTime.Now:yyyyMMdd_HHmmss}",
+ DefaultExt = ".json",
+ Filter = "JSON文件 (*.json)|*.json|CSV文件 (*.csv)|*.csv|所有文件 (*.*)|*.*"
+ };
+
+ if (dialog.ShowDialog() == true)
+ {
+ try
{
- // 使用真实路径数据重新计算
- LogManager.Info("使用真实路径数据重新计算时间分段");
+ _exporter.ExportToFile(CurrentProfile, _currentRoute, dialog.FileName);
- // 异步计算以避免UI阻塞
- System.Threading.Tasks.Task.Run(() =>
- {
- try
- {
- var result = _timeCalculationService.CalculateTimeMarkers(_pathPlanningManager.CurrentRoute);
-
- // 在UI线程上更新数据
- System.Windows.Application.Current.Dispatcher.Invoke(() =>
- {
- if (result.IsSuccessful)
- {
- UpdateTimeSegmentsFromCalculation(result);
- }
- else
- {
- LogManager.Warning($"重新计算失败: {result.ErrorMessage}");
- }
-
- IsCalculating = false;
- });
- }
- catch (Exception ex)
- {
- LogManager.Error($"重新计算时间分段异常: {ex.Message}", ex);
- System.Windows.Application.Current.Dispatcher.Invoke(() =>
- {
- IsCalculating = false;
- });
- }
- });
+ string message = $"导出成功:\n{dialog.FileName}\n\n" +
+ $"总时间: {CurrentProfile.TotalTime:F1} 秒\n" +
+ $"总距离: {_currentRoute.TotalLength:F1} 米\n" +
+ $"事件数: {CurrentProfile.Events?.Count ?? 0}";
+
+ System.Windows.MessageBox.Show(message, "导出完成",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Information);
+
+ LogManager.Info($"时标配置已导出: {dialog.FileName}");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"导出失败: {ex.Message}");
+ System.Windows.MessageBox.Show($"导出失败: {ex.Message}", "错误",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Error);
}
}
- catch (Exception ex)
- {
- IsCalculating = false;
- LogManager.Error($"重新计算时间分段失败: {ex.Message}", ex);
- }
}
- ///
- /// 从计算结果更新时间分段数据
- ///
- /// 计算结果
- private void UpdateTimeSegmentsFromCalculation(TimeMarkerCalculationService.TimeMarkerResult result)
- {
- try
- {
- // 清空现有数据
- TimeSegments.Clear();
-
- // 添加路径分段数据
- for (int i = 0; i < result.Segments.Count; i++)
- {
- var segment = result.Segments[i];
- var segmentItem = new TimeSegmentItem
- {
- Index = i + 1,
- Length = segment.Distance,
- Speed = UseElementSpeedLimit ? segment.SpeedLimit : DefaultSpeed,
- Duration = UseElementSpeedLimit ? segment.Time : (segment.Distance / DefaultSpeed),
- CumulativeTime = 0, // 稍后重新计算累计时间
- Remarks = segment.IsValid ? $"{segment.StartPointName}->{segment.EndPointName}" : "无限速"
- };
- TimeSegments.Add(segmentItem);
- }
-
- // 重新计算累计时间
- double cumulativeTime = 0;
- foreach (var segment in TimeSegments.Where(s => s.Index > 0))
- {
- cumulativeTime += segment.Duration;
- segment.CumulativeTime = cumulativeTime;
- }
-
- // 更新总时长(使用实际计算时间,不添加额外等待)
- TotalDuration = cumulativeTime;
-
- // 重新生成时间轴预览
- GenerateTimelineItems();
-
- LogManager.Info($"时间分段数据更新完成: 总时长 {TotalDuration:F1}秒");
- }
- catch (Exception ex)
- {
- LogManager.Error($"更新时间分段数据失败: {ex.Message}", ex);
- }
- }
-
- ///
- /// 应用时标到路径点
- ///
- public void ApplyTimeTags()
- {
- try
- {
- LogManager.Info("开始应用时标到路径点");
-
- // 模拟应用过程
- foreach (var segment in TimeSegments)
- {
- LogManager.Info($"应用时标 - 段{segment.Index}: 累计时间 {segment.CumulativeTime:F1}秒");
- }
-
- LogManager.Info("时标应用完成");
- }
- catch (Exception ex)
- {
- LogManager.Error($"应用时标失败: {ex.Message}", ex);
- }
- }
-
- ///
- /// 导出时标数据
- ///
- public void ExportTimeTags()
- {
- try
- {
- LogManager.Info("开始导出时标数据");
-
- // 模拟导出过程
- var exportData = TimeSegments.Select(s => new
- {
- Index = s.Index,
- Length = s.Length,
- Speed = s.Speed,
- Duration = s.Duration,
- CumulativeTime = s.CumulativeTime,
- Remarks = s.Remarks
- }).ToList();
-
- LogManager.Info($"时标数据导出完成,共{exportData.Count}个分段");
- }
- catch (Exception ex)
- {
- LogManager.Error($"导出时标数据失败: {ex.Message}", ex);
- }
- }
-
- ///
- /// 重置参数 - 从配置读取默认值
- ///
- public void ResetParameters()
- {
- try
- {
- var config = ConfigManager.Instance.Current;
- DefaultSpeed = config.Logistics.SpeedLimitMetersPerSecond;
- UseElementSpeedLimit = true;
- LogManager.Info($"参数重置完成 - 默认速度: {DefaultSpeed:F1}m/s(来自配置)");
- }
- catch (Exception ex)
- {
- LogManager.Error($"重置参数失败: {ex.Message}", ex);
- }
- }
-
- #endregion
-
- #region INotifyPropertyChanged实现
+ #region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
@@ -510,98 +528,5 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
#endregion
-
- #region 资源清理
-
- ///
- /// 释放资源
- ///
- public void Dispose()
- {
- UnsubscribeFromConfigChanges();
- }
-
- #endregion
}
-
- #region 数据模型
-
- ///
- /// 时间分段项目
- ///
- public class TimeSegmentItem : INotifyPropertyChanged
- {
- private double _speed;
- private double _duration;
- private double _cumulativeTime;
-
- /// 分段序号
- public int Index { get; set; }
-
- /// 分段长度(米)
- public double Length { get; set; }
-
- /// 分段速度(m/s)
- public double Speed
- {
- get => _speed;
- set
- {
- _speed = value;
- OnPropertyChanged();
- }
- }
-
- /// 分段耗时(秒)
- public double Duration
- {
- get => _duration;
- set
- {
- _duration = value;
- OnPropertyChanged();
- }
- }
-
- /// 累计时间(秒)
- public double CumulativeTime
- {
- get => _cumulativeTime;
- set
- {
- _cumulativeTime = value;
- OnPropertyChanged();
- }
- }
-
- /// 备注
- public string Remarks { get; set; }
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
- }
-
- ///
- /// 时间轴预览项目
- ///
- public class TimelinePreviewItem
- {
- /// 分段名称
- public string Name { get; set; }
-
- /// 持续时间(秒)
- public double Duration { get; set; }
-
- /// 颜色
- public string Color { get; set; }
-
- /// 百分比
- public double Percentage { get; set; }
- }
-
- #endregion
-}
\ No newline at end of file
+}
diff --git a/src/UI/WPF/Views/AboutDialog.xaml b/src/UI/WPF/Views/AboutDialog.xaml
index 510ccb5..27edb48 100644
--- a/src/UI/WPF/Views/AboutDialog.xaml
+++ b/src/UI/WPF/Views/AboutDialog.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -118,4 +118,4 @@ NavisworksTransport 关于对话框 - 采用与主界面一致的Navisworks 2026
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/AerialHeightDialog.xaml b/src/UI/WPF/Views/AerialHeightDialog.xaml
index ff29d5f..02ba713 100644
--- a/src/UI/WPF/Views/AerialHeightDialog.xaml
+++ b/src/UI/WPF/Views/AerialHeightDialog.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -69,4 +69,4 @@
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/AnimationControlView.xaml b/src/UI/WPF/Views/AnimationControlView.xaml
index ffa09a5..37dc9c6 100644
--- a/src/UI/WPF/Views/AnimationControlView.xaml
+++ b/src/UI/WPF/Views/AnimationControlView.xaml
@@ -1,4 +1,4 @@
-
@@ -659,4 +659,4 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/BatchTaskManagementView.xaml b/src/UI/WPF/Views/BatchTaskManagementView.xaml
index 7782fb5..d5a870e 100644
--- a/src/UI/WPF/Views/BatchTaskManagementView.xaml
+++ b/src/UI/WPF/Views/BatchTaskManagementView.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -153,4 +153,4 @@ NavisworksTransport 批处理队列管理页签视图 - 采用与其他页签一
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/CollisionAnalysisDialog.xaml b/src/UI/WPF/Views/CollisionAnalysisDialog.xaml
index bc07197..afa7233 100644
--- a/src/UI/WPF/Views/CollisionAnalysisDialog.xaml
+++ b/src/UI/WPF/Views/CollisionAnalysisDialog.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -194,3 +194,4 @@
+
diff --git a/src/UI/WPF/Views/CollisionReportDialog.xaml b/src/UI/WPF/Views/CollisionReportDialog.xaml
index 86483ed..0ed7187 100644
--- a/src/UI/WPF/Views/CollisionReportDialog.xaml
+++ b/src/UI/WPF/Views/CollisionReportDialog.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -634,4 +634,4 @@ NavisworksTransport 碰撞检测报告对话框 - 采用与主界面一致的Nav
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/ConfigEditorDialog.xaml b/src/UI/WPF/Views/ConfigEditorDialog.xaml
index a8b8b8d..57f8d2b 100644
--- a/src/UI/WPF/Views/ConfigEditorDialog.xaml
+++ b/src/UI/WPF/Views/ConfigEditorDialog.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -212,3 +212,4 @@ NavisworksTransport 配置编辑器对话框 - 采用与主界面一致的Navisw
+
diff --git a/src/UI/WPF/Views/CoordinateSystemResultDialog.xaml b/src/UI/WPF/Views/CoordinateSystemResultDialog.xaml
index c39fe65..7c8b254 100644
--- a/src/UI/WPF/Views/CoordinateSystemResultDialog.xaml
+++ b/src/UI/WPF/Views/CoordinateSystemResultDialog.xaml
@@ -1,4 +1,4 @@
-
+
diff --git a/src/UI/WPF/Views/EditCoordinatesWindow.xaml b/src/UI/WPF/Views/EditCoordinatesWindow.xaml
index 680eb68..d952326 100644
--- a/src/UI/WPF/Views/EditCoordinatesWindow.xaml
+++ b/src/UI/WPF/Views/EditCoordinatesWindow.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -137,3 +137,4 @@
+
diff --git a/src/UI/WPF/Views/EditRotationWindow.xaml b/src/UI/WPF/Views/EditRotationWindow.xaml
index 8da4210..f23922e 100644
--- a/src/UI/WPF/Views/EditRotationWindow.xaml
+++ b/src/UI/WPF/Views/EditRotationWindow.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -109,4 +109,4 @@
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/GenerateNavigationMapDialog.xaml b/src/UI/WPF/Views/GenerateNavigationMapDialog.xaml
index 492a87c..209846f 100644
--- a/src/UI/WPF/Views/GenerateNavigationMapDialog.xaml
+++ b/src/UI/WPF/Views/GenerateNavigationMapDialog.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -128,4 +128,4 @@ NavisworksTransport 生成导航地图对话框 - 采用与主界面一致的Nav
Click="CancelButton_Click"/>
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/HelpDialog.xaml b/src/UI/WPF/Views/HelpDialog.xaml
index 40ab687..2126724 100644
--- a/src/UI/WPF/Views/HelpDialog.xaml
+++ b/src/UI/WPF/Views/HelpDialog.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -144,4 +144,4 @@ NavisworksTransport 帮助对话框 - 采用与主界面一致的Navisworks 2026
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/LayerManagementView.xaml b/src/UI/WPF/Views/LayerManagementView.xaml
index 93f75f6..d0d0b80 100644
--- a/src/UI/WPF/Views/LayerManagementView.xaml
+++ b/src/UI/WPF/Views/LayerManagementView.xaml
@@ -1,4 +1,4 @@
-
@@ -328,4 +328,4 @@ NavisworksTransport 分层管理页签视图 - 重构优化版本
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/LogViewerDialog.xaml b/src/UI/WPF/Views/LogViewerDialog.xaml
index d1daaef..68e89e1 100644
--- a/src/UI/WPF/Views/LogViewerDialog.xaml
+++ b/src/UI/WPF/Views/LogViewerDialog.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -167,4 +167,4 @@ NavisworksTransport 日志查看器对话框 - 采用与主界面一致的Navisw
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/LogisticsControlPanel.xaml b/src/UI/WPF/Views/LogisticsControlPanel.xaml
index f4f7378..c9b1539 100644
--- a/src/UI/WPF/Views/LogisticsControlPanel.xaml
+++ b/src/UI/WPF/Views/LogisticsControlPanel.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -155,4 +155,4 @@ NavisworksTransport 主控制面板 - 采用与其他视图一致的Navisworks 2
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/MediaControlBar.xaml b/src/UI/WPF/Views/MediaControlBar.xaml
index e8a2b0a..b95aa01 100644
--- a/src/UI/WPF/Views/MediaControlBar.xaml
+++ b/src/UI/WPF/Views/MediaControlBar.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -208,4 +208,4 @@
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/ModelItemBoundsWindow.xaml b/src/UI/WPF/Views/ModelItemBoundsWindow.xaml
index ed353c8..2a8ff5d 100644
--- a/src/UI/WPF/Views/ModelItemBoundsWindow.xaml
+++ b/src/UI/WPF/Views/ModelItemBoundsWindow.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -265,3 +265,4 @@
+
diff --git a/src/UI/WPF/Views/ModelSettingsView.xaml b/src/UI/WPF/Views/ModelSettingsView.xaml
index 74b8604..cc63574 100644
--- a/src/UI/WPF/Views/ModelSettingsView.xaml
+++ b/src/UI/WPF/Views/ModelSettingsView.xaml
@@ -1,4 +1,4 @@
-
@@ -265,4 +265,4 @@ NavisworksTransport 类别设置页签视图 - 参考分层管理页签的设计
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/PathAnalysisDialog.xaml b/src/UI/WPF/Views/PathAnalysisDialog.xaml
index 3a19809..cca5413 100644
--- a/src/UI/WPF/Views/PathAnalysisDialog.xaml
+++ b/src/UI/WPF/Views/PathAnalysisDialog.xaml
@@ -1,4 +1,4 @@
-
@@ -22,7 +22,7 @@ NavisworksTransport 路径规划分析对话框 V2
-
+
@@ -329,3 +329,4 @@ NavisworksTransport 路径规划分析对话框 V2
+
diff --git a/src/UI/WPF/Views/PathConfigDialog.xaml b/src/UI/WPF/Views/PathConfigDialog.xaml
index e72acab..58d23fd 100644
--- a/src/UI/WPF/Views/PathConfigDialog.xaml
+++ b/src/UI/WPF/Views/PathConfigDialog.xaml
@@ -1,4 +1,4 @@
-
@@ -501,4 +501,4 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/PathEditingView.xaml.cs b/src/UI/WPF/Views/PathEditingView.xaml.cs
index 2b789a7..5d6c001 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml.cs
+++ b/src/UI/WPF/Views/PathEditingView.xaml.cs
@@ -4,6 +4,8 @@ using System.Windows;
using System.Windows.Controls;
using NavisworksTransport.UI.WPF.ViewModels;
using NavisworksTransport.UI.WPF.Views;
+using NavisworksTransport.Core.Services;
+using NavisworksTransport.Core.Models;
namespace NavisworksTransport.UI.WPF.Views
{
@@ -17,6 +19,11 @@ namespace NavisworksTransport.UI.WPF.Views
/// ViewModel属性,用于外部访问
///
public PathEditingViewModel ViewModel { get; private set; }
+
+ ///
+ /// 时标服务实例
+ ///
+ private TimeTagService _timeTagService;
///
/// 构造函数,需要传入主ViewModel以支持统一状态栏
@@ -52,43 +59,64 @@ namespace NavisworksTransport.UI.WPF.Views
}
///
- /// 路径列表中“时标”按钮点击,打开时间标签窗口(仅展示)
+ /// 路径列表中"时标"按钮点击,打开时间标签窗口
///
private void OnTimeTagButtonClick(object sender, RoutedEventArgs e)
{
try
{
- LogManager.Info("时标按钮被点击,准备打开TimeTagDialog窗口");
+ LogManager.Info("时标按钮被点击");
- var dlg = new TimeTagDialog();
+ // 从按钮的DataContext获取当前路径ViewModel
+ if (!(sender is Button button) || !(button.DataContext is PathRouteViewModel routeVm))
+ {
+ LogManager.Warning("无法获取当前路径信息");
+ return;
+ }
+
+ // 获取底层的PathRoute
+ var route = routeVm.Route;
+ if (route == null)
+ {
+ LogManager.Warning("PathRouteViewModel中的Route为空");
+ return;
+ }
- // 在Navisworks环境中,尝试找到合适的父窗口
+ // 确保服务已初始化
+ if (_timeTagService == null)
+ {
+ var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
+ var db = new PathDatabase(doc.FileName);
+ _timeTagService = new TimeTagService(db);
+ LogManager.Info("时标服务已初始化");
+ }
+
+ // 创建并显示时标对话框
+ var dlg = new TimeTagDialog(_timeTagService, route);
+
+ // 设置父窗口
try
{
- // 尝试设置Owner为当前WPF窗口的顶级父窗口
var parentWindow = Window.GetWindow(this);
if (parentWindow != null)
{
dlg.Owner = parentWindow;
- LogManager.Info("设置TimeTagDialog的Owner为当前窗口");
- }
- else
- {
- LogManager.Info("未找到父窗口,将以独立窗口方式显示TimeTagDialog");
}
}
catch (Exception ownerEx)
{
- LogManager.Warning($"设置Owner失败,将以独立窗口显示: {ownerEx.Message}");
+ LogManager.Warning($"设置Owner失败: {ownerEx.Message}");
}
dlg.ShowDialog();
- LogManager.Info("TimeTagDialog窗口显示完成");
+ LogManager.Info($"时标对话框关闭: {route.Name}");
}
catch (Exception ex)
{
LogManager.Error($"打开时间标签窗口失败: {ex.Message}");
LogManager.Error($"异常详情: {ex}");
+ MessageBox.Show($"打开时标窗口失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ MessageBox.Show($"打开时标窗口失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
diff --git a/src/UI/WPF/Views/PathSelectionDialog.xaml b/src/UI/WPF/Views/PathSelectionDialog.xaml
index 80ca164..ec900fe 100644
--- a/src/UI/WPF/Views/PathSelectionDialog.xaml
+++ b/src/UI/WPF/Views/PathSelectionDialog.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -301,4 +301,4 @@ NavisworksTransport 系统管理页签视图 - 采用与其他页签一致的Nav
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/TimeTagDialog.xaml b/src/UI/WPF/Views/TimeTagDialog.xaml
index 65b3bb9..04ddd7c 100644
--- a/src/UI/WPF/Views/TimeTagDialog.xaml
+++ b/src/UI/WPF/Views/TimeTagDialog.xaml
@@ -1,252 +1,211 @@
-
-
+ ResizeMode="CanResize"
+ x:Name="root">
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
-
-
-
-
-
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AlternatingRowBackground="#FFF0F8FF"
+ HorizontalScrollBarVisibility="Auto"
+ VerticalScrollBarVisibility="Auto"
+ HeadersVisibility="Column"
+ RowHeaderWidth="0">
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/UI/WPF/Views/TimeTagDialog.xaml.cs b/src/UI/WPF/Views/TimeTagDialog.xaml.cs
index ac74f36..8899dc2 100644
--- a/src/UI/WPF/Views/TimeTagDialog.xaml.cs
+++ b/src/UI/WPF/Views/TimeTagDialog.xaml.cs
@@ -1,75 +1,90 @@
using System;
+using System.Globalization;
using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Input;
+using NavisworksTransport.Core.Services;
using NavisworksTransport.UI.WPF.ViewModels;
+using NavisworksTransport.Core.Models;
namespace NavisworksTransport.UI.WPF.Views
{
///
- /// TimeTagDialog.xaml 的交互逻辑
- /// 时间标签设置对话框 - 用于路径时间分段计算和标签设置
+ /// 时标设置对话框
///
public partial class TimeTagDialog : Window
{
- ///
- /// 初始化时间标签设置对话框
- ///
- public TimeTagDialog()
+ private TimeTagViewModel _viewModel;
+
+ public TimeTagDialog(TimeTagService timeTagService, PathRoute route)
{
+ InitializeComponent();
+
try
{
- InitializeComponent();
- LogManager.Info("TimeTagDialog初始化完成");
-
- // 设置窗口标题包含当前时间
- Title = $"时间标签设置 [{DateTime.Now:MM-dd HH:mm:ss}]";
-
- // 设置ViewModel
- this.DataContext = new TimeTagViewModel();
-
- LogManager.Info("TimeTagDialog ViewModel设置完成");
+ _viewModel = new TimeTagViewModel(timeTagService, route);
+ DataContext = _viewModel;
}
catch (Exception ex)
{
- LogManager.Error($"初始化TimeTagDialog失败: {ex.Message}", ex);
+ LogManager.Error($"时标对话框初始化失败: {ex.Message}");
+ MessageBox.Show($"初始化失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ throw;
}
}
-
-
+
///
- /// 关闭按钮点击事件
+ /// 关闭按钮
///
- ///
- ///
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
- try
- {
- LogManager.Info("用户关闭时间标签设置对话框");
- this.Close();
- }
- catch (Exception ex)
- {
- LogManager.Error($"关闭TimeTagDialog时发生错误: {ex.Message}", ex);
- }
+ Close();
}
-
+
///
- /// 窗口关闭时的清理工作
+ /// 距离滑块预览鼠标左键按下时开始拖动
///
- ///
- protected override void OnClosed(EventArgs e)
+ private void DistanceSlider_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
- try
+ // 开始拖动,此时不更新源
+ }
+
+ ///
+ /// 距离滑块预览鼠标左键松开时结束拖动并更新源
+ ///
+ private void DistanceSlider_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ if (sender is Slider slider)
{
- // 执行清理工作(如果需要)
- LogManager.Info("TimeTagDialog已关闭,执行清理");
-
- base.OnClosed(e);
- }
- catch (Exception ex)
- {
- LogManager.Error($"TimeTagDialog清理过程中发生错误: {ex.Message}", ex);
+ // 鼠标松开时更新绑定源,触发重新排序
+ var binding = slider.GetBindingExpression(Slider.ValueProperty);
+ binding?.UpdateSource();
}
}
}
-}
\ No newline at end of file
+
+ ///
+ /// 反向布尔到可见性转换器(false时Visible,true时Collapsed)
+ ///
+ public class InverseBooleanToVisibilityConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool boolValue)
+ {
+ return boolValue ? Visibility.Collapsed : Visibility.Visible;
+ }
+ return Visibility.Collapsed;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is Visibility visibility)
+ {
+ return visibility != Visibility.Visible;
+ }
+ return false;
+ }
+ }
+}