NavisworksTransport/src/Utils/CollisionReportHtmlGenerator.cs

481 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Commands;
namespace NavisworksTransport.Utils
{
/// <summary>
/// 碰撞报告HTML生成器
/// </summary>
public static class CollisionReportHtmlGenerator
{
/// <summary>
/// 生成HTML报告内容
/// </summary>
/// <param name="report">碰撞报告结果</param>
/// <param name="htmlFilePath">HTML文件路径用于计算截图相对路径</param>
/// <returns>HTML内容</returns>
public static string GenerateHtmlReport(CollisionReportResult report, string htmlFilePath)
{
if (report == null)
{
throw new ArgumentNullException(nameof(report));
}
var html = new StringBuilder();
var now = DateTime.Now;
// HTML头部和样式
html.AppendLine("<!DOCTYPE html>");
html.AppendLine("<html><head>");
html.AppendLine("<meta charset='UTF-8'>");
html.AppendLine("<title>NavisworksTransport 碰撞检测报告</title>");
html.AppendLine("<style>");
html.AppendLine("body { font-family: 'Microsoft YaHei', Arial, sans-serif; margin: 20px; line-height: 1.6; }");
html.AppendLine("h1 { color: #2c5aa0; text-align: center; border-bottom: 2px solid #2c5aa0; padding-bottom: 10px; }");
html.AppendLine("h2 { color: #2c5aa0; margin-top: 25px; }");
html.AppendLine(".summary { background: #f8fbff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #2c5aa0; }");
html.AppendLine(".stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin: 15px 0; }");
html.AppendLine(".stat-item { background: white; padding: 15px; border-radius: 6px; border: 1px solid #ddd; text-align: center; }");
html.AppendLine(".stat-label { font-size: 12px; color: #666; margin-bottom: 5px; }");
html.AppendLine(".stat-value { font-size: 18px; font-weight: bold; color: #2c5aa0; }");
html.AppendLine(".moving-object { background: #f0f8ff; padding: 15px; border-radius: 8px; text-align: center; margin: 15px 0; border: 1px solid #b0d4f1; }");
html.AppendLine(".collided-list { background: #fdf2f2; padding: 15px; border-radius: 8px; margin: 15px 0; max-height: 200px; overflow-y: auto; border: 1px solid #f5c6c6; }");
html.AppendLine(".collided-list .list-item { margin: 5px 0; padding: 5px; border-left: 3px solid #dc3545; padding-left: 10px; color: #721c24; font-weight: 500; }");
html.AppendLine(".excluded-list { background: #fef9e7; padding: 15px; border-radius: 8px; margin: 15px 0; max-height: 250px; overflow-y: auto; border: 1px solid #f9e79f; }");
html.AppendLine(".excluded-list .list-item { margin: 5px 0; padding: 5px; border-left: 3px solid #f9e79f; padding-left: 10px; color: #856404; }");
html.AppendLine(".recommendation { background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 15px 0; }");
html.AppendLine(".collision-item { background: white; margin: 10px 0; padding: 15px; border-radius: 6px; border: 1px solid #eee; }");
html.AppendLine(".collision-title { font-weight: bold; color: #333; margin-bottom: 8px; }");
html.AppendLine(".collision-status { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 11px; color: white; margin-bottom: 8px; }");
html.AppendLine(".status-new { background-color: #ff6b6b; } .status-active { background-color: #ffa500; }");
html.AppendLine(".status-reviewed { background-color: #87ceeb; } .status-approved { background-color: #98fb98; } .status-resolved { background-color: #90ee90; }");
html.AppendLine(".screenshot-section { margin: 20px 0; padding: 15px; background-color: #f5f5f5; border-radius: 5px; }");
html.AppendLine(".screenshot-container { text-align: center; }");
html.AppendLine(".screenshot-image { max-width: 100%; height: auto; border: 1px solid #ddd; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }");
html.AppendLine(".screenshot-info { margin-top: 10px; font-size: 12px; color: #666; }");
html.AppendLine("</style>");
html.AppendLine("</head><body>");
// 从数据库获取检测记录信息
var (testName, testTime) = GetDetectionRecordInfo(report.ResultId);
// 报告标题
html.AppendLine($"<h1>NavisworksTransport 碰撞检测报告</h1>");
html.AppendLine($"<p style='text-align: center; color: #666;'>生成时间: {now:yyyy年MM月dd日 HH:mm:ss}</p>");
html.AppendLine($"<p style='text-align: center; color: #666;'>报告类型: 综合报告</p>");
// 检测记录信息
html.AppendLine("<div class='summary'>");
html.AppendLine("<h2>检测记录</h2>");
html.AppendLine("<div class='stats-grid' style='grid-template-columns: repeat(2, 1fr);'>");
html.AppendLine("<div class='stat-item'>");
html.AppendLine("<div class='stat-label'>测试名称</div>");
html.AppendLine($"<div class='stat-value'>{testName ?? ""}</div>");
html.AppendLine("</div>");
html.AppendLine("<div class='stat-item'>");
html.AppendLine("<div class='stat-label'>检测时间</div>");
html.AppendLine($"<div class='stat-value'>{testTime}</div>");
html.AppendLine("</div>");
html.AppendLine("</div>");
html.AppendLine("</div>");
// 总体统计
html.AppendLine("<div class='summary'>");
html.AppendLine("<h2>总体统计</h2>");
html.AppendLine("<div class='stats-grid'>");
html.AppendLine("<div class='stat-item'>");
html.AppendLine("<div class='stat-label'>总碰撞数</div>");
html.AppendLine($"<div class='stat-value'>{report.TotalCollisions}</div>");
html.AppendLine("</div>");
html.AppendLine("<div class='stat-item'>");
html.AppendLine("<div class='stat-label'>碰撞构件</div>");
html.AppendLine($"<div class='stat-value'>{report.UniqueCollidedObjectsCount}个</div>");
html.AppendLine("</div>");
html.AppendLine("<div class='stat-item'>");
html.AppendLine("<div class='stat-label'>检测点</div>");
html.AppendLine($"<div class='stat-value'>{report.AnimationCollisions}个</div>");
html.AppendLine("</div>");
html.AppendLine("</div>");
html.AppendLine("</div>");
// 动画参数
html.AppendLine("<h2>动画参数</h2>");
html.AppendLine("<div class='stats-grid'>");
html.AppendLine("<div class='stat-item'>");
html.AppendLine("<div class='stat-label'>帧率</div>");
html.AppendLine($"<div class='stat-value'>{report.FrameRate} FPS</div>");
html.AppendLine("</div>");
html.AppendLine("<div class='stat-item'>");
html.AppendLine("<div class='stat-label'>时长</div>");
html.AppendLine($"<div class='stat-value'>{report.Duration:F1} 秒</div>");
html.AppendLine("</div>");
html.AppendLine("<div class='stat-item'>");
html.AppendLine("<div class='stat-label'>检测容差</div>");
html.AppendLine($"<div class='stat-value'>{report.DetectionTolerance:F2} 米</div>");
html.AppendLine("</div>");
html.AppendLine("</div>");
// 碰撞场景截图(支持多张)
var validScreenshots = report.Screenshots?.Where(s => !string.IsNullOrEmpty(s.FilePath) && File.Exists(s.FilePath)).ToList();
if (validScreenshots?.Count > 0)
{
html.AppendLine("<h2>碰撞场景截图</h2>");
// 多张截图样式 - 大图+缩略图横排交互式布局
if (validScreenshots?.Count > 1)
{
html.AppendLine("<style>");
// 主图区域样式
html.AppendLine(".main-image-container { background: white; padding: 15px; border-radius: 6px; border: 1px solid #ddd; margin-bottom: 15px; }");
html.AppendLine(".main-image { width: 100%; height: auto; border-radius: 4px; cursor: pointer; transition: transform 0.2s; }");
html.AppendLine(".main-image:hover { transform: scale(1.01); }");
html.AppendLine(".main-image-info { margin-top: 10px; font-size: 13px; color: #666; text-align: center; }");
// 缩略图横排样式
html.AppendLine(".thumbnail-row { display: flex; gap: 10px; overflow-x: auto; padding: 10px; background: #f9f9f9; border-radius: 6px; border: 1px solid #e0e0e0; }");
html.AppendLine(".thumbnail-item { flex: 0 0 auto; width: 120px; cursor: pointer; transition: all 0.2s; }");
html.AppendLine(".thumbnail-item img { width: 100%; height: 80px; object-fit: cover; border-radius: 4px; border: 2px solid transparent; transition: all 0.2s; }");
html.AppendLine(".thumbnail-item:hover img { border-color: #87ceeb; transform: scale(1.05); }");
html.AppendLine(".thumbnail-item.active img { border-color: #2B579A; box-shadow: 0 0 6px rgba(43,87,154,0.4); }");
html.AppendLine(".thumbnail-label { font-size: 11px; color: #666; text-align: center; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }");
html.AppendLine(".thumbnail-item.active .thumbnail-label { color: #2B579A; font-weight: bold; }");
html.AppendLine("</style>");
// 主图区域
html.AppendLine("<div class='main-image-container'>");
html.AppendLine("<img id='mainImage' src=\"\" alt=\"主图\" class=\"main-image\" onclick=\"window.open(this.src)\">");
html.AppendLine("<div id='mainImageInfo' class='main-image-info'></div>");
html.AppendLine("</div>");
// 缩略图横排
html.AppendLine("<div class='thumbnail-row' id='thumbnailRow'>");
int index = 1;
foreach (var screenshot in validScreenshots.OrderBy(s => s.SortOrder))
{
string relativePath = PathHelper.GetRelativePath(htmlFilePath, screenshot.FilePath);
string label = string.IsNullOrEmpty(screenshot.Description) ? $"截图 {index}" : screenshot.Description;
string desc = screenshot.Description ?? $"截图 {index}";
html.AppendLine($"<div class='thumbnail-item' data-src=\"{relativePath}\" data-index=\"{index}\" data-width=\"{screenshot.Width}\" data-height=\"{screenshot.Height}\" data-time=\"{screenshot.CaptureTime:yyyy-MM-dd HH:mm}\" data-desc=\"{desc}\" onclick=\"selectImage(this)\">");
html.AppendLine($"<img src=\"{relativePath}\" alt=\"{label}\">");
html.AppendLine($"<div class='thumbnail-label'>{label}</div>");
html.AppendLine("</div>");
index++;
}
html.AppendLine("</div>");
// JavaScript交互
html.AppendLine("<script>");
html.AppendLine("function selectImage(element) {");
html.AppendLine(" // 更新选中状态");
html.AppendLine(" document.querySelectorAll('.thumbnail-item').forEach(function(el) { el.classList.remove('active'); });");
html.AppendLine(" element.classList.add('active');");
html.AppendLine(" // 更新主图");
html.AppendLine(" var mainImage = document.getElementById('mainImage');");
html.AppendLine(" mainImage.src = element.getAttribute('data-src');");
html.AppendLine(" // 更新主图信息");
html.AppendLine(" var desc = element.getAttribute('data-desc');");
html.AppendLine(" var width = element.getAttribute('data-width');");
html.AppendLine(" var height = element.getAttribute('data-height');");
html.AppendLine(" var time = element.getAttribute('data-time');");
html.AppendLine(" document.getElementById('mainImageInfo').innerHTML = desc + ' &bull; ' + width + 'x' + height + ' &bull; ' + time;");
html.AppendLine("}");
html.AppendLine("// 初始化显示第一张图");
html.AppendLine("document.addEventListener('DOMContentLoaded', function() {");
html.AppendLine(" var firstThumb = document.querySelector('.thumbnail-item');");
html.AppendLine(" if (firstThumb) selectImage(firstThumb);");
html.AppendLine("});");
html.AppendLine("</script>");
}
else if (validScreenshots?.Count == 1)
{
// 单张截图样式
var screenshot = validScreenshots.First();
html.AppendLine("<div class='screenshot-section'>");
string relativePath = PathHelper.GetRelativePath(htmlFilePath, screenshot.FilePath);
html.AppendLine("<div class='screenshot-container'>");
html.AppendLine($"<img src=\"{relativePath}\" alt=\"碰撞场景截图\" class=\"screenshot-image\"/>");
html.AppendLine("<div class='screenshot-info'>");
html.AppendLine($"<p>分辨率: {screenshot.Width} x {screenshot.Height}</p>");
html.AppendLine($"<p>格式: {screenshot.Format}</p>");
html.AppendLine("</div>");
html.AppendLine("</div>");
html.AppendLine("</div>");
}
}
// 运动构件信息
if (!string.IsNullOrEmpty(report.MovingObjectInfo))
{
html.AppendLine("<h2>运动构件信息</h2>");
html.AppendLine("<div class='moving-object'>");
html.AppendLine($"<strong>🚛 {report.MovingObjectInfo}</strong>");
html.AppendLine("</div>");
}
// 碰撞构件清单
if (report.CollidedObjectsList?.Count > 0)
{
html.AppendLine("<h2>碰撞构件清单</h2>");
html.AppendLine("<div class='collided-list'>");
html.AppendLine($"<p style='margin: 0 0 10px 0; color: #721c24;'><strong>⚠️ 发现 {report.UniqueCollidedObjectsCount} 个碰撞构件</strong></p>");
int index = 1;
foreach (var objName in report.CollidedObjectsList.Take(50)) // 限制显示前50个
{
html.AppendLine($"<div class='list-item'>{index}. {objName}</div>");
index++;
}
if (report.CollidedObjectsList.Count > 50)
{
html.AppendLine($"<div class='list-item'><em>... 还有 {report.CollidedObjectsList.Count - 50} 个构件</em></div>");
}
html.AppendLine("</div>");
}
// 排除对象清单
if (report.ExcludedObjects?.Count > 0)
{
html.AppendLine("<h2>排除对象清单</h2>");
html.AppendLine("<div class='excluded-list'>");
html.AppendLine($"<p style='margin: 0 0 10px 0; color: #856404;'><strong>🚫 以下 {report.ExcludedObjects.Count} 个对象被排除在碰撞检测之外</strong></p>");
int index = 1;
foreach (var excludedObj in report.ExcludedObjects.Take(30)) // 限制显示前30个
{
string displayName = !string.IsNullOrEmpty(excludedObj.DisplayName) ? excludedObj.DisplayName : excludedObj.ObjectName ?? "未知对象";
string reason = !string.IsNullOrEmpty(excludedObj.Reason) ? $"原因: {excludedObj.Reason}" : "无";
string time = excludedObj.ExcludedTime > DateTime.MinValue ? $"时间: {excludedObj.ExcludedTime:yyyy-MM-dd HH:mm}" : "";
html.AppendLine($"<div class='list-item'>");
html.AppendLine($"<strong>{index}. {displayName}</strong>");
html.AppendLine($"<span style='color: #856404; font-size: 11px; margin-left: 10px;'>({reason}{(!string.IsNullOrEmpty(time) ? " | " + time : "")})</span>");
html.AppendLine("</div>");
index++;
}
if (report.ExcludedObjects.Count > 30)
{
html.AppendLine($"<div class='list-item'><em>... 还有 {report.ExcludedObjects.Count - 30} 个排除对象</em></div>");
}
html.AppendLine("</div>");
}
// 总结与建议
html.AppendLine("<h2>总结与建议</h2>");
html.AppendLine("<div class='recommendation'>");
// 根据碰撞数量生成总结信息
string summaryMessage;
if (report.TotalCollisions == 0)
{
summaryMessage = "✅ 未检测到碰撞,路径规划合理,可以安全通行。";
}
else if (report.TotalCollisions <= 5)
{
summaryMessage = $"⚠️ 检测到 {report.TotalCollisions} 个碰撞,建议优化路径或调整构件位置。";
}
else
{
summaryMessage = $"🔴 检测到 {report.TotalCollisions} 个碰撞,存在严重安全隐患,必须立即处理。";
}
html.AppendLine($"<p><strong>{summaryMessage}</strong></p>");
// 生成建议措施
html.AppendLine("<p><strong>建议措施:</strong></p>");
html.AppendLine("<ul>");
if (report.TotalCollisions == 0)
{
html.AppendLine("<li>保持当前路径规划方案</li>");
html.AppendLine("<li>定期检查构件位置变化</li>");
html.AppendLine("<li>建议进行现场复核确认</li>");
}
else
{
html.AppendLine("<li>检查碰撞构件的实际位置和尺寸</li>");
html.AppendLine("<li>优化路径规划,避开碰撞区域</li>");
html.AppendLine("<li>调整构件位置或尺寸</li>");
html.AppendLine("<li>考虑增加安全间隙</li>");
html.AppendLine("<li>进行现场勘察确认</li>");
}
html.AppendLine("</ul>");
html.AppendLine("</div>");
// 详细碰撞信息前10个
if (report.AllCollisions?.Count > 0)
{
html.AppendLine("<h2>碰撞详情</h2>");
int collisionIndex = 1;
foreach (var collision in report.AllCollisions.Take(10))
{
html.AppendLine("<div class='collision-item'>");
// 生成碰撞标题
string item1Name = collision.Item1 != null ? ModelItemAnalysisHelper.GetSafeDisplayName(collision.Item1) : "未知对象";
string item2Name = collision.Item2 != null ? ModelItemAnalysisHelper.GetSafeDisplayName(collision.Item2) : "未知对象";
html.AppendLine($"<div class='collision-title'>{collisionIndex}. {item1Name} ↔ {item2Name}</div>");
// 全路径
string item1Path = GetModelItemFullPath(collision.Item1);
string item2Path = GetModelItemFullPath(collision.Item2);
html.AppendLine($"<p style='font-size: 11px; color: #666; margin: 3px 0;'>🚛 <strong>运动物体:</strong> {item1Path}</p>");
html.AppendLine($"<p style='font-size: 11px; color: #666; margin: 3px 0;'>💥 <strong>被撞对象:</strong> {item2Path}</p>");
// 碰撞位置
if (collision.Center != null)
{
html.AppendLine($"<p style='font-size: 11px; color: #999; margin: 3px 0;'><strong>位置:</strong> ({collision.Center.X:F2}, {collision.Center.Y:F2}, {collision.Center.Z:F2})</p>");
}
html.AppendLine("</div>");
collisionIndex++;
}
if (report.AllCollisions.Count > 10)
{
html.AppendLine($"<p><em>... 还有 {report.AllCollisions.Count - 10} 个碰撞未显示</em></p>");
}
}
// HTML结尾
html.AppendLine($"<hr style='margin: 30px 0;'>");
html.AppendLine($"<p style='text-align: center; color: #999; font-size: 12px;'>报告生成完成 - {now:HH:mm:ss}</p>");
html.AppendLine("</body></html>");
return html.ToString();
}
/// <summary>
/// 保存HTML报告到文件
/// </summary>
/// <param name="report">碰撞报告结果</param>
/// <param name="htmlFilePath">HTML文件保存路径</param>
/// <returns>是否保存成功</returns>
public static bool SaveHtmlReport(CollisionReportResult report, string htmlFilePath)
{
try
{
// 确保目录存在
string directory = Path.GetDirectoryName(htmlFilePath);
if (!string.IsNullOrEmpty(directory))
{
PathHelper.EnsureDirectoryExists(directory);
}
// 生成HTML内容
string htmlContent = GenerateHtmlReport(report, htmlFilePath);
// 保存到文件
File.WriteAllText(htmlFilePath, htmlContent, System.Text.Encoding.UTF8);
LogManager.Info($"HTML报告已保存: {htmlFilePath}");
return true;
}
catch (Exception ex)
{
LogManager.Error($"保存HTML报告失败: {ex.Message}", ex);
return false;
}
}
/// <summary>
/// 获取ModelItem的完整层级路径
/// </summary>
private static string GetModelItemFullPath(ModelItem item)
{
if (item == null) return "未知对象";
try
{
var pathParts = new List<string>();
var current = item;
int levels = 0;
while (current != null && levels < 20)
{
var name = ModelItemAnalysisHelper.GetSafeDisplayName(current);
if (!string.IsNullOrEmpty(name) && name != "未命名对象")
{
pathParts.Insert(0, name);
}
current = current.Parent;
levels++;
}
return pathParts.Count > 0 ? string.Join(" > ", pathParts) : "未知对象";
}
catch
{
return ModelItemAnalysisHelper.GetSafeDisplayName(item);
}
}
/// <summary>
/// 自动生成并保存HTML报告到默认目录
/// </summary>
/// <param name="report">碰撞报告结果</param>
/// <returns>HTML文件路径失败返回null</returns>
public static string AutoSaveHtmlReport(CollisionReportResult report)
{
try
{
// 生成文件名
string sanitizedName = PathHelper.SanitizeFileName(report.PathName ?? "未知路径");
string fileName = PathHelper.GenerateTimestampedFileName($"collision_{sanitizedName}", "html");
// 获取报告目录
string reportDir = PathHelper.GetReportDirectory();
string htmlFilePath = Path.Combine(reportDir, fileName);
// 保存报告
if (SaveHtmlReport(report, htmlFilePath))
{
return htmlFilePath;
}
return null;
}
catch (Exception ex)
{
LogManager.Error($"自动保存HTML报告失败: {ex.Message}", ex);
return null;
}
}
/// <summary>
/// 从数据库获取检测记录信息
/// </summary>
private static (string testName, string testTime) GetDetectionRecordInfo(int resultId)
{
try
{
if (resultId <= 0)
{
return (null, "未知时间");
}
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
if (pathDatabase == null)
{
return (null, "未知时间");
}
var record = pathDatabase.GetDetectionResultById(resultId);
if (record != null)
{
string timeStr = record.TestTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "未知时间";
return (record.TestName, timeStr);
}
}
catch (Exception ex)
{
LogManager.Error($"[CollisionReportHtmlGenerator] 获取检测记录信息失败: {ex.Message}");
}
return (null, "未知时间");
}
}
}