实现完整的时间标签功能

This commit is contained in:
tian 2026-02-16 19:46:38 +08:00
parent 178aa995ff
commit 57751e85fa
38 changed files with 2780 additions and 875 deletions

View File

@ -120,6 +120,7 @@
<Compile Include="src\Core\Models\PathAnalysisModels.cs" />
<Compile Include="src\Core\Models\BatchQueueItem.cs" />
<Compile Include="src\Core\Models\CollisionDetectionConfig.cs" />
<Compile Include="src\Core\Models\TimeTagModels.cs" />
<Compile Include="src\Core\PathCurveEngine.cs" />
<Compile Include="src\Core\GridVisualization.cs" />
<Compile Include="src\Core\BatchQueueManager.cs" />
@ -140,6 +141,10 @@
<Compile Include="src\Core\Config\SystemConfig.cs" />
<Compile Include="src\Core\Config\ConfigManager.cs" />
<Compile Include="src\Core\Database\BackupManager.cs" />
<Compile Include="src\Core\Services\TimeTagCalculator.cs" />
<Compile Include="src\Core\Services\TimeTagService.cs" />
<Compile Include="src\Core\Services\TimeTagExporter.cs" />
<Compile Include="src\Core\Services\TimeTagTimeLinerIntegration.cs" />
<!-- Commands - Command Pattern Framework (for testing) -->
<Compile Include="src\Commands\IPathPlanningCommand.cs" />
<Compile Include="src\Commands\CommandBase.cs" />
@ -236,6 +241,9 @@
<Compile Include="src\UI\WPF\Views\CollisionReportDialog.xaml.cs">
<DependentUpon>CollisionReportDialog.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Controls\EventTimelineControl.xaml.cs">
<DependentUpon>EventTimelineControl.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\TimeTagDialog.xaml.cs">
<DependentUpon>TimeTagDialog.xaml</DependentUpon>
</Compile>
@ -392,6 +400,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Controls\EventTimelineControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\TimeTagDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>

View File

@ -6,6 +6,7 @@
1. [x] (优化)实现完整的路径分析功能
2. [x] (功能)用物流分类属性筛选对应的物流元素
3. [x] (优化)实现完整的时间标签功能
### [2026/2/8]

View File

@ -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> (事件节点列表)
```
### 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
}
]
}
```

View File

@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
namespace NavisworksTransport.Core.Models
{
/// <summary>
/// 时标配置 - 一套完整的时间标签方案
/// </summary>
public class TimeTagProfile
{
/// <summary>
/// 配置ID
/// </summary>
public string Id { get; set; }
/// <summary>
/// 关联路径ID
/// </summary>
public string RouteId { get; set; }
/// <summary>
/// 配置名称(如:叉车满载)
/// </summary>
public string Name { get; set; }
/// <summary>
/// 默认速度 m/s
/// </summary>
public double DefaultSpeed { get; set; }
/// <summary>
/// 转弯速度比例0.5=半速)
/// </summary>
public double TurnSpeedRatio { get; set; }
/// <summary>
/// 事件节点列表
/// </summary>
public List<TimeTagEvent> Events { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 总时间(从最后一个事件获取)
/// </summary>
public double TotalTime => Events?.Count > 0 ? Events[Events.Count - 1].CumulativeTime : 0;
public TimeTagProfile()
{
Id = Guid.NewGuid().ToString();
Events = new List<TimeTagEvent>();
DefaultSpeed = 1.0;
TurnSpeedRatio = 0.5;
CreatedAt = DateTime.Now;
}
}
/// <summary>
/// 时间事件节点
/// </summary>
public class TimeTagEvent
{
/// <summary>
/// 事件ID
/// </summary>
public string Id { get; set; }
/// <summary>
/// 所属配置ID
/// </summary>
public string ProfileId { get; set; }
/// <summary>
/// 关联路径点ID可选
/// </summary>
public string PointId { get; set; }
/// <summary>
/// 距起点距离(米)
/// </summary>
public double Distance { get; set; }
/// <summary>
/// 事件类型
/// </summary>
public EventType Type { get; set; }
/// <summary>
/// 事件名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 等待时间(秒)
/// </summary>
public double WaitTime { get; set; }
/// <summary>
/// 动作时间(秒)
/// </summary>
public double ActionTime { get; set; }
/// <summary>
/// 累计时间(自动计算)
/// </summary>
public double CumulativeTime { get; set; }
/// <summary>
/// 到达速度限制(可选)
/// </summary>
public double? ArriveSpeed { get; set; }
/// <summary>
/// 离开速度限制(可选)
/// </summary>
public double? LeaveSpeed { get; set; }
/// <summary>
/// 排序序号
/// </summary>
public int SortOrder { get; set; }
public TimeTagEvent()
{
Id = Guid.NewGuid().ToString();
Type = EventType.Pass;
Name = "途经点";
}
}
/// <summary>
/// 事件类型枚举
/// </summary>
public enum EventType
{
/// <summary>
/// 起点
/// </summary>
Start,
/// <summary>
/// 途经点
/// </summary>
Pass,
/// <summary>
/// 转弯
/// </summary>
Turn,
/// <summary>
/// 等待
/// </summary>
Wait,
/// <summary>
/// 装货
/// </summary>
Load,
/// <summary>
/// 卸货
/// </summary>
Unload,
/// <summary>
/// 终点
/// </summary>
End
}
}

View File

@ -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
/// <summary>
/// 保存时标配置
/// </summary>
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;
}
}
/// <summary>
/// 获取路径的所有时标配置
/// </summary>
public List<TimeTagProfile> GetTimeTagProfiles(string routeId)
{
var profiles = new List<TimeTagProfile>();
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;
}
/// <summary>
/// 根据ID获取时标配置
/// </summary>
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<TimeTagEvent>();
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);
}
}
}
}
/// <summary>
/// 删除时标配置
/// </summary>
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
}
/// <summary>

