一、##CA 数组通道:从「按魔数猜」改成「按记录容量校验」
- 头字段全部按块长度做边界检查后读取:+0 ca_type / +1 ca_storage /
+2 ndim(U16) / +4 flags(U32) / +8 ca_byte_offset_base(U16) /
+10 ca_inval_bit_pos(U16) / +16 dim_size[](U64,步长 8),
其后才是**可选**的 cycle_count[](U32)。
⚠️ 实测 CANape 写出的文件没有 cycle_count(块长度恰好到 dim_size 就结束),
盲读会把下一个块头当数字(实测得到 1481909027)。
- 展开条件(任何一条不满足就原样保留,只显示元素 0,绝不猜):
ca_storage == 0;ca_type <= 2(0 数组 / 1 缩放轴 / 2 区间轴,三者物理布局相同);
cn_bit_count ∈ [1, 64];cycle_count 每维 <= 1(元素不被拆到多条记录);
bit_offset + 元素数 × 位宽 <= cg_data_bytes × 8(真正的完整性校验)。
规模护栏:元素数 <= 16384 且 元素数 × 记录数 <= 32M 单元格
(取代原来的 count > 1024 魔数)。
- 元素名按 dim_size 从末维向前取余,1/2/3 维分别得到 [i] / [i][j] / [i][j][k]。
- MF4-Record.MF4 仍是 283 个信号(25 个 ##CA 全部通过新的容量校验,最大 7x9=63)。
二、cc_type = 8(值 -> 文本,区间表)补齐
- cc_val[] 交错存放 (下限, 上限),cc_ref[4 + i] 是第 i 段文本(最后一个引用是默认文本,不使用);
cc_val_count 必须是偶数(奇数说明表残缺,整表丢弃);下限 > 上限时交换。
- SignalEnumText 新增 rangeMin / rangeMax / rangeTexts,并拆出 HasExact() / HasRanges();
Find() 先查精确表(cc_type = 7)再查区间表(cc_type = 8,闭区间)。
- 界面没有改动:两种枚举共用同一出口,只在**光标取值标注**追加 " = <文本>",
常规状态零变化。
三、修复:稠密包络带下改变线宽时竖直沿不变粗
- 根因:包络带每列只有 1 个 x 坐标,旧实现只在 top/bottom 各外扩半个线宽
=> 水平段会变粗,但方波竖直沿(上升/下降段)的横向宽度恒为 1 个像素列。
- 修法:EmitEnvelopeBand 新增 halfWidth 形参,先对上下包络做横向窗口膨胀
(top 取窗口最小、bottom 取窗口最大,半径 ceil(halfWidth / 列距)),再纵向各外扩;
膨胀只在同一段内生效,不会跨 NaN 断点连线。
验证(全离屏,不占用鼠标/键盘/屏幕)
- .preflight/convcheck 18/18:新增 3 维命名与取值、元素放不进记录时必须拒绝展开、
cc_type = 8 的命中/未命中。
- 新增 .preflight/round5check 5/5:合成 20 万采样数字信号(必然走包络带),
在网格中间行测量竖直沿横向像素宽度:线宽 1px -> 6px,10px -> 30px。
- A/B:临时把横向膨胀半径置 0(等价旧行为)=> 1px -> 2px、10px -> 2px 且如实 FAIL。
- 回归:check 137/137、editcheck 20/20、round3check 31/31、round4check 34/34;
真实 MF4 信号数 283/8/18/24 不变;make release / make debug 零警告;
TargetCheck.exe 808,448 字节(789.5 KiB)。
文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 165-170 条硬约束与文末本轮小节
(含「6 位有效数字」的口径说明)。
214 lines
7.6 KiB
C++
214 lines
7.6 KiB
C++
#ifndef __DATA_TABLE_HPP__
|
||
#define __DATA_TABLE_HPP__
|
||
|
||
#include <cmath>
|
||
#include <cstddef>
|
||
#include <functional>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
/**
|
||
* @brief 一列数值的「值 → 文本」枚举表(如 ASAM MDF v4 的 `cc_type = 7` / `8` 换算)。
|
||
*
|
||
* 它描述的是**信号本身的取值含义**(例如 `0 = Off`、`1 = On`),
|
||
* 与单位、注释、物理量纲无关,因此这里刻意命名为 `enumText` 而不是 `unit` / `label`。
|
||
*
|
||
* 支持两种表(可同时存在,界面只用来给取值追加文本,不改变数值):
|
||
* * **精确表** `values[] ↔ texts[]`(`cc_type = 7`,值 → 文本);
|
||
* * **区间表** `[rangeMin[i], rangeMax[i]] ↔ rangeTexts[i]`(`cc_type = 8`,值区间 → 文本)。
|
||
*
|
||
* 空表(`Empty()`)表示该列没有枚举信息,界面必须保持与以前完全一致的显示。
|
||
*/
|
||
struct SignalEnumText
|
||
{
|
||
/** @brief 枚举取值(与采样列同一量纲,通常是整数)。 */
|
||
std::vector<double> values;
|
||
|
||
/** @brief 与 `values` 一一对应的显示文本。 */
|
||
std::vector<std::wstring> texts;
|
||
|
||
/**
|
||
* @brief 区间枚举的下限(`cc_type = 8`,与 `rangeMax`/`rangeTexts` 一一对应)。
|
||
*
|
||
* ⚠️ 三个数组必须同长度;长度不一致时整张区间表视为无效(`Empty()` 为真),
|
||
* 绝不能把错位的区间套到取值上。
|
||
*/
|
||
std::vector<double> rangeMin;
|
||
|
||
/** @brief 区间枚举的上限(闭区间)。 */
|
||
std::vector<double> rangeMax;
|
||
|
||
/** @brief 与 `rangeMin`/`rangeMax` 一一对应的显示文本。 */
|
||
std::vector<std::wstring> rangeTexts;
|
||
|
||
/** @brief 精确表是否有效。 */
|
||
bool HasExact() const { return !values.empty() && texts.size() == values.size(); }
|
||
|
||
/** @brief 区间表是否有效(三个数组同长度且非空)。 */
|
||
bool HasRanges() const
|
||
{
|
||
return !rangeMin.empty() && rangeMax.size() == rangeMin.size() &&
|
||
rangeTexts.size() == rangeMin.size();
|
||
}
|
||
|
||
/** @brief 是否没有任何枚举信息。 */
|
||
bool Empty() const { return !HasExact() && !HasRanges(); }
|
||
|
||
/**
|
||
* @brief 按取值查找枚举文本。
|
||
*
|
||
* 先查**精确表**(枚举值优先),未命中再查**区间表**(闭区间)。
|
||
*
|
||
* @param [IN] value 取值(物理值,未叠加界面「调整」变换)。
|
||
* @return 命中返回对应文本指针,未命中返回 nullptr。
|
||
*/
|
||
const std::wstring* Find(double value) const
|
||
{
|
||
if (Empty() || !(value == value)) // NaN 永远不命中
|
||
{
|
||
return nullptr;
|
||
}
|
||
if (HasExact())
|
||
{
|
||
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)
|
||
{
|
||
for (std::size_t index = 0; index < values.size(); ++index)
|
||
{
|
||
if (std::nearbyint(values[index]) == rounded)
|
||
{
|
||
return &texts[index];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (HasRanges())
|
||
{
|
||
for (std::size_t index = 0; index < rangeMin.size(); ++index)
|
||
{
|
||
if (value >= rangeMin[index] && value <= rangeMax[index])
|
||
{
|
||
return &rangeTexts[index];
|
||
}
|
||
}
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
/** @brief 枚举项总数(精确表 + 区间表,供诊断/日志使用)。 */
|
||
std::size_t Count() const
|
||
{
|
||
return (HasExact() ? values.size() : 0u) + (HasRanges() ? rangeMin.size() : 0u);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @brief 内存中的**列优先**数据表(与具体文件格式无关)。
|
||
*
|
||
* 这是「从任意格式读出来的原始格子」:一行是表头,其余是各列的数值。
|
||
* 它**不是**应用的数据模型(不含时间轴语义、不含信号名规范化),
|
||
* 只是各格式加载器(CSV / MF4 / 以后更多格式)与数据模型之间的中转结构。
|
||
*
|
||
* 采用列优先(每列一整块连续内存)而不是行优先的二维数组,原因有二:
|
||
* 1. 大型文件(百万行级)若按行存储会产生上百万次独立堆分配,
|
||
* 每行还要额外承担 vector 的控制块与容量对齐开销,实测内存放大接近 1.5 倍;
|
||
* 2. 构建信号时可以直接把整列**移动**进 `SignalData`,不再逐元素拷贝。
|
||
*/
|
||
struct DataTable
|
||
{
|
||
std::vector<std::wstring> headers; /**< 列名(已去除引号与首尾空白)。 */
|
||
std::vector<std::vector<double>> columns; /**< 列优先数值矩阵:columns[列][行],缺失处为 NaN。 */
|
||
/**
|
||
* @brief 与 `columns` 同下标的「值 → 文本」枚举表(没有枚举信息的列为空)。
|
||
*
|
||
* 加载器只在源格式自带枚举文本(如 MDF4 `cc_type = 7`)时填充;
|
||
* 它不是单位、不是注释,界面只在光标读数处以尾缀形式展示。
|
||
*/
|
||
std::vector<SignalEnumText> 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<bool(const LoadProgressInfo& info)>;
|
||
|
||
/**
|
||
* @brief 一组**共享同一时间轴**的数据列(一个「时间轴块」)。
|
||
*
|
||
* 一个源文件可以包含多个这样的块:CSV 里每个 `time` 列都会开启一个新块,
|
||
* MF4 里每个测量通道组(CG)是一个块。块内的各列长度一致(都等于本块的行数)。
|
||
*/
|
||
struct TableBlock
|
||
{
|
||
std::wstring timeHeader; /**< 本块时间列的列名(原样保留,如 `time`)。 */
|
||
DataTable table; /**< 本块的列集合(**含**时间列本身)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 一个源文件加载后的全部内容(可能包含多个时间轴块)。
|
||
*
|
||
* ⚠️ 这是「加载器 → 数据模型」的唯一接口:加载器只负责把文件读成
|
||
* `LoadedSource`(纯格子,无应用语义),`BuildSignalGroup` 再把它规范化为
|
||
* `SignalGroup`(含时间轴去重、信号命名、调色板等应用语义)。
|
||
* 新增文件格式时**只需新增一个加载器**,数据模型与界面代码完全不用改。
|
||
*/
|
||
struct LoadedSource
|
||
{
|
||
std::wstring sourceName; /**< 源文件名(不含目录,含扩展名)。 */
|
||
std::vector<TableBlock> blocks; /**< 时间轴块集合(至少一个)。 */
|
||
|
||
/** @brief 是否包含可用的数据。 */
|
||
bool HasData() const
|
||
{
|
||
for (const TableBlock& block : blocks)
|
||
{
|
||
if (block.table.HasData())
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
};
|
||
|
||
} // namespace jdi
|
||
|
||
#endif // __DATA_TABLE_HPP__
|