#ifndef __DATA_TABLE_HPP__ #define __DATA_TABLE_HPP__ #include #include #include #include #include namespace jdi { /** * @brief 一列数值的「值 → 文本」枚举表(如 ASAM MDF v4 的 `cc_type = 7` 换算)。 * * 它描述的是**信号本身的取值含义**(例如 `0 = Off`、`1 = On`), * 与单位、注释、物理量纲无关,因此这里刻意命名为 `enumText` 而不是 `unit` / `label`。 * * 空表(`texts.empty()`)表示该列没有枚举信息,界面必须保持与以前完全一致的显示。 */ struct SignalEnumText { /** @brief 枚举取值(与采样列同一量纲,通常是整数)。 */ std::vector values; /** @brief 与 `values` 一一对应的显示文本。 */ std::vector texts; /** @brief 是否没有任何枚举信息。 */ bool Empty() const { return values.empty() || texts.size() != values.size(); } /** * @brief 按取值查找枚举文本。 * @param [IN] value 取值(物理值,未叠加界面「调整」变换)。 * @return 命中返回对应文本指针,未命中返回 nullptr。 */ const std::wstring* Find(double value) const { if (Empty() || !(value == value)) // NaN 永远不命中 { return nullptr; } for (std::size_t index = 0; index < values.size(); ++index) { if (values[index] == value) { return &texts[index]; } } // 采样值按 double 保存,整数枚举在任意位宽下都是精确可表示的, // 但个别工具会写 `1.0000001` 这类带噪声的换算表:再做一次四舍五入匹配。 const double rounded = std::nearbyint(value); if (rounded == value) { return nullptr; } for (std::size_t index = 0; index < values.size(); ++index) { if (std::nearbyint(values[index]) == rounded) { return &texts[index]; } } return nullptr; } }; /** * @brief 内存中的**列优先**数据表(与具体文件格式无关)。 * * 这是「从任意格式读出来的原始格子」:一行是表头,其余是各列的数值。 * 它**不是**应用的数据模型(不含时间轴语义、不含信号名规范化), * 只是各格式加载器(CSV / MF4 / 以后更多格式)与数据模型之间的中转结构。 * * 采用列优先(每列一整块连续内存)而不是行优先的二维数组,原因有二: * 1. 大型文件(百万行级)若按行存储会产生上百万次独立堆分配, * 每行还要额外承担 vector 的控制块与容量对齐开销,实测内存放大接近 1.5 倍; * 2. 构建信号时可以直接把整列**移动**进 `SignalData`,不再逐元素拷贝。 */ struct DataTable { std::vector headers; /**< 列名(已去除引号与首尾空白)。 */ std::vector> columns; /**< 列优先数值矩阵:columns[列][行],缺失处为 NaN。 */ /** * @brief 与 `columns` 同下标的「值 → 文本」枚举表(没有枚举信息的列为空)。 * * 加载器只在源格式自带枚举文本(如 MDF4 `cc_type = 7`)时填充; * 它不是单位、不是注释,界面只在光标读数处以尾缀形式展示。 */ std::vector columnEnumText; std::size_t rowCount = 0; /**< 有效数据行数。 */ wchar_t delimiter = L','; /**< 实际使用的分隔符(仅文本格式有意义)。 */ unsigned long long sourceBytes = 0; /**< 源文件字节数(用于按比例预留容量)。 */ /** * @brief 判断解析结果是否包含可用的表格数据。 * @return 存在列名且存在数据行时返回 true。 */ bool HasData() const { return !headers.empty() && (rowCount > 0) && (columns.size() >= headers.size()); } }; /** * @brief 加载进度信息。 * * 解析阶段会携带真实的"已读 / 总字节数";列重组与统计阶段没有字节可计, * 此时 `hasBytes` 为 false,界面应只展示百分比。 */ struct LoadProgressInfo { double ratio = 0.0; /**< 0-1 总体完成比例。 */ unsigned long long bytesRead = 0; /**< 已读取字节数。 */ unsigned long long totalBytes = 0; /**< 文件总字节数。 */ bool hasBytes = false; /**< 上述字节数字段是否有效。 */ }; /** * @brief 加载进度回调。 * * 在解析过程中按块回调;返回 false 表示请求取消,解析会尽快停止并返回失败。 */ using LoadProgress = std::function; /** * @brief 一组**共享同一时间轴**的数据列(一个「时间轴块」)。 * * 一个源文件可以包含多个这样的块:CSV 里每个 `time` 列都会开启一个新块, * MF4 里每个测量通道组(CG)是一个块。块内的各列长度一致(都等于本块的行数)。 */ struct TableBlock { std::wstring timeHeader; /**< 本块时间列的列名(原样保留,如 `time`)。 */ DataTable table; /**< 本块的列集合(**含**时间列本身)。 */ }; /** * @brief 一个源文件加载后的全部内容(可能包含多个时间轴块)。 * * ⚠️ 这是「加载器 → 数据模型」的唯一接口:加载器只负责把文件读成 * `LoadedSource`(纯格子,无应用语义),`BuildSignalGroup` 再把它规范化为 * `SignalGroup`(含时间轴去重、信号命名、调色板等应用语义)。 * 新增文件格式时**只需新增一个加载器**,数据模型与界面代码完全不用改。 */ struct LoadedSource { std::wstring sourceName; /**< 源文件名(不含目录,含扩展名)。 */ std::vector blocks; /**< 时间轴块集合(至少一个)。 */ /** @brief 是否包含可用的数据。 */ bool HasData() const { for (const TableBlock& block : blocks) { if (block.table.HasData()) { return true; } } return false; } }; } // namespace jdi #endif // __DATA_TABLE_HPP__