View File

@ -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
{
/// <summary>
/// 时标计算器
/// 基于事件节点和路径几何计算时间
/// </summary>
public class TimeTagCalculator
{
/// <summary>
/// 计算时标配置的所有时间
/// </summary>
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}");
}
/// <summary>
/// 计算路段速度(简化模型)
/// </summary>
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); // 最小速度限制
}
/// <summary>
/// 检查路段是否包含圆弧
/// </summary>
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;
}
/// <summary>
/// 从路径自动生成事件节点
/// </summary>
public List<TimeTagEvent> GenerateDefaultEvents(PathRoute route)
{
var events = new List<TimeTagEvent>();
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;
}
/// <summary>
/// 判断指定索引的点是否为转弯点
/// </summary>
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;
}
/// <summary>
/// 计算两点间距离(米)
/// </summary>
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);
}
}
}

View File

@ -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
{
/// <summary>
/// 时标数据导出器 - 支持JSON和CSV格式
/// </summary>
public class TimeTagExporter
{
/// <summary>
/// 导出配置为JSON格式
/// </summary>
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();
}
/// <summary>
/// 导出配置为CSV格式
/// </summary>
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();
}
/// <summary>
/// 导出到文件
/// </summary>
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);
}
/// <summary>
/// 批量导出多个配置
/// </summary>
public string ExportMultipleToJson(List<TimeTagProfile> 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();
}
/// <summary>
/// 转义JSON字符串
/// </summary>
private string EscapeJson(string str)
{
if (string.IsNullOrEmpty(str)) return "";
return str.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\n", "\\n")
.Replace("\r", "\\r")
.Replace("\t", "\\t");
}
/// <summary>
/// 转义CSV字符串
/// </summary>
private string EscapeCsv(string str)
{
if (string.IsNullOrEmpty(str)) return "";
if (str.Contains(",") || str.Contains("\"") || str.Contains("\n"))
{
return "\"" + str.Replace("\"", "\"\"") + "\"";
}
return str;
}
/// <summary>
/// 获取事件说明
/// </summary>
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 "";
}
}
}
}

View File

@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NavisworksTransport.Core.Models;
namespace NavisworksTransport.Core.Services
{
/// <summary>
/// 时标服务
/// 整合时标的CRUD操作和计算
/// </summary>
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();
}
/// <summary>
/// 为路径创建新的时标配置
/// </summary>
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;
}
/// <summary>
/// 获取路径的所有时标配置
/// </summary>
public List<TimeTagProfile> GetProfiles(string routeId)
{
return _database.GetTimeTagProfiles(routeId);
}
/// <summary>
/// 获取单个时标配置
/// </summary>
public TimeTagProfile GetProfile(string profileId)
{
return _database.GetTimeTagProfile(profileId);
}
/// <summary>
/// 更新时标配置(并重新计算时间)
/// </summary>
public void UpdateProfile(TimeTagProfile profile, PathRoute route)
{
if (profile == null) return;
// 重新计算时间
_calculator.Calculate(profile, route);
// 保存
_database.SaveTimeTagProfile(profile);
}
/// <summary>
/// 删除时标配置
/// </summary>
public void DeleteProfile(string profileId)
{
_database.DeleteTimeTagProfile(profileId);
}
/// <summary>
/// 添加事件到配置
/// </summary>
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);
}
/// <summary>
/// 从配置中移除事件
/// </summary>
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);
}
}
/// <summary>
/// 更新事件属性
/// </summary>
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);
}
}
/// <summary>
/// 复制时标配置
/// </summary>
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;
}
}
}

View File

@ -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
{
/// <summary>
/// 时标系统与TimeLiner集成 - 将时标数据转换为TimeLiner任务
/// </summary>
public class TimeTagTimeLinerIntegration
{
private readonly TimeTagService _timeTagService;
public TimeTagTimeLinerIntegration(TimeTagService timeTagService)
{
_timeTagService = timeTagService ?? throw new ArgumentNullException(nameof(timeTagService));
}
/// <summary>
/// 将时标配置导入为TimeLiner仿真任务
/// </summary>
/// <param name="profile">时标配置</param>
/// <param name="route">路径</param>
/// <param name="taskName">任务名称</param>
/// <returns>是否成功</returns>
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;
}
}
/// <summary>
/// 创建仿真数据 - 用于动画系统
/// </summary>
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<SimulationWaypoint>()
};
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;
}
/// <summary>
/// 获取指定距离处的路径点(插值)
/// </summary>
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];
}
/// <summary>
/// 计算两点距离
/// </summary>
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);
}
/// <summary>
/// 线性插值两个路径点
/// </summary>
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
)
};
}
/// <summary>
/// 计算每段的速度
/// </summary>
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;
}
}
}
/// <summary>
/// 生成仿真脚本(用于外部系统)
/// </summary>
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();
}
/// <summary>
/// 导出仿真数据为JSON
/// </summary>
public string ExportSimulationJson(TimeTagProfile profile, PathRoute route)
{
var data = CreateSimulationData(profile, route);
return ExportSimulationDataToJson(data);
}
/// <summary>
/// 将仿真数据转换为JSON
/// </summary>
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();
}
/// <summary>
/// 转义JSON字符串
/// </summary>
private string EscapeJson(string str)
{
if (string.IsNullOrEmpty(str)) return "";
return str.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\n", "\\n")
.Replace("\r", "\\r")
.Replace("\t", "\\t");
}
}
/// <summary>
/// 仿真数据结构
/// </summary>
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<SimulationWaypoint> Waypoints { get; set; }
}
/// <summary>
/// 仿真路径点
/// </summary>
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; }
}
}

View File

