534 lines
22 KiB
C#
534 lines
22 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using XP.Common.Logging.Interfaces;
|
|
using XP.ReportEngine.Interfaces;
|
|
using XP.ReportEngine.Models;
|
|
|
|
namespace XP.ReportEngine.Services
|
|
{
|
|
/// <summary>
|
|
/// 处理器数据适配器实现 | Processor data adapter implementation
|
|
/// 将 XP.ImageProcessing 的 ProcessorOutput 转换为 ReportContext
|
|
/// Converts XP.ImageProcessing ProcessorOutput to ReportContext
|
|
/// </summary>
|
|
public class ProcessorDataAdapter : IReportDataAdapter
|
|
{
|
|
private readonly ILoggerService _logger;
|
|
|
|
// 处理器类型常量 | Processor type constants
|
|
private const string LineMeasurementProcessor = "LineMeasurementProcessor";
|
|
private const string BgaVoidRateProcessor = "BgaVoidRateProcessor";
|
|
private const string VoidMeasurementProcessor = "VoidMeasurementProcessor";
|
|
private const string FillRateProcessor = "FillRateProcessor";
|
|
|
|
public ProcessorDataAdapter(ILoggerService logger)
|
|
{
|
|
_logger = logger?.ForModule<ProcessorDataAdapter>() ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 将处理器输出数据适配为报告上下文 | Adapt processor output data to report context
|
|
/// </summary>
|
|
public ReportContext Adapt(List<ProcessorOutput> processorOutputs, ReportMetadata metadata)
|
|
{
|
|
if (processorOutputs == null) throw new ArgumentNullException(nameof(processorOutputs));
|
|
if (metadata == null) throw new ArgumentNullException(nameof(metadata));
|
|
|
|
_logger.Info("开始数据适配,处理器数量: {Count} | Starting data adaptation, processor count: {Count}", processorOutputs.Count);
|
|
|
|
var context = new ReportContext
|
|
{
|
|
Metadata = metadata,
|
|
ResultGroups = new List<InspectionResultGroup>(),
|
|
Images = new Dictionary<string, ImageData>(),
|
|
Properties = new Dictionary<string, object>()
|
|
};
|
|
|
|
// 多处理器输出聚合逻辑:每个 ProcessorOutput 生成一个 InspectionResultGroup | Aggregation: each ProcessorOutput becomes one InspectionResultGroup
|
|
for (var i = 0; i < processorOutputs.Count; i++)
|
|
{
|
|
var output = processorOutputs[i];
|
|
if (output == null)
|
|
{
|
|
_logger.Warn("处理器输出为 null,索引: {Index},已跳过 | Processor output is null at index: {Index}, skipped", i);
|
|
continue;
|
|
}
|
|
|
|
var group = AdaptProcessorOutput(output, i);
|
|
context.ResultGroups.Add(group);
|
|
|
|
// 将结果数据扁平化到 Properties(供模板 ${key} 表达式绑定)
|
|
// Flatten result data to Properties (for template ${key} expression binding)
|
|
if (group.Data != null)
|
|
{
|
|
foreach (var kvp in group.Data)
|
|
{
|
|
context.Properties[kvp.Key] = kvp.Value;
|
|
}
|
|
}
|
|
|
|
// 将 Classification 也放入 Properties | Also put Classification into Properties
|
|
if (!string.IsNullOrEmpty(group.Classification))
|
|
{
|
|
context.Properties["classification"] = group.Classification;
|
|
}
|
|
|
|
// 将表格数据以模板期望的 dataKey 存入 Properties
|
|
// Store table data with template-expected dataKey into Properties
|
|
if (group.TableRows != null && group.TableRows.Count > 0)
|
|
{
|
|
var tableKey = GetTableDataKey(output.ProcessorType);
|
|
if (!string.IsNullOrEmpty(tableKey))
|
|
{
|
|
context.Properties[tableKey] = group.TableRows;
|
|
}
|
|
}
|
|
|
|
// 关联标注图像(使用模板期望的 dataKey)
|
|
// Associate annotated image (using template-expected dataKey)
|
|
if (output.AnnotatedImage != null)
|
|
{
|
|
var imageKey = GetImageDataKey(output.ProcessorType);
|
|
context.Images[imageKey] = output.AnnotatedImage;
|
|
|
|
// 同时保留原始键名以兼容其他调用方 | Also keep original key for other callers
|
|
var originalKey = $"{output.ProcessorType}_{i}_annotated";
|
|
context.Images[originalKey] = output.AnnotatedImage;
|
|
}
|
|
}
|
|
|
|
_logger.Info("数据适配完成,生成 {Count} 个结果分组 | Data adaptation completed, generated {Count} result groups", context.ResultGroups.Count);
|
|
return context;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 根据处理器类型分发适配逻辑 | Dispatch adaptation logic by processor type
|
|
/// </summary>
|
|
private InspectionResultGroup AdaptProcessorOutput(ProcessorOutput output, int index)
|
|
{
|
|
var sourceId = $"{output.ProcessorType}_{index}";
|
|
|
|
return output.ProcessorType switch
|
|
{
|
|
LineMeasurementProcessor => AdaptLineMeasurement(output, sourceId),
|
|
BgaVoidRateProcessor => AdaptBgaVoidRate(output, sourceId),
|
|
VoidMeasurementProcessor => AdaptVoidMeasurement(output, sourceId),
|
|
FillRateProcessor => AdaptFillRate(output, sourceId),
|
|
_ => AdaptGeneric(output, sourceId)
|
|
};
|
|
}
|
|
|
|
#region LineMeasurementProcessor 适配 | LineMeasurementProcessor Adaptation
|
|
|
|
/// <summary>
|
|
/// 适配线测量处理器输出 | Adapt line measurement processor output
|
|
/// 提取 MeasurementType、Point1、Point2、PixelDistance、ActualDistance、Unit、Angle
|
|
/// </summary>
|
|
private InspectionResultGroup AdaptLineMeasurement(ProcessorOutput output, string sourceId)
|
|
{
|
|
_logger.Debug("适配 LineMeasurementProcessor 输出,SourceId: {SourceId} | Adapting LineMeasurementProcessor output, SourceId: {SourceId}", sourceId);
|
|
|
|
var data = output.OutputData ?? new Dictionary<string, object>();
|
|
var group = new InspectionResultGroup
|
|
{
|
|
ProcessorType = LineMeasurementProcessor,
|
|
SourceId = sourceId,
|
|
Classification = string.Empty,
|
|
Data = new Dictionary<string, object>
|
|
{
|
|
["measurementType"] = GetValueOrDefault<string>(data, "MeasurementType", string.Empty),
|
|
["point1"] = GetValueOrDefault<object>(data, "Point1", null),
|
|
["point2"] = GetValueOrDefault<object>(data, "Point2", null),
|
|
["pixelDistance"] = GetValueOrDefault<double>(data, "PixelDistance", 0.0),
|
|
["actualDistance"] = GetValueOrDefault<double>(data, "ActualDistance", 0.0),
|
|
["unit"] = GetValueOrDefault<string>(data, "Unit", string.Empty),
|
|
["angle"] = GetValueOrDefault<double>(data, "Angle", 0.0)
|
|
},
|
|
TableRows = new List<Dictionary<string, object>>()
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region BgaVoidRateProcessor 适配 | BgaVoidRateProcessor Adaptation
|
|
|
|
/// <summary>
|
|
/// 适配 BGA 气泡率处理器输出 | Adapt BGA void rate processor output
|
|
/// 提取 BgaCount、BgaBalls 列表转 TableRows、VoidRate、FillRate、TotalBgaArea、TotalVoidArea、Classification、VoidLimit
|
|
/// </summary>
|
|
private InspectionResultGroup AdaptBgaVoidRate(ProcessorOutput output, string sourceId)
|
|
{
|
|
_logger.Debug("适配 BgaVoidRateProcessor 输出,SourceId: {SourceId} | Adapting BgaVoidRateProcessor output, SourceId: {SourceId}", sourceId);
|
|
|
|
var data = output.OutputData ?? new Dictionary<string, object>();
|
|
var group = new InspectionResultGroup
|
|
{
|
|
ProcessorType = BgaVoidRateProcessor,
|
|
SourceId = sourceId,
|
|
Classification = GetValueOrDefault<string>(data, "Classification", string.Empty),
|
|
Data = new Dictionary<string, object>
|
|
{
|
|
["bgaCount"] = GetValueOrDefault<int>(data, "BgaCount", 0),
|
|
["voidRate"] = GetValueOrDefault<double>(data, "VoidRate", 0.0),
|
|
["fillRate"] = GetValueOrDefault<double>(data, "FillRate", 0.0),
|
|
["totalBgaArea"] = GetValueOrDefault<double>(data, "TotalBgaArea", 0.0),
|
|
["totalVoidArea"] = GetValueOrDefault<double>(data, "TotalVoidArea", 0.0),
|
|
["voidLimit"] = GetValueOrDefault<double>(data, "VoidLimit", 0.0)
|
|
},
|
|
TableRows = ConvertBgaBallsToTableRows(data)
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 将 BgaBalls 列表转换为表格行 | Convert BgaBalls list to table rows
|
|
/// 每个焊球一行,包含 index、voidRate、area、classification
|
|
/// </summary>
|
|
private List<Dictionary<string, object>> ConvertBgaBallsToTableRows(Dictionary<string, object> data)
|
|
{
|
|
var tableRows = new List<Dictionary<string, object>>();
|
|
var bgaBalls = GetListValue(data, "BgaBalls");
|
|
|
|
if (bgaBalls == null || bgaBalls.Count == 0)
|
|
{
|
|
return tableRows;
|
|
}
|
|
|
|
for (var i = 0; i < bgaBalls.Count; i++)
|
|
{
|
|
var ball = bgaBalls[i];
|
|
var row = new Dictionary<string, object>
|
|
{
|
|
["index"] = i + 1,
|
|
["voidRate"] = GetNestedValue<double>(ball, "VoidRate", 0.0),
|
|
["area"] = GetNestedValue<double>(ball, "Area", 0.0),
|
|
["classification"] = GetNestedValue<string>(ball, "Classification", string.Empty)
|
|
};
|
|
tableRows.Add(row);
|
|
}
|
|
|
|
return tableRows;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region VoidMeasurementProcessor 适配 | VoidMeasurementProcessor Adaptation
|
|
|
|
/// <summary>
|
|
/// 适配空隙测量处理器输出 | Adapt void measurement processor output
|
|
/// 提取 RoiArea、TotalVoidArea、VoidRate、VoidLimit、VoidCount、MaxVoidArea、Classification、Voids 列表转 TableRows
|
|
/// </summary>
|
|
private InspectionResultGroup AdaptVoidMeasurement(ProcessorOutput output, string sourceId)
|
|
{
|
|
_logger.Debug("适配 VoidMeasurementProcessor 输出,SourceId: {SourceId} | Adapting VoidMeasurementProcessor output, SourceId: {SourceId}", sourceId);
|
|
|
|
var data = output.OutputData ?? new Dictionary<string, object>();
|
|
var group = new InspectionResultGroup
|
|
{
|
|
ProcessorType = VoidMeasurementProcessor,
|
|
SourceId = sourceId,
|
|
Classification = GetValueOrDefault<string>(data, "Classification", string.Empty),
|
|
Data = new Dictionary<string, object>
|
|
{
|
|
["roiArea"] = GetValueOrDefault<double>(data, "RoiArea", 0.0),
|
|
["totalVoidArea"] = GetValueOrDefault<double>(data, "TotalVoidArea", 0.0),
|
|
["voidRate"] = GetValueOrDefault<double>(data, "VoidRate", 0.0),
|
|
["voidLimit"] = GetValueOrDefault<double>(data, "VoidLimit", 0.0),
|
|
["voidCount"] = GetValueOrDefault<int>(data, "VoidCount", 0),
|
|
["maxVoidArea"] = GetValueOrDefault<double>(data, "MaxVoidArea", 0.0)
|
|
},
|
|
TableRows = ConvertVoidsToTableRows(data)
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 将 Voids 列表转换为表格行 | Convert Voids list to table rows
|
|
/// 每个空隙一行,包含 index、area、areaPercent、centerX、centerY
|
|
/// </summary>
|
|
private List<Dictionary<string, object>> ConvertVoidsToTableRows(Dictionary<string, object> data)
|
|
{
|
|
var tableRows = new List<Dictionary<string, object>>();
|
|
var voids = GetListValue(data, "Voids");
|
|
|
|
if (voids == null || voids.Count == 0)
|
|
{
|
|
return tableRows;
|
|
}
|
|
|
|
for (var i = 0; i < voids.Count; i++)
|
|
{
|
|
var voidItem = voids[i];
|
|
var row = new Dictionary<string, object>
|
|
{
|
|
["index"] = i + 1,
|
|
["area"] = GetNestedValue<double>(voidItem, "Area", 0.0),
|
|
["areaPercent"] = GetNestedValue<double>(voidItem, "AreaPercent", 0.0),
|
|
["centerX"] = GetNestedValue<double>(voidItem, "CenterX", 0.0),
|
|
["centerY"] = GetNestedValue<double>(voidItem, "CenterY", 0.0)
|
|
};
|
|
tableRows.Add(row);
|
|
}
|
|
|
|
return tableRows;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region FillRateProcessor 适配 | FillRateProcessor Adaptation
|
|
|
|
/// <summary>
|
|
/// 适配填锡率处理器输出 | Adapt fill rate processor output
|
|
/// 提取 FillRate、VoidRate、FullDistance、FillDistance、THTLimit、Classification、E1-E4 椭圆几何数据
|
|
/// </summary>
|
|
private InspectionResultGroup AdaptFillRate(ProcessorOutput output, string sourceId)
|
|
{
|
|
_logger.Debug("适配 FillRateProcessor 输出,SourceId: {SourceId} | Adapting FillRateProcessor output, SourceId: {SourceId}", sourceId);
|
|
|
|
var data = output.OutputData ?? new Dictionary<string, object>();
|
|
var group = new InspectionResultGroup
|
|
{
|
|
ProcessorType = FillRateProcessor,
|
|
SourceId = sourceId,
|
|
Classification = GetValueOrDefault<string>(data, "Classification", string.Empty),
|
|
Data = new Dictionary<string, object>
|
|
{
|
|
["fillRate"] = GetValueOrDefault<double>(data, "FillRate", 0.0),
|
|
["voidRate"] = GetValueOrDefault<double>(data, "VoidRate", 0.0),
|
|
["fullDistance"] = GetValueOrDefault<double>(data, "FullDistance", 0.0),
|
|
["fillDistance"] = GetValueOrDefault<double>(data, "FillDistance", 0.0),
|
|
["thtLimit"] = GetValueOrDefault<double>(data, "THTLimit", 0.0),
|
|
["e1"] = GetValueOrDefault<object>(data, "E1", null),
|
|
["e2"] = GetValueOrDefault<object>(data, "E2", null),
|
|
["e3"] = GetValueOrDefault<object>(data, "E3", null),
|
|
["e4"] = GetValueOrDefault<object>(data, "E4", null)
|
|
},
|
|
TableRows = new List<Dictionary<string, object>>()
|
|
};
|
|
|
|
return group;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 通用适配 | Generic Adaptation
|
|
|
|
/// <summary>
|
|
/// 通用处理器适配(未知类型)| Generic processor adaptation (unknown type)
|
|
/// 将所有 OutputData 键值对直接映射到 Data 字典
|
|
/// </summary>
|
|
private InspectionResultGroup AdaptGeneric(ProcessorOutput output, string sourceId)
|
|
{
|
|
_logger.Warn("未知处理器类型: {ProcessorType},使用通用适配 | Unknown processor type: {ProcessorType}, using generic adaptation", output.ProcessorType);
|
|
|
|
var data = output.OutputData ?? new Dictionary<string, object>();
|
|
var group = new InspectionResultGroup
|
|
{
|
|
ProcessorType = output.ProcessorType ?? "Unknown",
|
|
SourceId = sourceId,
|
|
Classification = GetValueOrDefault<string>(data, "Classification", string.Empty),
|
|
Data = new Dictionary<string, object>(),
|
|
TableRows = new List<Dictionary<string, object>>()
|
|
};
|
|
|
|
// 将所有键值对转为小驼峰命名映射 | Map all key-value pairs with camelCase naming
|
|
foreach (var kvp in data)
|
|
{
|
|
var camelKey = ToCamelCase(kvp.Key);
|
|
group.Data[camelKey] = kvp.Value;
|
|
}
|
|
|
|
return group;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 辅助方法 | Helper Methods
|
|
|
|
/// <summary>
|
|
/// 从字典中获取值,缺失时使用默认值并记录警告 | Get value from dictionary, use default and log warning if missing
|
|
/// </summary>
|
|
private T GetValueOrDefault<T>(Dictionary<string, object> data, string key, T defaultValue)
|
|
{
|
|
if (data == null || !data.TryGetValue(key, out var value) || value == null)
|
|
{
|
|
_logger.Warn("处理器输出缺少键: {Key},使用默认值: {Default} | Processor output missing key: {Key}, using default: {Default}", key, defaultValue);
|
|
return defaultValue;
|
|
}
|
|
|
|
try
|
|
{
|
|
return ConvertValue<T>(value);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Warn("键 {Key} 的值类型转换失败: {Message},使用默认值 | Value type conversion failed for key {Key}: {Message}, using default", key, ex.Message);
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从字典中获取列表值 | Get list value from dictionary
|
|
/// </summary>
|
|
private IList GetListValue(Dictionary<string, object> data, string key)
|
|
{
|
|
if (data == null || !data.TryGetValue(key, out var value) || value == null)
|
|
{
|
|
_logger.Warn("处理器输出缺少列表键: {Key},返回空列表 | Processor output missing list key: {Key}, returning empty list", key);
|
|
return null;
|
|
}
|
|
|
|
if (value is IList list)
|
|
{
|
|
return list;
|
|
}
|
|
|
|
_logger.Warn("键 {Key} 的值不是列表类型 | Value for key {Key} is not a list type", key);
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从嵌套对象中获取属性值 | Get property value from nested object
|
|
/// </summary>
|
|
private T GetNestedValue<T>(object obj, string propertyName, T defaultValue)
|
|
{
|
|
if (obj == null) return defaultValue;
|
|
|
|
// 字典访问 | Dictionary access
|
|
if (obj is IDictionary<string, object> dict)
|
|
{
|
|
if (dict.TryGetValue(propertyName, out var dictValue) && dictValue != null)
|
|
{
|
|
try
|
|
{
|
|
return ConvertValue<T>(dictValue);
|
|
}
|
|
catch
|
|
{
|
|
return defaultValue;
|
|
}
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
// 反射访问 | Reflection access
|
|
var type = obj.GetType();
|
|
var propInfo = type.GetProperty(propertyName,
|
|
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase);
|
|
if (propInfo != null)
|
|
{
|
|
try
|
|
{
|
|
var value = propInfo.GetValue(obj);
|
|
if (value == null) return defaultValue;
|
|
return ConvertValue<T>(value);
|
|
}
|
|
catch
|
|
{
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 类型转换辅助方法 | Type conversion helper
|
|
/// </summary>
|
|
private T ConvertValue<T>(object value)
|
|
{
|
|
if (value == null) return default;
|
|
|
|
var targetType = typeof(T);
|
|
|
|
// 直接类型匹配 | Direct type match
|
|
if (value is T typedValue)
|
|
{
|
|
return typedValue;
|
|
}
|
|
|
|
// 处理 nullable 类型 | Handle nullable types
|
|
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
|
|
|
|
// object 类型直接返回 | Return directly for object type
|
|
if (underlyingType == typeof(object))
|
|
{
|
|
return (T)value;
|
|
}
|
|
|
|
// 数值类型转换 | Numeric type conversion
|
|
if (underlyingType == typeof(double))
|
|
{
|
|
return (T)(object)Convert.ToDouble(value);
|
|
}
|
|
if (underlyingType == typeof(int))
|
|
{
|
|
return (T)(object)Convert.ToInt32(value);
|
|
}
|
|
if (underlyingType == typeof(float))
|
|
{
|
|
return (T)(object)Convert.ToSingle(value);
|
|
}
|
|
if (underlyingType == typeof(long))
|
|
{
|
|
return (T)(object)Convert.ToInt64(value);
|
|
}
|
|
|
|
// 字符串转换 | String conversion
|
|
if (underlyingType == typeof(string))
|
|
{
|
|
return (T)(object)value.ToString();
|
|
}
|
|
|
|
// 通用转换 | General conversion
|
|
return (T)Convert.ChangeType(value, underlyingType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 将 PascalCase 转换为 camelCase | Convert PascalCase to camelCase
|
|
/// </summary>
|
|
private string ToCamelCase(string input)
|
|
{
|
|
if (string.IsNullOrEmpty(input)) return input;
|
|
if (input.Length == 1) return input.ToLowerInvariant();
|
|
return char.ToLowerInvariant(input[0]) + input.Substring(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 根据处理器类型获取模板中对应的图像 dataKey | Get template image dataKey by processor type
|
|
/// </summary>
|
|
private static string GetImageDataKey(string processorType)
|
|
{
|
|
return processorType switch
|
|
{
|
|
LineMeasurementProcessor => "lineMeasurementImage",
|
|
BgaVoidRateProcessor => "bgaInspectionImage",
|
|
VoidMeasurementProcessor => "voidInspectionImage",
|
|
FillRateProcessor => "viaFillImage",
|
|
_ => $"{processorType}_image"
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 根据处理器类型获取模板中对应的表格 dataKey | Get template table dataKey by processor type
|
|
/// </summary>
|
|
private static string GetTableDataKey(string processorType)
|
|
{
|
|
return processorType switch
|
|
{
|
|
BgaVoidRateProcessor => "bgaBallsTable",
|
|
VoidMeasurementProcessor => "voidsTable",
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|