从数据库中比对历史检测配置是否重复

This commit is contained in:
tian 2026-02-18 23:25:39 +08:00
parent 7fbbcdd448
commit 76d277b6c2
2 changed files with 357 additions and 66 deletions

View File

@ -193,7 +193,7 @@ namespace NavisworksTransport
FrameRate INTEGER NOT NULL DEFAULT 30,
DurationSeconds REAL NOT NULL DEFAULT 10.0,
--
DetectionToleranceMeters REAL NOT NULL DEFAULT 0.05,
DetectionTolerance REAL NOT NULL DEFAULT 0.05,
--
IsVirtualObject INTEGER NOT NULL DEFAULT 0,
AnimatedObjectName TEXT,
@ -292,7 +292,7 @@ namespace NavisworksTransport
// 12. 设置数据库版本SQLite内置user_version
// 版本号格式:主版本*10000 + 次版本*100 + 修订号
// 例如2.1.3 = 20103
ExecuteNonQuery("PRAGMA user_version = 20102"); // v2.1.2 - 增加时标配置和事件表
ExecuteNonQuery("PRAGMA user_version = 20103"); // v2.1.3 - 重命名DetectionToleranceMeters为DetectionTolerance
// 创建索引
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_reports_route ON CollisionReports(RouteId)");
@ -319,7 +319,7 @@ namespace NavisworksTransport
ErrorMessage TEXT,
FrameRate INTEGER NOT NULL,
DurationSeconds REAL NOT NULL,
DetectionToleranceMeters REAL NOT NULL,
DetectionTolerance REAL NOT NULL,
IsVirtualObject INTEGER NOT NULL,
VirtualObjectLength REAL,
VirtualObjectWidth REAL,
@ -1187,7 +1187,175 @@ namespace NavisworksTransport
return results;
}
/// <summary>
/// 获取检测记录的排除对象列表
/// </summary>
public List<CollisionDetectionExcludedObjectRecord> GetDetectionExcludedObjects(int detectionRecordId)
{
var results = new List<CollisionDetectionExcludedObjectRecord>();
try
{
var sql = @"
SELECT Id, DetectionRecordId, ModelIndex, PathId, DisplayName, ObjectName, Reason
FROM CollisionDetectionExcludedObjects
WHERE DetectionRecordId = @detectionRecordId
";
using (var cmd = new SQLiteCommand(sql, _connection))
{
cmd.Parameters.AddWithValue("@detectionRecordId", detectionRecordId);
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
results.Add(new CollisionDetectionExcludedObjectRecord
{
Id = Convert.ToInt32(reader["Id"]),
DetectionRecordId = Convert.ToInt32(reader["DetectionRecordId"]),
ModelIndex = reader["ModelIndex"] == DBNull.Value ? 0 : Convert.ToInt32(reader["ModelIndex"]),
PathId = reader["PathId"] == DBNull.Value ? null : reader["PathId"].ToString(),
DisplayName = reader["DisplayName"]?.ToString(),
ObjectName = reader["ObjectName"]?.ToString(),
Reason = reader["Reason"]?.ToString()
});
}
}
}
}
catch (Exception ex)
{
LogManager.Error($"获取排除对象列表失败: {ex.Message}", ex);
}
return results;
}
/// <summary>
/// 获取检测记录的手工碰撞目标列表
/// </summary>
public List<CollisionDetectionManualTargetRecord> GetDetectionManualTargets(int detectionRecordId)
{
var results = new List<CollisionDetectionManualTargetRecord>();
try
{
var sql = @"
SELECT Id, DetectionRecordId, ModelIndex, PathId, DisplayName, ObjectName
FROM CollisionDetectionManualTargets
WHERE DetectionRecordId = @detectionRecordId
";
using (var cmd = new SQLiteCommand(sql, _connection))
{
cmd.Parameters.AddWithValue("@detectionRecordId", detectionRecordId);
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
results.Add(new CollisionDetectionManualTargetRecord
{
Id = Convert.ToInt32(reader["Id"]),
DetectionRecordId = Convert.ToInt32(reader["DetectionRecordId"]),
ModelIndex = reader["ModelIndex"] == DBNull.Value ? 0 : Convert.ToInt32(reader["ModelIndex"]),
PathId = reader["PathId"] == DBNull.Value ? null : reader["PathId"].ToString(),
DisplayName = reader["DisplayName"]?.ToString(),
ObjectName = reader["ObjectName"]?.ToString()
});
}
}
}
}
catch (Exception ex)
{
LogManager.Error($"获取手工目标列表失败: {ex.Message}", ex);
}
return results;
}
#endregion
/// <summary>
/// 通过基础参数查询匹配的检测记录(用于检查配置是否重复)
/// 注意:数值参数使用容差匹配,返回的记录还需要进一步比较排除列表和手工目标
/// </summary>
public List<CollisionDetectionRecord> FindDetectionRecordsByBaseParams(
string routeId,
int frameRate,
double durationSeconds,
double detectionTolerance,
bool isVirtualObject,
double? virtualObjectLength,
double? virtualObjectWidth,
double? virtualObjectHeight,
double objectRotationCorrection,
bool isManualMode)
{
var results = new List<CollisionDetectionRecord>();
try
{
// 使用容差匹配浮动数值参数
var sql = @"
SELECT cdr.*, pr.Name AS PathName
FROM CollisionDetectionRecords cdr
INNER JOIN PathRoutes pr ON cdr.RouteId = pr.Id
WHERE cdr.RouteId = @routeId
AND cdr.FrameRate = @frameRate
AND ABS(cdr.DurationSeconds - @duration) < 0.01
AND ABS(cdr.DetectionTolerance - @tolerance) < 0.001
AND cdr.IsVirtualObject = @isVirtual
AND ABS(cdr.ObjectRotationCorrection - @rotation) < 0.01
AND cdr.DetectAllObjects = @detectAll
AND cdr.TestName IS NOT NULL
";
using (var cmd = new SQLiteCommand(sql, _connection))
{
cmd.Parameters.AddWithValue("@routeId", routeId);
cmd.Parameters.AddWithValue("@frameRate", frameRate);
cmd.Parameters.AddWithValue("@duration", durationSeconds);
cmd.Parameters.AddWithValue("@tolerance", detectionTolerance);
cmd.Parameters.AddWithValue("@isVirtual", isVirtualObject ? 1 : 0);
cmd.Parameters.AddWithValue("@rotation", objectRotationCorrection);
cmd.Parameters.AddWithValue("@detectAll", isManualMode ? 0 : 1);
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var record = ReadCollisionDetectionRecordFromReader(reader);
// 虚拟物体尺寸比较(如果是虚拟物体)
if (isVirtualObject && virtualObjectLength.HasValue)
{
if (!record.VirtualObjectLength.HasValue ||
Math.Abs(record.VirtualObjectLength.Value - virtualObjectLength.Value) > 0.01)
continue;
if (!record.VirtualObjectWidth.HasValue ||
Math.Abs(record.VirtualObjectWidth.Value - virtualObjectWidth.Value) > 0.01)
continue;
if (!record.VirtualObjectHeight.HasValue ||
Math.Abs(record.VirtualObjectHeight.Value - virtualObjectHeight.Value) > 0.01)
continue;
}
results.Add(record);
}
}
}
LogManager.Debug($"[FindByBaseParams] 找到 {results.Count} 条基础参数匹配的记录");
}
catch (Exception ex)
{
LogManager.Error($"通过基础参数查询检测记录失败: {ex.Message}", ex);
}
return results;
}
/// <summary>
/// 保存分析评分结果
/// </summary>
@ -2086,14 +2254,14 @@ namespace NavisworksTransport
{
var sql = @"
INSERT INTO CollisionDetectionRecords (
RouteId, CreatedTime, FrameRate, DurationSeconds, DetectionToleranceMeters,
RouteId, CreatedTime, FrameRate, DurationSeconds, DetectionTolerance,
IsVirtualObject, AnimatedObjectName, ObjectModelIndex, ObjectPathId,
VirtualObjectLength, VirtualObjectWidth, VirtualObjectHeight,
DetectAllObjects, ObjectRotationCorrection, Description,
TestName, TestTime, CollisionCount, AnimationCollisionCount
)
VALUES (
@RouteId, @CreatedTime, @FrameRate, @DurationSeconds, @DetectionToleranceMeters,
@RouteId, @CreatedTime, @FrameRate, @DurationSeconds, @DetectionTolerance,
@IsVirtualObject, @AnimatedObjectName, @ObjectModelIndex, @ObjectPathId,
@VirtualObjectLength, @VirtualObjectWidth, @VirtualObjectHeight,
@DetectAllObjects, @ObjectRotationCorrection, @Description,
@ -2109,7 +2277,7 @@ namespace NavisworksTransport
cmd.Parameters.AddWithValue("@FrameRate", record.FrameRate);
cmd.Parameters.AddWithValue("@DurationSeconds", record.DurationSeconds);
// 内部使用模型单位,写入数据库时转换为米单位
cmd.Parameters.AddWithValue("@DetectionToleranceMeters", record.DetectionTolerance * UnitsConverter.GetUnitsToMetersConversionFactor());
cmd.Parameters.AddWithValue("@DetectionTolerance", record.DetectionTolerance); // 模型单位,不转换
cmd.Parameters.AddWithValue("@IsVirtualObject", record.IsVirtualObject ? 1 : 0);
cmd.Parameters.AddWithValue("@AnimatedObjectName", record.AnimatedObjectName ?? "");
cmd.Parameters.AddWithValue("@ObjectModelIndex", record.ObjectModelIndex ?? (object)DBNull.Value);
@ -2417,7 +2585,7 @@ namespace NavisworksTransport
FrameRate = Convert.ToInt32(reader["FrameRate"]),
DurationSeconds = Convert.ToDouble(reader["DurationSeconds"]),
// 数据库中存储米单位,读取时转换为模型单位
DetectionTolerance = Convert.ToDouble(reader["DetectionToleranceMeters"]) * UnitsConverter.GetMetersToUnitsConversionFactor(),
DetectionTolerance = Convert.ToDouble(reader["DetectionTolerance"]), // 模型单位,不转换
IsVirtualObject = Convert.ToInt32(reader["IsVirtualObject"]) == 1,
AnimatedObjectName = reader["AnimatedObjectName"]?.ToString(),
ObjectModelIndex = reader["ObjectModelIndex"] != DBNull.Value ? Convert.ToInt32(reader["ObjectModelIndex"]) : (int?)null,
@ -2581,7 +2749,7 @@ namespace NavisworksTransport
cmd.CommandText = @"
INSERT INTO BatchQueueItems (
RouteId, Status, CreatedTime, StartTime, EndTime, ErrorMessage,
FrameRate, DurationSeconds, DetectionToleranceMeters,
FrameRate, DurationSeconds, DetectionTolerance,
IsVirtualObject, VirtualObjectLength, VirtualObjectWidth, VirtualObjectHeight,
DetectAllObjects,
ClashDetectiveTestName, CollisionCount, ObjectRotationCorrection,
@ -2589,7 +2757,7 @@ namespace NavisworksTransport
)
VALUES (
@RouteId, @Status, @CreatedTime, @StartTime, @EndTime, @ErrorMessage,
@FrameRate, @DurationSeconds, @DetectionToleranceMeters,
@FrameRate, @DurationSeconds, @DetectionTolerance,
@IsVirtualObject, @VirtualObjectLength, @VirtualObjectWidth, @VirtualObjectHeight,
@DetectAllObjects,
@ClashDetectiveTestName, @CollisionCount, @ObjectRotationCorrection,
@ -2606,7 +2774,7 @@ namespace NavisworksTransport
cmd.Parameters.AddWithValue("@FrameRate", item.FrameRate);
cmd.Parameters.AddWithValue("@DurationSeconds", item.DurationSeconds);
// 内部使用模型单位,写入数据库时转换为米单位
cmd.Parameters.AddWithValue("@DetectionToleranceMeters", item.DetectionTolerance * UnitsConverter.GetUnitsToMetersConversionFactor());
cmd.Parameters.AddWithValue("@DetectionTolerance", item.DetectionTolerance); // 模型单位,不转换
cmd.Parameters.AddWithValue("@IsVirtualObject", item.IsVirtualObject ? 1 : 0);
cmd.Parameters.AddWithValue("@VirtualObjectLength", item.VirtualObjectLength);
cmd.Parameters.AddWithValue("@VirtualObjectWidth", item.VirtualObjectWidth);
@ -2651,7 +2819,7 @@ namespace NavisworksTransport
FrameRate = Convert.ToInt32(reader["FrameRate"]),
DurationSeconds = Convert.ToDouble(reader["DurationSeconds"]),
// 数据库中存储米单位,读取时转换为模型单位
DetectionTolerance = Convert.ToDouble(reader["DetectionToleranceMeters"]) * UnitsConverter.GetMetersToUnitsConversionFactor(),
DetectionTolerance = Convert.ToDouble(reader["DetectionTolerance"]), // 模型单位,不转换
IsVirtualObject = isVirtualObject,
VirtualObjectLength = Convert.ToDouble(reader["VirtualObjectLength"]),
VirtualObjectWidth = Convert.ToDouble(reader["VirtualObjectWidth"]),
@ -2690,7 +2858,7 @@ namespace NavisworksTransport
{
var sql = @"
SELECT bqi.Id, bqi.RouteId, pr.Name AS PathRouteName, pr.PathType, bqi.Status, bqi.CreatedTime, bqi.StartTime, bqi.EndTime, bqi.ErrorMessage,
bqi.FrameRate, bqi.DurationSeconds, bqi.DetectionToleranceMeters,
bqi.FrameRate, bqi.DurationSeconds, bqi.DetectionTolerance,
bqi.IsVirtualObject, bqi.VirtualObjectLength, bqi.VirtualObjectWidth, bqi.VirtualObjectHeight,
bqi.DetectAllObjects,
bqi.ClashDetectiveTestName, bqi.CollisionCount, bqi.ObjectRotationCorrection,
@ -2750,7 +2918,7 @@ namespace NavisworksTransport
{
cmd.CommandText = @"
SELECT bqi.Id, bqi.RouteId, pr.Name AS PathRouteName, pr.PathType, bqi.Status, bqi.CreatedTime, bqi.StartTime, bqi.EndTime, bqi.ErrorMessage,
bqi.FrameRate, bqi.DurationSeconds, bqi.DetectionToleranceMeters,
bqi.FrameRate, bqi.DurationSeconds, bqi.DetectionTolerance,
bqi.IsVirtualObject, bqi.VirtualObjectLength, bqi.VirtualObjectWidth, bqi.VirtualObjectHeight,
bqi.DetectAllObjects,
bqi.ClashDetectiveTestName, bqi.CollisionCount, bqi.ObjectRotationCorrection,

View File

@ -3255,73 +3255,87 @@ namespace NavisworksTransport.UI.WPF.ViewModels
/// <summary>
/// 检查是否已有相同配置的检测记录
/// 通过动画配置哈希匹配已完成的检测记录
/// 策略1.先检查内存缓存 2.SQL过滤基础参数 3.比较排除列表和手工目标
/// </summary>
/// <returns>(记录ID, 用户选择)不存在或用户取消则返回null</returns>
private (int id, UserChoice userChoice)? CheckExistingDetectionRecord()
{
try
{
// 计算当前动画配置的哈希
var animatedObject = UseVirtualObject ? VirtualObjectManager.Instance.CurrentVirtualObject : SelectedAnimatedObject;
var route = CurrentPathRoute?.Route;
if (animatedObject == null || route == null)
var routeId = CurrentPathRoute?.Id;
if (string.IsNullOrEmpty(routeId))
{
return null;
}
var currentHash = _pathAnimationManager.ComputeAnimationConfigHashFromRoute(animatedObject, route);
// 查询PathAnimationManager中是否有相同配置的记录
var existingRecordId = _pathAnimationManager.GetDetectionRecordIdByHash(currentHash);
if (existingRecordId.HasValue)
var pathDatabase = _pathPlanningManager?.GetPathDatabase();
if (pathDatabase == null)
{
// 验证数据库中该记录是否仍然存在
var pathDatabase = _pathPlanningManager?.GetPathDatabase();
var record = pathDatabase?.GetDetectionResultById(existingRecordId.Value);
if (record != null)
return null;
}
var animatedObject = UseVirtualObject ? VirtualObjectManager.Instance.CurrentVirtualObject : SelectedAnimatedObject;
if (animatedObject == null)
{
return null;
}
// 1. 先检查内存缓存(程序运行期间已处理过的)
var currentHash = _pathAnimationManager.ComputeAnimationConfigHashFromRoute(animatedObject, CurrentPathRoute.Route);
var cachedRecordId = _pathAnimationManager.GetDetectionRecordIdByHash(currentHash);
if (cachedRecordId.HasValue)
{
var cachedRecord = pathDatabase.GetDetectionResultById(cachedRecordId.Value);
if (cachedRecord != null)
{
LogManager.Info($"[CheckExistingDetectionRecord] 找到相同配置的检测记录: Hash={currentHash.Substring(0, 8)}..., RecordId={existingRecordId}");
// 弹出提示框询问用户
var message = $"检测配置已存在!\n\n" +
$"找到一条相同配置的历史检测记录:\n" +
$"• 测试名称:{record.TestName ?? ""}\n" +
$"• 检测时间:{record.TestTime:yyyy-MM-dd HH:mm:ss}\n" +
$"• 碰撞数量:{record.CollisionCount} 个\n\n" +
$"您可以:\n" +
$"• 【是】直接使用历史记录(跳过重复检测)\n" +
$"• 【否】重新生成新记录";
var result = System.Windows.MessageBox.Show(
message,
"检测配置已存在",
System.Windows.MessageBoxButton.YesNo,
System.Windows.MessageBoxImage.Question);
if (result == System.Windows.MessageBoxResult.Yes)
{
// 用户选择使用历史记录
_lastReusedRecordId = existingRecordId.Value;
return (existingRecordId.Value, UserChoice.UseExisting);
}
else
{
// 用户选择重新生成
_lastReusedRecordId = null;
return (existingRecordId.Value, UserChoice.CreateNew);
}
}
else
{
// 记录已被删除,清除缓存
_pathAnimationManager.RemoveDetectionRecordHash(currentHash);
LogManager.Info($"[CheckExistingDetectionRecord] 内存缓存命中: RecordId={cachedRecordId}");
return ShowDuplicateConfigDialog(cachedRecord);
}
}
// 2. SQL过滤基础参数匹配的记录
// 注意数据库存储的是模型单位不带Meters后缀查询时传模型单位
var factor = UnitsConverter.GetMetersToUnitsConversionFactor();
var matchingRecords = pathDatabase.FindDetectionRecordsByBaseParams(
routeId,
_animationFrameRate,
AnimationDuration,
_detectionTolerance * factor, // 米 → 模型单位
UseVirtualObject,
UseVirtualObject ? VirtualObjectLengthInMeters * factor : (double?)null, // 米 → 模型单位
UseVirtualObject ? VirtualObjectWidthInMeters * factor : (double?)null,
UseVirtualObject ? VirtualObjectHeightInMeters * factor : (double?)null,
_objectRotationCorrection,
IsManualCollisionTargetEnabled
);
if (matchingRecords == null || matchingRecords.Count == 0)
{
LogManager.Info("[CheckExistingDetectionRecord] SQL基础参数过滤无匹配");
return null;
}
LogManager.Info($"[CheckExistingDetectionRecord] SQL找到 {matchingRecords.Count} 条记录,开始比较排除列表");
// 3. 逐个比较排除列表和手工目标
var currentExcluded = GetCurrentExcludedObjects();
var currentTargets = GetCurrentManualTargets();
foreach (var record in matchingRecords)
{
if (IsExcludedListMatch(record.Id, currentExcluded, pathDatabase) &&
IsManualTargetListMatch(record.Id, currentTargets, pathDatabase))
{
LogManager.Info($"[CheckExistingDetectionRecord] 找到完全匹配: RecordId={record.Id}");
// 注册到内存缓存
_pathAnimationManager.RegisterAnimationHashToRecordId(currentHash, record.Id);
return ShowDuplicateConfigDialog(record);
}
}
LogManager.Info("[CheckExistingDetectionRecord] 未找到完全匹配的记录");
return null;
}
catch (Exception ex)
@ -3331,6 +3345,115 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
/// <summary>
/// 显示重复配置对话框
/// </summary>
private (int id, UserChoice userChoice)? ShowDuplicateConfigDialog(CollisionDetectionRecord record)
{
var message = $"检测配置已存在!\n\n" +
$"找到一条相同配置的历史检测记录:\n" +
$"• 测试名称:{record.TestName ?? ""}\n" +
$"• 检测时间:{record.TestTime:yyyy-MM-dd HH:mm:ss}\n" +
$"• 碰撞数量:{record.CollisionCount} 个\n\n" +
$"您可以:\n" +
$"• 【是】直接使用历史记录(跳过重复检测)\n" +
$"• 【否】重新生成新记录";
var result = System.Windows.MessageBox.Show(
message,
"检测配置已存在",
System.Windows.MessageBoxButton.YesNo,
System.Windows.MessageBoxImage.Question);
if (result == System.Windows.MessageBoxResult.Yes)
{
_lastReusedRecordId = record.Id;
return (record.Id, UserChoice.UseExisting);
}
else
{
_lastReusedRecordId = null;
return (record.Id, UserChoice.CreateNew);
}
}
/// <summary>
/// 获取当前排除对象列表ModelIndex + PathId
/// </summary>
private List<(int ModelIndex, string PathId)> GetCurrentExcludedObjects()
{
var result = new List<(int, string)>();
var excludedObjects = _pathAnimationManager?.GetExcludedObjects();
if (excludedObjects != null)
{
foreach (var obj in excludedObjects)
{
if (obj == null) continue;
try
{
var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(obj);
result.Add((pathId.ModelIndex, pathId.PathId));
}
catch { }
}
}
return result.OrderBy(x => x.Item1).ThenBy(x => x.Item2).ToList();
}
/// <summary>
/// 获取当前手工碰撞目标列表ModelIndex + PathId
/// </summary>
private List<(int ModelIndex, string PathId)> GetCurrentManualTargets()
{
var result = new List<(int, string)>();
if (IsManualCollisionTargetEnabled && _manualCollisionTargets != null)
{
foreach (var target in _manualCollisionTargets)
{
if (target?.ModelItem == null) continue;
try
{
var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(target.ModelItem);
result.Add((pathId.ModelIndex, pathId.PathId));
}
catch { }
}
}
return result.OrderBy(x => x.Item1).ThenBy(x => x.Item2).ToList();
}
/// <summary>
/// 比较排除列表是否匹配
/// </summary>
private bool IsExcludedListMatch(int recordId, List<(int ModelIndex, string PathId)> currentList, PathDatabase db)
{
var dbList = db.GetDetectionExcludedObjects(recordId)
.Select(e => (e.ModelIndex, e.PathId))
.OrderBy(x => x.ModelIndex)
.ThenBy(x => x.PathId)
.ToList();
return dbList.SequenceEqual(currentList);
}
/// <summary>
/// 比较手工目标列表是否匹配
/// </summary>
private bool IsManualTargetListMatch(int recordId, List<(int ModelIndex, string PathId)> currentList, PathDatabase db)
{
var dbList = db.GetDetectionManualTargets(recordId)
.Select(t => (t.ModelIndex, t.PathId))
.OrderBy(x => x.ModelIndex)
.ThenBy(x => x.PathId)
.ToList();
return dbList.SequenceEqual(currentList);
}
private void ExecuteToggleWireframeMode()
{
try