@ -0,0 +1,48 @@
<UserControl x:Class="NavisworksTransport.UI.WPF.Controls.EventTimelineControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="120" d:DesignWidth="800"
Background="White">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标题 -->
<TextBlock Grid.Row="0" Text="事件时间线" FontWeight="Bold" Margin="5,2"/>
<!-- 时间线画布 -->
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<Canvas x:Name="TimelineCanvas" Height="80" MinWidth="600">
<!-- 基线 -->
<Line x:Name="BaseLine" Stroke="#FFCCCCCC" StrokeThickness="2"
X1="50" Y1="40" X2="550" Y2="40"/>
<!-- 事件节点将由代码动态添加 -->
</Canvas>
</ScrollViewer>
<!-- 图例 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="5,2">
<TextBlock Text="图例: " FontSize="10" Foreground="Gray"/>
<Ellipse Width="8" Height="8" Fill="#4CAF50" Margin="5,0"/>
<TextBlock Text="起点" FontSize="10" Margin="0,0,10,0"/>
<Ellipse Width="8" Height="8" Fill="#2196F3" Margin="5,0"/>
<TextBlock Text="途经" FontSize="10" Margin="0,0,10,0"/>
<Ellipse Width="8" Height="8" Fill="#FF9800" Margin="5,0"/>
<TextBlock Text="转弯" FontSize="10" Margin="0,0,10,0"/>
<Ellipse Width="8" Height="8" Fill="#9C27B0" Margin="5,0"/>
<TextBlock Text="等待" FontSize="10" Margin="0,0,10,0"/>
<Ellipse Width="8" Height="8" Fill="#F44336" Margin="5,0"/>
<TextBlock Text="装卸" FontSize="10" Margin="0,0,10,0"/>
<Ellipse Width="8" Height="8" Fill="#607D8B" Margin="5,0"/>
<TextBlock Text="终点" FontSize="10"/>
</StackPanel>
</Grid>
</UserControl>

View File

@ -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
{
/// <summary>
/// 事件时间线可视化控件
/// </summary>
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<TimeTagEvent>),
typeof(EventTimelineControl),
new PropertyMetadata(null, OnEventsChanged));
public IEnumerable<TimeTagEvent> Events
{
get => (IEnumerable<TimeTagEvent>)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();
}
/// <summary>
/// 附加集合变更监听
/// </summary>
private void AttachCollectionChanged()
{
if (Events is INotifyCollectionChanged notifyCollection)
{
notifyCollection.CollectionChanged += OnCollectionChanged;
}
}
/// <summary>
/// 移除集合变更监听
/// </summary>
private void DetachCollectionChanged(INotifyCollectionChanged oldCollection)
{
if (oldCollection != null)
{
oldCollection.CollectionChanged -= OnCollectionChanged;
}
}
/// <summary>
/// 集合内容变化时重绘
/// </summary>
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
/// <summary>
/// 获取实际可用的画布宽度
/// </summary>
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);
}
/// <summary>
/// 获取父级ScrollViewer
/// </summary>
private ScrollViewer GetParentScrollViewer()
{
DependencyObject parent = TimelineCanvas;
while (parent != null)
{
if (parent is ScrollViewer sv) return sv;
parent = VisualTreeHelper.GetParent(parent);
}
return null;
}
/// <summary>
/// 绘制时间线
/// </summary>
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();
}
/// <summary>
/// 清除动态元素
/// </summary>
private void ClearDynamicElements()
{
var toRemove = TimelineCanvas.Children.OfType<FrameworkElement>()
.Where(c => c != BaseLine)
.ToList();
foreach (var child in toRemove)
{
TimelineCanvas.Children.Remove(child);
}
}
/// <summary>
/// 仅绘制基线(无事件时)
/// </summary>
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);
}
/// <summary>
/// 更新基线位置
/// </summary>
private void UpdateBaseLine(double x1, double x2)
{
BaseLine.X1 = x1;
BaseLine.Y1 = NODE_Y;
BaseLine.X2 = x2;
BaseLine.Y2 = NODE_Y;
}
/// <summary>
/// 绘制单个事件节点
/// </summary>
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);
}
/// <summary>
/// 绘制事件标签
/// </summary>
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);
}
/// <summary>
/// 高亮选中事件
/// </summary>
private void HighlightSelectedEvent()
{
if (TimelineCanvas == null) return;
foreach (var ellipse in TimelineCanvas.Children.OfType<Ellipse>()
.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);
}
}
}
/// <summary>
/// 获取事件类型对应的颜色
/// </summary>
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;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 关于对话框 - 采用与主界面一致的Navisworks 2026风格
功能说明:
@ -24,7 +24,7 @@ NavisworksTransport 关于对话框 - 采用与主界面一致的Navisworks 2026
<!-- 引用共享的Navisworks 2026样式资源 -->
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
@ -118,4 +118,4 @@ NavisworksTransport 关于对话框 - 采用与主界面一致的Navisworks 2026
</StackPanel>
</Border>
</Grid>
</Window>
</Window>

View File

@ -1,4 +1,4 @@
<Window x:Class="NavisworksTransport.UI.WPF.Views.AerialHeightDialog"
<Window x:Class="NavisworksTransport.UI.WPF.Views.AerialHeightDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="设置吊装提升高度" Height="240" Width="400"
@ -10,7 +10,7 @@
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
@ -69,4 +69,4 @@
</StackPanel>
</Border>
</Grid>
</Window>
</Window>

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管理一致的Navisworks 2026风格
功能说明:
@ -22,7 +22,7 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 转换器资源 -->
@ -659,4 +659,4 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
</StackPanel>
</ScrollViewer>
</UserControl>
</UserControl>

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 批处理队列管理页签视图 - 采用与其他页签一致的Navisworks 2026风格
功能说明:
@ -22,7 +22,7 @@ NavisworksTransport 批处理队列管理页签视图 - 采用与其他页签一
<!-- 引用共享的Navisworks 2026样式资源 -->
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 转换器资源 -->
@ -153,4 +153,4 @@ NavisworksTransport 批处理队列管理页签视图 - 采用与其他页签一
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>
</UserControl>

