ReportPdfExporter.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. using QuestPDF.Fluent;
  2. using QuestPDF.Helpers;
  3. using QuestPDF.Infrastructure;
  4. using SWRIS.Core;
  5. using SWRIS.Dtos;
  6. using SWRIS.Models;
  7. using System;
  8. using System.Collections.Generic;
  9. using System.Linq;
  10. namespace SWRIS.Extensions
  11. {
  12. public class ReportPdfExporter
  13. {
  14. private readonly string[] ChineseNumbers = { "零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十" };
  15. private readonly string _crane;
  16. private readonly RecordDto _record;
  17. private readonly EquipmentModel _equipment;
  18. private readonly int _damageNearPointCount = 20;
  19. public ReportPdfExporter(string crane, RecordDto record, EquipmentModel equipment)
  20. {
  21. _crane = crane;
  22. _record = record;
  23. _equipment = equipment;
  24. _damageNearPointCount = App.Config.DamageNearPointCount;
  25. }
  26. public bool SavePdf(string filePath)
  27. {
  28. if (_record == null || _equipment == null)
  29. {
  30. return false;
  31. }
  32. Document.Create(document =>
  33. {
  34. var damageList = _record.Damages.OrderByDescending(c => c.DamageValue).Take(5).OrderBy(c => c.DamagePoint).ToList();
  35. List<(double? position, double? damage, byte[] image)> images = GenerateImage(_record.DataFilePath,
  36. _record.SensorCount,
  37. _record.StartPoint,
  38. _record.EndPoint,
  39. damageList: damageList);
  40. document.Page(page =>
  41. {
  42. page.Size(PageSizes.A4);
  43. page.MarginTop(2, Unit.Centimetre);
  44. page.MarginBottom(1, Unit.Centimetre);
  45. page.MarginLeft(1, Unit.Centimetre);
  46. page.MarginRight(1, Unit.Centimetre);
  47. page.Header().AlignCenter().AlignTop().Text("钢丝绳无损检测报告").FontSize(20).Bold();
  48. page.Content()
  49. .AlignCenter()
  50. .AlignTop()
  51. .PaddingTop(20)
  52. .Table(table =>
  53. {
  54. table.ColumnsDefinition(columns =>
  55. {
  56. columns.ConstantColumn(22);
  57. columns.RelativeColumn();
  58. columns.RelativeColumn();
  59. columns.RelativeColumn();
  60. columns.RelativeColumn();
  61. columns.RelativeColumn();
  62. columns.RelativeColumn();
  63. });
  64. table.Cell().Row(1).Column(1).ColumnSpan(4).PaddingBottom(5).Text($"所在起重机:{_crane}").AlignLeft();
  65. table.Cell().Row(1).Column(5).ColumnSpan(3).PaddingBottom(5).Text($"检测时间:{_record.StartTime:yyyy-MM-dd HH:mm:ss}")
  66. .AlignRight();
  67. table.Cell().Row(2).Column(1).RowSpan(7).Border(1).AlignMiddle().Text("基本信息").Bold().AlignCenter();
  68. table.Cell().Row(2).Column(2).Element(CellNameStyle).Text("被检测钢丝绳");
  69. table.Cell().Row(2).Column(3).ColumnSpan(2).Element(CellValueStyle).Text(_equipment.RopeName);
  70. table.Cell().Row(2).Column(5).Element(CellNameStyle).Text("钢丝绳制造商");
  71. table.Cell().Row(2).Column(6).ColumnSpan(2).Element(CellValueStyle).Text(_equipment.Supplier);
  72. table.Cell().Row(3).Column(2).Element(CellNameStyle).Text("钢丝绳长");
  73. table.Cell().Row(3).Column(3).ColumnSpan(2).Element(CellValueStyle).Text(_equipment.Parameter.WireRopeLength.ToString() + "m");
  74. table.Cell().Row(3).Column(5).Element(CellNameStyle).Text("检测长度");
  75. table.Cell().Row(3).Column(6).ColumnSpan(2).Element(CellValueStyle).Text(_record.DetectionLength.ToString("F3") + "m");
  76. table.Cell().Row(4).Column(2).Element(CellNameStyle).Text("开始位置");
  77. table.Cell().Row(4).Column(3).ColumnSpan(2).Element(CellValueStyle).Text(_record.StartPoint.ToString("F3") + "m");
  78. table.Cell().Row(4).Column(5).Element(CellNameStyle).Text("结束位置");
  79. table.Cell().Row(4).Column(6).ColumnSpan(2).Element(CellValueStyle).Text(_record.EndPoint.ToString("F3") + "m");
  80. table.Cell().Row(5).Column(2).Element(CellNameStyle).Text("钢丝绳规格");
  81. table.Cell().Row(5).Column(3).ColumnSpan(2).Element(CellValueStyle).Text(
  82. $"{_equipment.Parameter.WireRopeDiameter} {_equipment.WireSurfaceType} " +
  83. $"{_equipment.Parameter.WireRopeStrandCount}*" +
  84. $"{_equipment.Parameter.WireRopeStrandWireCount}S+{_equipment.RopeCoreType} " +
  85. $"{_equipment.LayType}");
  86. table.Cell().Row(5).Column(5).Element(CellNameStyle).Text("钢丝材质");
  87. table.Cell().Row(5).Column(6).ColumnSpan(2).Element(CellValueStyle).Text(_equipment.WireMaterialType.GetDescription());
  88. table.Cell().Row(6).Column(2).Element(CellNameStyle).Text("钢丝绳公称直径");
  89. table.Cell().Row(6).Column(3).ColumnSpan(2).Element(CellValueStyle).Text(_equipment.Parameter.WireRopeDiameter + "mm");
  90. table.Cell().Row(6).Column(5).Element(CellNameStyle).Text("绳芯材质");
  91. table.Cell().Row(6).Column(6).ColumnSpan(2).Element(CellValueStyle).Text(_equipment.RopeCoreType.GetDescription());
  92. table.Cell().Row(7).Column(2).Element(CellNameStyle).Text("捻制方法");
  93. table.Cell().Row(7).Column(3).ColumnSpan(2).Element(CellValueStyle).Text(_equipment.LayType.GetDescription());
  94. table.Cell().Row(7).Column(5).Element(CellNameStyle).Text("钢丝绳表面状态");
  95. table.Cell().Row(7).Column(6).ColumnSpan(2).Element(CellValueStyle).Text(_equipment.WireSurfaceType.GetDescription());
  96. table.Cell().Row(8).Column(2).Element(CellNameStyle).Text("依据标准");
  97. table.Cell().Row(8).Column(3).ColumnSpan(5).Element(CellValueStyle).AlignLeft().Text(
  98. "GB/T 21837-2023 《铁磁性钢丝绳电磁检测方法》\r\n" +
  99. "GB/T 26832-2011 《无损检测仪器 钢丝绳电磁检测仪技术条件》\r\n" +
  100. "GB/T 5972-2023 《起重机 钢丝绳 保养、维护、检验和报废》");
  101. table.Cell().Row(9).Column(1).RowSpan(5).Border(1).AlignMiddle().Text("检测结果").Bold().AlignCenter();
  102. table.Cell().Row(9).RowSpan(2).Column(2).Element(CellNameStyle).Text("最大损伤\r\n程度");
  103. table.Cell().Row(9).Column(3).ColumnSpan(5).Element(CellNameStyle).Text("五处重要损伤");
  104. table.Cell().Row(10).Column(3).Element(CellValueStyle).Text("第一处");
  105. table.Cell().Row(10).Column(4).Element(CellValueStyle).Text("第二处");
  106. table.Cell().Row(10).Column(5).Element(CellValueStyle).Text("第三处");
  107. table.Cell().Row(10).Column(6).Element(CellValueStyle).Text("第四处");
  108. table.Cell().Row(10).Column(7).Element(CellValueStyle).Text("第五处");
  109. table.Cell().Row(11).Column(2).Element(CellNameStyle).Text("损伤位置");
  110. table.Cell().Row(12).Column(2).Element(CellNameStyle).Text("损伤当量");
  111. table.Cell().Row(13).Column(2).Element(CellNameStyle).Text("损伤级别");
  112. for (int i = 0; i < 5; i++)
  113. {
  114. string positionValue = i < damageList.Count ? damageList[i].DamagePoint.ToString("F3") + "m" : "-";
  115. string damageValue = i < damageList.Count ? damageList[i].DamageValue.ToString("F3") + "%" : "-";
  116. string damageLevel = i < damageList.Count ? damageList[i].DamageLevel.GetDescription() : "-";
  117. table.Cell().Row(11).Column((uint)(3 + i)).Element(CellValueStyle).Text(positionValue);
  118. table.Cell().Row(12).Column((uint)(3 + i)).Element(CellValueStyle).Text(damageValue);
  119. table.Cell().Row(13).Column((uint)(3 + i)).Element(CellValueStyle).Text(damageLevel);
  120. }
  121. });
  122. });
  123. document.Page(page =>
  124. {
  125. page.Size(PageSizes.A4);
  126. page.MarginTop(2, Unit.Centimetre);
  127. page.MarginBottom(2, Unit.Centimetre);
  128. page.MarginLeft(1, Unit.Centimetre);
  129. page.MarginRight(1, Unit.Centimetre);
  130. page.Content().Column(column =>
  131. {
  132. for (int i = 0; i < images.Count; i++)
  133. {
  134. if (i == 0)
  135. {
  136. column.Item().PaddingLeft(20).Text($"整绳检测波形图").FontSize(12).Bold();
  137. column.Item().Image(images[i].image);
  138. column.Item().Height(35);
  139. }
  140. else
  141. {
  142. column.Item().PaddingLeft(20).Text($"第{ChineseNumbers[i]}处重要损伤").FontSize(12).Bold();
  143. column.Item().Image(images[i].image);
  144. column.Item().AlignCenter().Text($"损伤位置:{images[i].position?.ToString("F3")}m " +
  145. $"损伤量值:{images[i].damage?.ToString("F3")}%").FontSize(10).FontColor(Color.FromHex("#7a08fa"));
  146. column.Item().Height(34);
  147. }
  148. }
  149. });
  150. });
  151. }).GeneratePdf(filePath);
  152. return true;
  153. }
  154. protected List<(double? position, double? damage, byte[] image)> GenerateImage(string dataFilePath,
  155. int sensorCount,
  156. double startPoint,
  157. double endPoint,
  158. List<DamageDto> damageList)
  159. {
  160. List<(double? position, double? damage, byte[] image)> imageDamages = new List<(double? position, double? damage, byte[] image)>();
  161. if (dataFilePath.IsNullOrEmpty())
  162. {
  163. return null;
  164. }
  165. var damageData = DatFileHandler.ReadDatFile(dataFilePath);
  166. if (damageData.Length > 0)
  167. {
  168. (double[] positions, ushort[] damages) = ParseSensorData(damageData, sensorCount, startPoint, endPoint);
  169. if (positions != null && damages != null)
  170. {
  171. ScottPlot.Plot plot = new ScottPlot.Plot();
  172. plot.Add.Scatter(positions, damages, new ScottPlot.Color("#70a1d7"));
  173. plot.Axes.SetLimits(startPoint * 0.95, endPoint * 1.05, 40, 80);
  174. imageDamages.Add((null, null, plot.GetImageBytes(900, 300)));
  175. foreach (var damage in damageList)
  176. {
  177. int damageIndex = FindClosestIndexBinarySearch(positions, damage.DamagePoint);
  178. if (damageIndex >= 0)
  179. {
  180. // 计算左右各10个点的索引范围
  181. int startIndex = Math.Max(0, damageIndex - _damageNearPointCount);
  182. int endIndex = Math.Min(positions.Length - 1, damageIndex + _damageNearPointCount);
  183. // 设置X轴范围为左右各10个点
  184. double start = positions[startIndex];
  185. double end = positions[endIndex];
  186. plot.Axes.SetLimitsX(start, end);
  187. }
  188. else
  189. {
  190. // 如果找不到精确匹配,使用近似值
  191. double start = damage.DamagePoint * 0.995;
  192. double end = damage.DamagePoint * 1.005;
  193. plot.Axes.SetLimitsX(start, end);
  194. }
  195. imageDamages.Add((damage.DamagePoint, damage.DamageValue, plot.GetImageBytes(900, 300)));
  196. }
  197. }
  198. }
  199. return imageDamages;
  200. }
  201. public (double[] Positions, ushort[] Damages) ParseSensorData(byte[] data,
  202. int sensorCount,
  203. double startPosition,
  204. double endPosition)
  205. {
  206. // 检查数据长度是否能被传感器数量整除
  207. if (data.Length % sensorCount != 0)
  208. {
  209. LogHelper.Error($"数据长度不匹配。数据长度 {data.Length} 字节不能被传感器数量 {sensorCount} 整除。");
  210. return (null, null);
  211. }
  212. // 计算样本数量
  213. int sampleCount = data.Length / sensorCount;
  214. // 计算采样步长
  215. double samplingStep = (endPosition - startPosition) / (sampleCount - 1);
  216. // 初始化结果数组
  217. double[] positions = new double[sampleCount];
  218. ushort[] damages = new ushort[sampleCount];
  219. // 解析数据并计算平均值
  220. for (int sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++)
  221. {
  222. positions[sampleIndex] = startPosition;
  223. // 计算当前样本的所有传感器Damage总和
  224. int sum = 0;
  225. for (int sensorIndex = 0; sensorIndex < sensorCount; sensorIndex++)
  226. {
  227. // 计算数据在字节数组中的位置
  228. int dataIndex = (sampleIndex * sensorCount) + sensorIndex;
  229. sum += data[dataIndex];
  230. }
  231. // 计算平均值
  232. damages[sampleIndex] = (ushort)(sum / sensorCount);
  233. startPosition += samplingStep;
  234. }
  235. return (positions, damages);
  236. }
  237. /// <summary>
  238. /// 使用二分查找在有序数组中查找最接近目标值的索引
  239. /// </summary>
  240. /// <returns></returns>
  241. private int FindClosestIndexBinarySearch(double[] array, double targetValue)
  242. {
  243. if (array == null || array.Length == 0)
  244. return -1;
  245. int left = 0;
  246. int right = array.Length - 1;
  247. while (left <= right)
  248. {
  249. int mid = left + (right - left) / 2;
  250. if (array[mid] == targetValue)
  251. return mid;
  252. else if (array[mid] < targetValue)
  253. left = mid + 1;
  254. else
  255. right = mid - 1;
  256. }
  257. // 此时left和right已经交叉,需要比较最接近的两个值
  258. if (right < 0) return 0;
  259. if (left >= array.Length) return array.Length - 1;
  260. return Math.Abs(array[left] - targetValue) < Math.Abs(array[right] - targetValue) ? left : right;
  261. }
  262. static IContainer CellNameStyle(IContainer container)
  263. {
  264. return container
  265. .DefaultTextStyle(x => x.FontColor(Colors.Black).Bold())
  266. .Border(1)
  267. .Padding(10)
  268. .AlignCenter()
  269. .AlignMiddle();
  270. }
  271. static IContainer CellValueStyle(IContainer container)
  272. {
  273. return container
  274. .DefaultTextStyle(x => x.FontColor(Colors.Black).Medium())
  275. .Border(1)
  276. .Padding(10)
  277. .AlignCenter()
  278. .AlignMiddle();
  279. }
  280. }
  281. }