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"> - - + - - - - - - - - - - - - - - - - - + + + - - + + + + + - - - + + + + + + + + + + + + + + + + + +