View File

@ -1,4 +1,4 @@
<Window x:Class="NavisworksTransport.UI.WPF.Views.CollisionAnalysisDialog"
<Window x:Class="NavisworksTransport.UI.WPF.Views.CollisionAnalysisDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@ -13,7 +13,7 @@
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 提示框样式 -->
@ -194,3 +194,4 @@
</StackPanel>
</Grid>
</Window>

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 碰撞检测报告对话框 - 采用与主界面一致的Navisworks 2026风格
功能说明:
@ -28,7 +28,7 @@ NavisworksTransport 碰撞检测报告对话框 - 采用与主界面一致的Nav
<!-- 引用共享的Navisworks 2026样式资源 -->
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 转换器定义 -->
@ -634,4 +634,4 @@ NavisworksTransport 碰撞检测报告对话框 - 采用与主界面一致的Nav
</StackPanel>
</Border>
</Grid>
</Window>
</Window>

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 配置编辑器对话框 - 采用与主界面一致的Navisworks 2026风格
功能说明:
@ -28,7 +28,7 @@ NavisworksTransport 配置编辑器对话框 - 采用与主界面一致的Navisw
<!-- 引用共享的Navisworks 2026样式资源 -->
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 配置编辑器特有的样式 -->
@ -212,3 +212,4 @@ NavisworksTransport 配置编辑器对话框 - 采用与主界面一致的Navisw
</Border>
</Grid>
</Window>

View File

@ -1,4 +1,4 @@
<Window x:Class="NavisworksTransport.UI.WPF.Views.CoordinateSystemResultDialog"
<Window x:Class="NavisworksTransport.UI.WPF.Views.CoordinateSystemResultDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="坐标系探索结果"
@ -50,3 +50,4 @@
</StackPanel>
</Grid>
</Window>

View File

@ -1,4 +1,4 @@
<Window x:Class="NavisworksTransport.UI.WPF.Views.EditCoordinatesWindow"
<Window x:Class="NavisworksTransport.UI.WPF.Views.EditCoordinatesWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="编辑路径点坐标" Height="380" Width="400"
@ -10,7 +10,7 @@
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
@ -137,3 +137,4 @@
</Border>
</Grid>
</Window>

View File

@ -1,4 +1,4 @@
<Window x:Class="NavisworksTransport.UI.WPF.Views.EditRotationWindow"
<Window x:Class="NavisworksTransport.UI.WPF.Views.EditRotationWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="调整物体角度" Height="320" Width="400"
@ -10,7 +10,7 @@
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
@ -109,4 +109,4 @@
</StackPanel>
</Border>
</Grid>
</Window>
</Window>

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 生成导航地图对话框 - 采用与主界面一致的Navisworks 2026风格
功能说明:
@ -27,7 +27,7 @@ NavisworksTransport 生成导航地图对话框 - 采用与主界面一致的Nav
<!-- 引用共享的Navisworks 2026样式资源 -->
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 对话框专用样式 -->
@ -128,4 +128,4 @@ NavisworksTransport 生成导航地图对话框 - 采用与主界面一致的Nav
Click="CancelButton_Click"/>
</StackPanel>
</Grid>
</Window>
</Window>

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 帮助对话框 - 采用与主界面一致的Navisworks 2026风格
功能说明:
@ -24,7 +24,7 @@ NavisworksTransport 帮助对话框 - 采用与主界面一致的Navisworks 2026
<!-- 引用共享的Navisworks 2026样式资源 -->
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 帮助对话框专用样式 -->
@ -144,4 +144,4 @@ NavisworksTransport 帮助对话框 - 采用与主界面一致的Navisworks 2026
</StackPanel>
</Border>
</Grid>
</Window>
</Window>

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 分层管理页签视图 - 重构优化版本
功能优化:
1. 移除独立楼层分析区域,整合到预览功能中
@ -21,7 +21,7 @@ NavisworksTransport 分层管理页签视图 - 重构优化版本
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 转换器资源 -->
@ -328,4 +328,4 @@ NavisworksTransport 分层管理页签视图 - 重构优化版本
</StackPanel>
</ScrollViewer>
</UserControl>
</UserControl>

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 日志查看器对话框 - 采用与主界面一致的Navisworks 2026风格
功能说明:
@ -27,7 +27,7 @@ NavisworksTransport 日志查看器对话框 - 采用与主界面一致的Navisw
<!-- 引用共享的Navisworks 2026样式资源 -->
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 日志查看器特有的样式 -->
@ -167,4 +167,4 @@ NavisworksTransport 日志查看器对话框 - 采用与主界面一致的Navisw
</Grid>
</Border>
</Grid>
</Window>
</Window>

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 主控制面板 - 采用与其他视图一致的Navisworks 2026风格
功能说明:
@ -24,7 +24,7 @@ NavisworksTransport 主控制面板 - 采用与其他视图一致的Navisworks 2
<!-- 引用共享的Navisworks 2026样式资源 -->
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 转换器资源 -->
@ -155,4 +155,4 @@ NavisworksTransport 主控制面板 - 采用与其他视图一致的Navisworks 2
</Grid>
</Border>
</Grid>
</UserControl>
</UserControl>

View File

@ -1,4 +1,4 @@
<UserControl x:Class="NavisworksTransport.UI.WPF.Views.MediaControlBar"
<UserControl x:Class="NavisworksTransport.UI.WPF.Views.MediaControlBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@ -9,7 +9,7 @@
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/MediaControlIcons.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/MediaControlIcons.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
@ -208,4 +208,4 @@
</Grid>
</Grid>
</Border>
</UserControl>
</UserControl>

View File

@ -1,4 +1,4 @@
<Window x:Class="NavisworksTransport.UI.WPF.Views.ModelItemBoundsWindow"
<Window x:Class="NavisworksTransport.UI.WPF.Views.ModelItemBoundsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="元素包围盒信息" Height="460" Width="420"
@ -10,7 +10,7 @@
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
@ -265,3 +265,4 @@
</Border>
</Grid>
</Window>

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 类别设置页签视图 - 参考分层管理页签的设计风格
功能改进:
@ -22,7 +22,7 @@ NavisworksTransport 类别设置页签视图 - 参考分层管理页签的设计
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 转换器资源 -->
@ -265,4 +265,4 @@ NavisworksTransport 类别设置页签视图 - 参考分层管理页签的设计
</StackPanel>
</ScrollViewer>
</UserControl>
</UserControl>

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 路径规划分析对话框 V2
支持多维度评分和同终点组分析
-->
@ -22,7 +22,7 @@ NavisworksTransport 路径规划分析对话框 V2
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 颜色定义 -->
@ -329,3 +329,4 @@ NavisworksTransport 路径规划分析对话框 V2
</Border>
</Grid>
</Window>

View File

@ -1,4 +1,4 @@
<!--
<!--
路径配置对话框
功能说明:
@ -23,7 +23,7 @@
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
@ -179,4 +179,4 @@
Style="{StaticResource SecondaryButtonStyle}" Width="100"/>
</StackPanel>
</Grid>
</Window>
</Window>

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管理一致的Navisworks 2026风格
功能说明:
@ -21,7 +21,7 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 转换器资源 -->
@ -501,4 +501,4 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>
</UserControl>

View File

@ -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属性用于外部访问
/// </summary>
public PathEditingViewModel ViewModel { get; private set; }
/// <summary>
/// 时标服务实例
/// </summary>
private TimeTagService _timeTagService;
/// <summary>
/// 构造函数需要传入主ViewModel以支持统一状态栏
@ -52,43 +59,64 @@ namespace NavisworksTransport.UI.WPF.Views
}
/// <summary>
/// 路径列表中“时标”按钮点击,打开时间标签窗口(仅展示)
/// 路径列表中"时标"按钮点击,打开时间标签窗口
/// </summary>
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);
}
}

View File

@ -1,4 +1,4 @@
<!--
<!--
路径选择对话框
功能说明:
@ -21,7 +21,7 @@
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
@ -79,4 +79,4 @@
Style="{StaticResource SecondaryButtonStyle}" Width="100"/>
</StackPanel>
</Grid>
</Window>
</Window>

View File

@ -1,4 +1,4 @@
<!--
<!--
NavisworksTransport 系统管理页签视图 - 采用与其他页签一致的Navisworks 2026风格
功能说明:
@ -21,7 +21,7 @@ NavisworksTransport 系统管理页签视图 - 采用与其他页签一致的Nav
<!-- 引用共享的Navisworks 2026样式资源 -->
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 系统管理页面特有的样式 -->
@ -301,4 +301,4 @@ NavisworksTransport 系统管理页签视图 - 采用与其他页签一致的Nav
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>
</UserControl>

View File

@ -1,252 +1,211 @@
<!--
NavisworksTransport 时间标签设置对话框 - 采用与主界面一致的Navisworks 2026风格
功能说明:
1. 路径时间标签设置,支持分段时间计算和速度调整
2. 支持默认速度设置、元素限速优先配置
3. 提供估算、应用写回等操作功能
4. 可视化时间分段展示和累计时间计算
设计原则参考PathAnalysisDialog的现代化布局和样式使用统一的蓝色主题
-->
<Window x:Class="NavisworksTransport.UI.WPF.Views.TimeTagDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Title="时间标签设置 - Route_A1"
Height="700"
Width="950"
ResizeMode="CanResize"
xmlns:local="clr-namespace:NavisworksTransport.UI.WPF.Views"
xmlns:controls="clr-namespace:NavisworksTransport.UI.WPF.Controls"
Title="时标设置"
Height="750" Width="1100"
WindowStartupLocation="CenterOwner"
MinHeight="500"
MinWidth="700">
ResizeMode="CanResize"
x:Name="root">
<Window.Resources>
<!-- 引用共享的Navisworks 2026样式资源 -->
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 转换器定义 -->
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<!-- 时间标签专用样式 -->
<Style x:Key="SettingsCardStyle" TargetType="Border">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#FFD4E7FF"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Padding" Value="15"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="Gray" Direction="315" ShadowDepth="2" Opacity="0.3" BlurRadius="4"/>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SectionHeaderStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{StaticResource NavisworksPrimaryBrush}"/>
<Setter Property="Margin" Value="0,0,0,10"/>
</Style>
<Style x:Key="ParameterGroupStyle" TargetType="StackPanel">
<Setter Property="Orientation" Value="Horizontal"/>
<Setter Property="Margin" Value="0,5"/>
</Style>
<Style x:Key="ParameterLabelStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="11"/>
<Setter Property="Foreground" Value="#FF666666"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Width" Value="100"/>
</Style>
<Style x:Key="ParameterInputStyle" TargetType="TextBox">
<Setter Property="Width" Value="80"/>
<Setter Property="Height" Value="25"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="BorderBrush" Value="#FFCCCCCC"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="5,0"/>
</Style>
<Style x:Key="ResultValueStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="{StaticResource NavisworksPrimaryBrush}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<!-- 转换器 -->
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<local:InverseBooleanToVisibilityConverter x:Key="InverseBoolToVisibilityConverter"/>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 主内容区域 -->
<Grid Grid.Row="0" Margin="15">
<!-- 标题和路径信息 -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="路径: " FontWeight="Bold" FontSize="14"/>
<TextBlock Text="{Binding RouteName}" FontWeight="Bold" FontSize="14"/>
</StackPanel>
<!-- 事件时间线可视化 -->
<Border Grid.Row="1" BorderBrush="#FFDDDDDD" BorderThickness="1" Margin="0,0,0,10"
Height="140" IsEnabled="{Binding CanEdit}">
<controls:EventTimelineControl
Events="{Binding Events}"
TotalDistance="{Binding TotalDistanceValue}"
SelectedEvent="{Binding SelectedEvent, Mode=TwoWay}"/>
</Border>
<!-- 配置选择 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="时标配置: " VerticalAlignment="Center" Margin="0,0,5,0" FontWeight="SemiBold"/>
<TextBlock Text="时标配置: " VerticalAlignment="Center" Margin="0,0,5,0" FontWeight="SemiBold"/>
<ComboBox Width="200"
ItemsSource="{Binding Profiles}"
SelectedValue="{Binding SelectedProfileId}"
SelectedValuePath="Id"
DisplayMemberPath="Name"/>
<Button Content="新建配置" Margin="10,0,5,0" Padding="15,3" Command="{Binding CreateProfileCommand}"/>
<Button Content="删除配置" Margin="5,0,0,0" Padding="15,3" Command="{Binding DeleteProfileCommand}"/>
<Separator Margin="20,0" Style="{StaticResource {x:Static ToolBar.SeparatorStyleKey}}"/>
<Button Content="导出" Padding="15,3" Command="{Binding ExportCommand}"/>
</StackPanel>
<!-- 主内容区 -->
<Grid Grid.Row="3" IsEnabled="{Binding CanEdit}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="190"/>
<ColumnDefinition Width="2.5*"/>
<ColumnDefinition Width="260"/>
</Grid.ColumnDefinitions>
<!-- 左侧:设置面板 -->
<StackPanel Grid.Column="0">
<!-- 基本设置卡片 -->
<Border Style="{StaticResource SettingsCardStyle}">
<!-- 左侧:参数设置 -->
<StackPanel Grid.Column="0" Margin="0,0,10,0">
<GroupBox Header="全局参数" Padding="10">
<StackPanel>
<TextBlock Text="⚙️ 基本设置" Style="{StaticResource SectionHeaderStyle}"/>
<TextBlock Text="默认速度 (m/s):" Margin="0,0,0,5" FontWeight="SemiBold"/>
<Slider Minimum="0.1" Maximum="5"
Value="{Binding DefaultSpeed, Mode=TwoWay}"
TickFrequency="0.1" IsSnapToTickEnabled="True"/>
<TextBlock Text="{Binding DefaultSpeed, StringFormat={}{0:F1}}"
HorizontalAlignment="Center" Margin="0,2,0,15"/>
<TextBlock Text="转弯速度比例:" Margin="0,0,0,5" FontWeight="SemiBold"/>
<Slider Minimum="0.1" Maximum="1"
Value="{Binding TurnSpeedRatio, Mode=TwoWay}"
TickFrequency="0.1" IsSnapToTickEnabled="True"/>
<TextBlock Text="{Binding TurnSpeedRatio, StringFormat=P0}"
HorizontalAlignment="Center" Margin="0,2,0,15"/>
<Separator Margin="0,10"/>
<TextBlock Text="总距离:" FontWeight="Bold"/>
<TextBlock Text="{Binding TotalDistanceText}" Margin="0,0,0,10" FontSize="14"/>
<StackPanel Style="{StaticResource ParameterGroupStyle}">
<TextBlock Text="默认速度:" Style="{StaticResource ParameterLabelStyle}"/>
<TextBox Text="{Binding DefaultSpeed, StringFormat=0.0###}" Style="{StaticResource ParameterInputStyle}"/>
<TextBlock Text="m/s" FontSize="10" Foreground="#FF666666" VerticalAlignment="Center" Margin="5,0,0,0"/>
</StackPanel>
<CheckBox Content="使用元素限速优先" IsChecked="{Binding UseElementSpeedLimit}" Margin="0,10,0,5" FontSize="11"/>
<StackPanel Style="{StaticResource ParameterGroupStyle}" Margin="0,15,0,5">
<TextBlock Text="总时长:" Style="{StaticResource ParameterLabelStyle}"/>
<TextBlock Text="{Binding TotalDurationText}" Style="{StaticResource ResultValueStyle}"/>
</StackPanel>
<TextBlock Text="总时间:" FontWeight="Bold"/>
<TextBlock Text="{Binding TotalTimeText}" Foreground="Blue" FontSize="18" FontWeight="Bold"/>
</StackPanel>
</Border>
<!-- 操作按钮卡片 -->
<Border Style="{StaticResource SettingsCardStyle}">
<StackPanel>
<TextBlock Text="🔧 操作" Style="{StaticResource SectionHeaderStyle}"/>
<Button Content="重新估算"
Style="{StaticResource ActionButtonStyle}"
Height="32"
Margin="0,5"/>
<Button Content="应用写回"
Style="{StaticResource ActionButtonStyle}"
Height="32"
Background="#FF4CAF50"
BorderBrush="#FF4CAF50"
Margin="0,5"/>
<Button Content="导出时标"
Style="{StaticResource SecondaryButtonStyle}"
Height="32"
Margin="0,5"/>
</StackPanel>
</Border>
<!-- 说明卡片 -->
<Border Style="{StaticResource SettingsCardStyle}">
<StackPanel>
<TextBlock Text="💡 说明" Style="{StaticResource SectionHeaderStyle}"/>
<TextBlock Text="• 单位:米/秒 与 秒" FontSize="10" Margin="0,2" Foreground="#FF666666"/>
<TextBlock Text="• 点击「应用写回」将累计时标写入路径点" FontSize="10" Margin="0,2" Foreground="#FF666666" TextWrapping="Wrap"/>
<TextBlock Text="• 元素限速优先时使用模型中设置的最高速度" FontSize="10" Margin="0,2" Foreground="#FF666666" TextWrapping="Wrap"/>
<TextBlock Text="• 等待事件段时长可手动调整" FontSize="10" Margin="0,2" Foreground="#FF666666"/>
</StackPanel>
</Border>
</GroupBox>
</StackPanel>
<!-- 右侧:时间分段表格 -->
<Border Grid.Column="1" Style="{StaticResource SettingsCardStyle}">
<StackPanel>
<TextBlock Text="📊 路径时间分段" Style="{StaticResource SectionHeaderStyle}"/>
<!-- 现代化DataGrid -->
<DataGrid AutoGenerateColumns="False"
HeadersVisibility="Column"
CanUserReorderColumns="False"
CanUserResizeColumns="True"
CanUserSortColumns="False"
BorderBrush="#FFD4E7FF"
BorderThickness="1"
<!-- 中间:事件列表 -->
<GroupBox Grid.Column="1" Header="事件节点列表">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 列表工具栏 -->
<ToolBar Grid.Row="0" Margin="0,0,0,5">
<Button Content="插入事件" Command="{Binding InsertEventCommand}" Padding="8,2"
ToolTip="在选中事件后插入新事件,未选中则在起点后插入"/>
<Button Content="删除事件" Command="{Binding DeleteEventCommand}" Padding="8,2" Margin="5,0"
Background="#FFFFE6E6" ToolTip="删除选中的事件节点"/>
</ToolBar>
<!-- 事件表格 -->
<DataGrid Grid.Row="1"
ItemsSource="{Binding Events}"
SelectedItem="{Binding SelectedEvent}"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
SelectionMode="Single"
GridLinesVisibility="Horizontal"
HorizontalGridLinesBrush="#FFEEEEEE"
RowBackground="White"
AlternatingRowBackground="#FFF8FBFF"
Height="240"
ItemsSource="{Binding TimeSegments}">
AlternatingRowBackground="#FFF0F8FF"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
HeadersVisibility="Column"
RowHeaderWidth="0">
<DataGrid.Columns>
<DataGridTextColumn Header="#" Width="40" IsReadOnly="True" Binding="{Binding Index}"/>
<DataGridTextColumn Header="长度(m)" Width="80" IsReadOnly="True" Binding="{Binding Length, StringFormat=F2}"/>
<DataGridTextColumn Header="速度(m/s)" Width="90" Binding="{Binding Speed, StringFormat=F2}"/>
<DataGridTextColumn Header="段耗时(s)" Width="90" Binding="{Binding Duration, StringFormat=F1}"/>
<DataGridTextColumn Header="累计(s)" Width="80" IsReadOnly="True" Binding="{Binding CumulativeTime, StringFormat=F1}"/>
<DataGridTextColumn Header="备注" Width="*" IsReadOnly="True" Binding="{Binding Remarks}"/>
<DataGridTextColumn Header="序号" Binding="{Binding SortOrder}" Width="50" IsReadOnly="True"/>
<DataGridTextColumn Header="事件名称" Binding="{Binding Name}" Width="*" MinWidth="120"/>
<DataGridTextColumn Header="类型" Binding="{Binding Type}" Width="80" IsReadOnly="True"/>
<DataGridTextColumn Header="距离(m)" Binding="{Binding Distance, StringFormat=F2}" Width="85" IsReadOnly="True"/>
<DataGridTextColumn Header="等待(s)" Binding="{Binding WaitTime, StringFormat=F1}" Width="75" IsReadOnly="True"/>
<DataGridTextColumn Header="动作(s)" Binding="{Binding ActionTime, StringFormat=F1}" Width="75" IsReadOnly="True"/>
<DataGridTextColumn Header="累计时间(s)" Binding="{Binding CumulativeTime, StringFormat=F2}" Width="95" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
<!-- 时间轴可视化 -->
<TextBlock Text="📈 时间轴预览" Style="{StaticResource SectionHeaderStyle}" Margin="0,20,0,10"/>
<StackPanel>
<!-- 动态时间轴段绑定到TimelineItems集合 -->
<ItemsControl ItemsSource="{Binding TimelineItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#FFF0F8FF" BorderBrush="#FFB0D4F1" BorderThickness="1" CornerRadius="3" Padding="8" Margin="0,2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Name}" FontWeight="SemiBold" FontSize="10" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
<Rectangle Grid.Column="1" Height="8" Fill="{Binding Color}" Margin="5,0" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Text="{Binding Duration, StringFormat='{}{0:F1}s'}" FontSize="10" HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 总计绑定到ViewModel -->
<Border Background="#FFE8F5E8" BorderBrush="#FF4CAF50" BorderThickness="2" CornerRadius="4" Padding="10" Margin="0,10,0,0">
<Grid>
<TextBlock Text="总计用时" FontWeight="Bold" FontSize="12" HorizontalAlignment="Left" VerticalAlignment="Center"/>
<TextBlock Text="{Binding TotalDurationText}" FontWeight="Bold" FontSize="14" Foreground="#FF4CAF50" HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid>
</Border>
</StackPanel>
<!-- 提示 -->
<TextBlock Grid.Row="2" Text="提示:点击事件选中,在右侧编辑详情"
Foreground="Gray" FontStyle="Italic" Margin="0,5,0,0"/>
</Grid>
</GroupBox>
<!-- 右侧:事件详情编辑 -->
<GroupBox Grid.Column="2" Header="事件详情" IsEnabled="{Binding CanEditEvent}">
<StackPanel Margin="10">
<TextBlock Text="选中事件:" FontWeight="Bold" Margin="0,0,0,5"/>
<TextBlock Text="{Binding SelectedEvent.Name}" FontSize="16" Margin="0,0,0,15"
Foreground="#FF2B579A"/>
<TextBlock Text="事件类型:" FontWeight="SemiBold" Margin="0,0,0,3"/>
<!-- 事件类型可编辑时显示ComboBox -->
<ComboBox ItemsSource="{Binding DataContext.EventTypes, ElementName=root}"
SelectedItem="{Binding SelectedEventType, Mode=TwoWay}"
Margin="0,0,0,15"
Visibility="{Binding IsSelectedEventTypeEditable, Converter={StaticResource BoolToVisibilityConverter}}"/>
<!-- 事件类型不可编辑时显示只读文本 -->
<TextBlock Text="{Binding SelectedEventType}"
FontSize="14" Foreground="Gray" Margin="0,0,0,15"
Visibility="{Binding IsSelectedEventTypeEditable, Converter={StaticResource InverseBoolToVisibilityConverter}}"/>
<TextBlock Text="距离 (米):" FontWeight="SemiBold" Margin="0,0,0,3"/>
<!-- 距离可编辑时显示Slider和输入框 -->
<Slider x:Name="DistanceSlider"
Minimum="0"
Maximum="{Binding TotalDistanceValue, FallbackValue=100}"
Value="{Binding SelectedDistance, Mode=TwoWay, UpdateSourceTrigger=Explicit}"
TickFrequency="1" IsSnapToTickEnabled="True"
Visibility="{Binding IsSelectedDistanceEditable, Converter={StaticResource BoolToVisibilityConverter}}"
PreviewMouseLeftButtonDown="DistanceSlider_PreviewMouseLeftButtonDown"
PreviewMouseLeftButtonUp="DistanceSlider_PreviewMouseLeftButtonUp"/>
<TextBox Text="{Binding SelectedDistance, StringFormat=F2, Mode=TwoWay}"
Margin="0,0,0,15" HorizontalAlignment="Right" Width="80"
Visibility="{Binding IsSelectedDistanceEditable, Converter={StaticResource BoolToVisibilityConverter}}"/>
<!-- 距离不可编辑时显示只读文本 -->
<TextBlock Text="{Binding SelectedDistanceDisplay}"
FontSize="14" Foreground="Gray" Margin="0,0,0,15"
Visibility="{Binding IsSelectedDistanceEditable, Converter={StaticResource InverseBoolToVisibilityConverter}}"/>
<TextBlock Text="等待时间 (秒):" FontWeight="SemiBold" Margin="0,0,0,3"/>
<Slider Minimum="0" Maximum="60"
Value="{Binding SelectedWaitTime, Mode=TwoWay}"
TickFrequency="1" IsSnapToTickEnabled="True"/>
<TextBox Text="{Binding SelectedWaitTime, StringFormat=F1, Mode=TwoWay}"
Margin="0,0,0,15" HorizontalAlignment="Right" Width="80"/>
<TextBlock Text="动作时间 (秒):" FontWeight="SemiBold" Margin="0,0,0,3"/>
<Slider Minimum="0" Maximum="60"
Value="{Binding SelectedActionTime, Mode=TwoWay}"
TickFrequency="1" IsSnapToTickEnabled="True"/>
<TextBox Text="{Binding SelectedActionTime, StringFormat=F1, Mode=TwoWay}"
Margin="0,0,0,15" HorizontalAlignment="Right" Width="80"/>
<Separator Margin="0,10"/>
<TextBlock Text="当前累计时间:" FontWeight="SemiBold"/>
<TextBlock Text="{Binding SelectedEvent.CumulativeTime, StringFormat={}{0:F2} 秒}"
FontSize="14" Foreground="Green"/>
</StackPanel>
</Border>
</GroupBox>
</Grid>
<!-- 底部按钮区域 -->
<Border Grid.Row="1"
BorderBrush="#FFD4E7FF"
BorderThickness="1"
CornerRadius="0"
Background="#FFF8FBFF"
Margin="15,0,15,15"
Padding="15,10">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Content="重置参数"
Style="{StaticResource SecondaryButtonStyle}"
Margin="0,0,10,0"/>
<Button Content="预览动画"
Style="{StaticResource ActionButtonStyle}"
Margin="0,0,10,0"/>
<Button Content="关闭"
Click="CloseButton_Click"
Style="{StaticResource SecondaryButtonStyle}"/>
</StackPanel>
</Border>
<!-- 底部按钮 -->
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
<Button Content="刷新" Padding="20,5" Margin="0,0,10,0" Command="{Binding RefreshCommand}"/>
<Button Content="关闭" Padding="20,5" Click="CloseButton_Click"/>
</StackPanel>
</Grid>
</Window>
</Window>

View File

@ -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
{
/// <summary>
/// TimeTagDialog.xaml 的交互逻辑
/// 时间标签设置对话框 - 用于路径时间分段计算和标签设置
/// 时标设置对话框
/// </summary>
public partial class TimeTagDialog : Window
{
/// <summary>
/// 初始化时间标签设置对话框
/// </summary>
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;
}
}
/// <summary>
/// 关闭按钮点击事件
/// 关闭按钮
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
try
{
LogManager.Info("用户关闭时间标签设置对话框");
this.Close();
}
catch (Exception ex)
{
LogManager.Error($"关闭TimeTagDialog时发生错误: {ex.Message}", ex);
}
Close();
}
/// <summary>
/// 窗口关闭时的清理工作
/// 距离滑块预览鼠标左键按下时开始拖动
/// </summary>
/// <param name="e"></param>
protected override void OnClosed(EventArgs e)
private void DistanceSlider_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
try
// 开始拖动,此时不更新源
}
/// <summary>
/// 距离滑块预览鼠标左键松开时结束拖动并更新源
/// </summary>
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();
}
}
}
}
/// <summary>
/// 反向布尔到可见性转换器false时Visibletrue时Collapsed
/// </summary>
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;
}
}
}