Files
Just-DataInspector/Utilities/Src/DataModel.cpp
T
DeepSeek-V4.1-Flash 181f95cc3a MF4 补 ##SD/##SI/VLSD + 信号枚举文本 enumText + .mat 改随机读(流式进度)
1) 修复上轮记录的两个已知问题
   - MatLoader: LoadPlainObject 不再为拿 dims 调 ReadDataset(整块解码第二次),
     改用 ReadDatasetInfo;普通 v7.3 大矩阵的时间/内存不再翻倍。
   - MdfFile::TextBlock 注释与实现对齐:真的支持 ##MD(UTF-8 XML,无长度前缀)。

2) 信号枚举信息(以 TestData/NEW-MF4.mf4 为目标)
   - 新增 SignalEnumText(DataTable.hpp):values[] + texts[] + Empty()/Find(),
     命名用 enumText —— 它描述"取值代表哪个状态",不是单位。
   - DataTable::columnEnumText 与 columns 同下标;SignalData::enumText 由
     BuildSignalGroup 移动赋值;其它格式(CSV/.mat)保持为空。
   - MF4 cc_type = 7:cc_val[i](F64,数据段 +24)↔ cc_ref[4 + i](##TX/##MD 文本),
     最后一个引用是默认文本,不使用。
   - 界面只在**光标取值标注**追加 " = <枚举文本>"(等号两侧各一个空格、无尖括号),
     查表用采样原值(ApplyDrawTransform 之前);常规状态显示一个像素都不变。
   - 实测:NEW-MF4.mf4 8 信号 / 4 条带枚举(32+5+32+5),MF4-Record.MF4 另有 2 种(12/32)。

3) MF4 支持范围
   - ##SI 采样索引:只有索引 ≥2 条且全部 < 周期数才生效(逐记录有效性位图)。
     实测部分工具给每个通道挂只含常量 513/4 的 ##SI,必须忽略,否则整列变 NaN。
   - ##SD + VLSD(cn_type == 1):##SD 数据段是「UINT32 长度 + 字节」样本序列,
     记录里放 64 位偏移;AppendSignalData 递归展开 SD/DZ/DL/HL(单通道上限 512MB),
     DecodeVlsdValue 先按直接偏移、失败再按 >>1 回退,取样本第一个元素。
   - CN 的 cn_data / md_unit / md_comment 下标按链接总数反推:8 链接(MDF 4.0 风格)
     = 换算[4]/数据[5]/单位[6]/注释[7],9 链接(4.1)= [5]/[6]/[7]/[8]。
     实测 Simulink 导出的“4.10”仍写 8 链接,不能写死 Link(cn,4/5)。
   - 主通道:cn_sync_type 1/2/3(时间/角度/距离)→ 名字 t/time/timestamp/zeit/时间
     (含 t[...])→ 第一个数值通道兜底;cg_flags 第 2 位(远程主通道)跳过 sync 判定。
   - 记录长度优先 cg_data_bytes(CG 数据段 +24);旧实现把 U8(cgData) 当
     cg_record_id_len,其实是 cg_flags 低字节(总线事件位)。
   - 进度:先走 DG 链得到真实组数(不再写死 64),hasBytes = true,bytesRead 取
     MdfFile::ReadAt 累计(夹到文件大小);4MB 分块读取检查取消。
   - ##DZ:数据段前有 28 字节头(original_type/zip_type/param/original_size/zip_size),
     旧实现整段当 zlib 流是错的;只支持纯 deflate(zip_type = 0)。
   - 一个信号都没有时,错误信息附带「布局无法解析 / 缺少时间通道 / 空数据组」计数。

4) .mat:内存映射 → 随机读 + 元数据块缓存(消除 SEH 风险)
   - MatHdf5::Open 改为 CreateFileW(FILE_FLAG_SEQUENTIAL_SCAN) + ReadFile;
     删除 MappedBytes,改为 ReadAt / Cached(16 × 64KB LRU + 上次命中槽)/ Tag /
     ReadData / ReadCString。I/O 失败只会变成 false,不再触发页访问异常。
   - 数据读取:连续布局按 4MB 切片;分块布局按需 + 64KB 对齐。
     固定 4MB 预读在交错存放的分块数据集上会把读盘量放大到 2.2 倍(2874MB/1293MB),
     改后 1301MB。
   - 关键提速:dims=[N]、chunk=[32768,1] 归一化后 rank=2、每行 1 字节,逐行算坐标
     是 12.9 亿次迭代;新增「rank == 2 且 chunkDims[1] == dims[1]」快路径(整块一次
     sink)。BIG_MAT.mat(1.29GB / 5 × 16,142,471):21.5s → 7.9s,私有内存 +741MB。
   - 进度:Open 立刻上报「0 / 总大小」,之后每读一块回调 ReadProgress(bytesRead,total)
     → MatLoader 映射到 0.02–0.98 并单调夹紧(字节进度与逐流解码取较大值),收尾 1.0。
     遮罩层百分比不再一上来就 100%;取消会中止(StreamDataset 返回 false),不留 NaN 空洞。
   - 连续布局数据不足(文件被截断)现在明确失败,不再静默截短。

验证(全部离屏:无可见窗口、不注入鼠标键盘屏幕)
- 新增 .preflight/perf/loadercheck(数据层)32/32:
  NEW-MF4 8 信号 / 4 条枚举 / 枚举文本逐条核对 / 9396 与 94 行;
  MF4-Record 124 信号 / 3 轴 / 4 条枚举;data-sample.mat 4 组 8 信号 / 行数;
  BIG_MAT.mat 5 信号 × 16,142,471 + 进度(total 已知、ratio 单调、bytes 递增、
  起点 ≤0.05、终值 1.0、更新 ≥10 次)。
- 新增 .preflight/perf/enumui(真实 Application + SW_HIDE)12/12:
  只显示 gVR_WinOpenReq[0],光标落在枚举采样点上 —— 标签宽度 265px;
  临时清空 enumText 后 25px;差值 240px 与 " = WinOpenReqAtDrvr_Idle" 的
  UiMeasureTextWidth 完全一致;恢复后宽度回到 265px。
- 回归:datacheck 69/69、matcheck 21/21、matui 15/15(loading bytes 达到总大小)、
  verify 49/49。
- make release / make debug 零警告。Build/Release/TargetCheck.exe = 820,224 字节
  (上轮 785,920,+34KB)。(Target.exe 正在被用户运行的程序占用,链接步骤未覆盖它。)
2026-10-20 01:56:47 +08:00

1594 lines
52 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.
// DataModel 实现分片 —— 本文件只包含 DataModel 的**成员函数定义**(纯代码搬移,行为与拆分前完全一致)。
//
// 共用的常量与自由函数在 DataModelInternal.hpp(命名空间 `dmdetail`,全部 inline)。
// 分片按“职责”划分:核心(状态/布局/几何)、渲染、交互与编辑,见各文件名。
#include "DataModelInternal.hpp"
namespace jdi
{
const wchar_t* DrawModeName(DrawMode mode)
{
switch (mode)
{
case DrawMode::LINE:
return L"线段";
case DrawMode::SCATTER:
return L"散点";
case DrawMode::LINE_SCATTER:
return L"点线";
default:
return L"线段";
}
}
// ============================ SignalGroup 构建 ============================
/**
* @brief 粗粒度包络层每个分组覆盖的采样数。
*
* 16KB 的双精度分组可覆盖 1024 个采样(内存开销 = 采样数的 1.6%),
* 但会让"每列采样数略多于分组跨度"的场景退化为逐点扫描。
* 取 256 时:额外内存约为采样数的 0.8%,最坏情况的扫描量也能压低到
* 每列 ≤256 个采样(2400 列 ≈ 0.6M 次读取,约 1ms)。
*/
const std::size_t kCoarseStride = 256;
/**
* @brief 单条信号的取值范围统计结果。
*/
struct SignalStats
{
double minValue = 0.0; /**< 有限采样值最小值。 */
double maxValue = 0.0; /**< 有限采样值最大值。 */
std::size_t finiteCount = 0; /**< 有限采样点数量。 */
};
/**
* @brief 为**只有一个有效采样点**的信号设置初始绘制样式。
*
* 单个数据点在「线段」模式下完全画不出来,因此初始就用「散点」并取**最大线宽**
* (散点半径随线宽线性增长,取最大值才能看清那个点)。
*
* ⚠️ 用户 2026-10-16 要求:初始绘制方式由「点线」改为「**散点**」,并且信号面板
* 「样式」行里的「线段」/「点线」两个按钮对该信号**置灰**
* (见 `SignalPanel::ModeOptionEnabled`)。
*
* ⚠️ 曾经还对「有效采样点 2…16」的信号也改用「点线 + 2px」—— 该需求已**作废**
*(用户 2026-10-15 明确取消:放大后自动切点线的机制已覆盖那个场景)。
*
* 只在**新建信号**时调用(用户改过样式后不再覆盖)。
*
* @param [IN] finiteCount 有效采样点数量。
* @param [IN,OUT] style 待初始化的显示外观。
*/
void ApplySinglePointDefaultStyle(std::size_t finiteCount, SignalStyle& style)
{
if (finiteCount == 1)
{
style.mode = DrawMode::SCATTER;
style.lineWidth = kSinglePointSignalLineWidth;
}
}
/**
* @brief 缓存「有效采样点很少」的信号的**全部有效点**(时刻 + 取值,按时间升序)。
*
* 供光标吸附(二分查找最近数据点、O(log n) 判定稀疏)与单点信号的初始视图使用。
* 有效点很多时(> `kSparseTimesLimit`)不做任何缓存(用不到,也没必要占内存)。
*
* @param [IN] axis 该信号使用的时间轴。
* @param [IN,OUT] signal 目标信号。
*/
void CacheSparseSamples(const TimeAxis& axis, SignalData& signal)
{
signal.sparseTimes.clear();
signal.sparseValues.clear();
if (signal.finiteCount == 0 || signal.finiteCount > kSparseTimesLimit)
{
return;
}
const std::size_t count = std::min(axis.time.size(), signal.values.size());
signal.sparseTimes.reserve(signal.finiteCount);
signal.sparseValues.reserve(signal.finiteCount);
for (std::size_t index = 0; index < count; ++index)
{
const double value = signal.values[index];
const double time = axis.time[index];
if (!std::isfinite(value) || !std::isfinite(time))
{
continue;
}
signal.sparseTimes.push_back(time);
signal.sparseValues.push_back(value);
}
// 非单调时间轴:按时间排序,保证后续二分查找合法
//(单调时间轴的采样本身就是升序,无需排序)。
if (!axis.monotonic && signal.sparseTimes.size() > 1)
{
std::vector<std::size_t> order(signal.sparseTimes.size());
for (std::size_t index = 0; index < order.size(); ++index)
{
order[index] = index;
}
std::stable_sort(order.begin(), order.end(), [&signal](std::size_t a, std::size_t b)
{ return signal.sparseTimes[a] < signal.sparseTimes[b]; });
std::vector<double> times;
std::vector<double> values;
times.reserve(order.size());
values.reserve(order.size());
for (const std::size_t index : order)
{
times.push_back(signal.sparseTimes[index]);
values.push_back(signal.sparseValues[index]);
}
signal.sparseTimes = std::move(times);
signal.sparseValues = std::move(values);
}
}
/**
* @brief 统计一列采样的最小/最大值与有效点数量,并顺带构建粗粒度包络层。
*
* 两者在同一个循环内完成,避免为粗粒度层多扫一遍数据(大文件下那是一整趟内存遍历)。
*
* @param [IN] values 采样列。
* @param [OUT] coarseMin 粗粒度分组最小值输出。
* @param [OUT] coarseMax 粗粒度分组最大值输出。
* @return 统计结果。
*/
SignalStats ComputeStats(const std::vector<double>& values, std::vector<double>& coarseMin,
std::vector<double>& coarseMax)
{
SignalStats stats;
double low = std::numeric_limits<double>::infinity();
double high = -std::numeric_limits<double>::infinity();
const std::size_t count = values.size();
const std::size_t buckets = (count + kCoarseStride - 1) / kCoarseStride;
coarseMin.assign(buckets, std::numeric_limits<double>::infinity());
coarseMax.assign(buckets, -std::numeric_limits<double>::infinity());
std::size_t bucket = 0;
std::size_t bucketEnd = kCoarseStride;
for (std::size_t index = 0; index < count; ++index)
{
if (index >= bucketEnd)
{
++bucket;
bucketEnd += kCoarseStride;
}
const double value = values[index];
if (std::isfinite(value))
{
++stats.finiteCount;
low = std::min(low, value);
high = std::max(high, value);
if (value < coarseMin[bucket])
{
coarseMin[bucket] = value;
}
if (value > coarseMax[bucket])
{
coarseMax[bucket] = value;
}
}
}
if (stats.finiteCount == 0)
{
stats.minValue = 0.0;
stats.maxValue = 0.0;
}
else
{
stats.minValue = low;
stats.maxValue = high;
}
return stats;
}
bool BuildSignalGroup(LoadedSource&& source, std::size_t paletteBase, SignalGroup& group,
std::wstring& error, const LoadProgress& progress)
{
error.clear();
group = SignalGroup();
if (source.blocks.empty() || !source.HasData())
{
error = L"数据源为空。";
return false;
}
// 分组默认名 = 源文件名(含扩展名),用户可重命名。
group.name = source.sourceName;
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
// 时间轴去重表:内容完全相同(长度与全部数值逐位相同)的时间轴只保留一条。
// 这样 CSV 里多个同刻度的 `time` 列、MF4 里多个同基线的通道组不会重复占内存。
std::vector<std::size_t> axisHashToIndex;
std::vector<std::uint64_t> axisHashValues;
const auto timeHash = [](const std::vector<double>& time) -> std::uint64_t
{
std::uint64_t hash = 1469598103934665603ull;
for (const double value : time)
{
std::uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
hash ^= bits;
hash *= 1099511628211ull;
}
hash ^= static_cast<std::uint64_t>(time.size());
hash *= 1099511628211ull;
return hash;
};
const auto sameTime = [](const std::vector<double>& a, const std::vector<double>& b) -> bool
{
if (a.size() != b.size())
{
return false;
}
for (std::size_t i = 0; i < a.size(); ++i)
{
const double x = a[i];
const double y = b[i];
if (x != y)
{
// NaN 视为“相同”(缺失位置一致),其余必须完全相等。
if (!(std::isnan(x) && std::isnan(y)))
{
return false;
}
}
}
return true;
};
std::size_t blockIndex = 0;
for (TableBlock& block : source.blocks)
{
++blockIndex;
DataTable& table = block.table;
if (!table.HasData())
{
continue;
}
const std::size_t columnCount = table.headers.size();
if (table.columns.size() < columnCount || columnCount < 2)
{
continue; // 只有时间列:没有可绘制信号
}
// 时间列固定为块的第 0 列(加载器已保证)。
std::vector<double>& timeColumn = table.columns[0];
// 统计时间轴的取值范围与单调性。
TimeAxis axis;
axis.name = table.headers[0];
{
bool monotonic = true;
bool hasPrevious = false;
double previousTime = 0.0;
double low = std::numeric_limits<double>::infinity();
double high = -std::numeric_limits<double>::infinity();
std::size_t finiteCount = 0;
for (const double value : timeColumn)
{
if (!std::isfinite(value))
{
continue;
}
++finiteCount;
low = std::min(low, value);
high = std::max(high, value);
if (hasPrevious && value < previousTime)
{
monotonic = false;
}
previousTime = value;
hasPrevious = true;
}
if (finiteCount < 1)
{
continue; // 该块的时间轴完全无效
}
axis.monotonic = monotonic;
axis.finiteCount = finiteCount;
axis.minTime = low;
axis.maxTime = high;
}
axis.time = std::move(timeColumn);
// 去重:命中已有时间轴则复用它,否则新增一条。
std::size_t axisIndex = group.axes.size();
{
const std::uint64_t hash = timeHash(axis.time);
for (std::size_t candidate = 0; candidate < axisHashValues.size(); ++candidate)
{
if (axisHashValues[candidate] == hash &&
sameTime(group.axes[axisHashToIndex[candidate]].time, axis.time))
{
axisIndex = axisHashToIndex[candidate];
break;
}
}
if (axisIndex == group.axes.size())
{
group.axes.push_back(std::move(axis));
axisHashValues.push_back(hash);
axisHashToIndex.push_back(axisIndex);
}
}
// 构建信号列:每列整块移动,随后就地统计取值范围。
group.signals.reserve(group.signals.size() + columnCount - 1);
for (std::size_t column = 1; column < columnCount; ++column)
{
SignalData signal;
signal.name = Trim(table.headers[column]);
if (signal.name.empty())
{
signal.name = L"Column" + std::to_wstring(column + 1);
}
signal.axisIndex = axisIndex;
// 值 → 文本枚举表(MDF4 `cc_type = 7` 等):与列同下标,缺省为空。
if (column < table.columnEnumText.size())
{
signal.enumText = std::move(table.columnEnumText[column]);
}
signal.values = std::move(table.columns[column]);
const SignalStats stats = ComputeStats(signal.values, signal.coarseMin, signal.coarseMax);
signal.coarseStride = kCoarseStride;
signal.minValue = stats.minValue;
signal.maxValue = stats.maxValue;
signal.finiteCount = stats.finiteCount;
// 有效点很少时缓存全部有效点(光标吸附 / 单点信号初始视图用)。
CacheSparseSamples(group.AxisAt(signal.axisIndex), signal);
// 显示外观随信号一起初始化:调色板按「全局信号序号」取色,
// 使同一信号在任意标签页中都保持同一默认颜色。
{
const std::size_t globalIndex = paletteBase + group.signals.size();
if (palette != nullptr && paletteCount > 0)
{
const std::size_t count = static_cast<std::size_t>(paletteCount);
const std::size_t slot = globalIndex % count;
signal.style.color = palette[slot];
// 自定义色初值取调色板中「向后递进」的下一个颜色。
// 若初值等于本信号自身的预设色,样式区里点击该预设色块会被判定成
// “当前用的是自定义色”,高亮会直接跳到自定义色块上(此前的缺陷)。
signal.style.customColor = palette[(slot + 1) % count];
}
else
{
signal.style.color = 0xFF268BD2u;
signal.style.customColor = 0xFF859900u;
}
signal.style.opacity = 1.0f;
signal.style.lineWidth = 1.0f;
signal.style.mode = DrawMode::LINE;
// 只有一个有效采样点的信号改用「点线」+ 最大线宽,否则根本看不见。
ApplySinglePointDefaultStyle(stats.finiteCount, signal.style);
}
group.signals.push_back(std::move(signal));
}
if (progress)
{
LoadProgressInfo info;
const double ratio = static_cast<double>(blockIndex) /
static_cast<double>(std::max<std::size_t>(1, source.blocks.size()));
info.ratio = 0.75 + 0.25 * ratio;
info.hasBytes = false;
if (!progress(info))
{
error = L"已取消加载。";
return false;
}
}
}
if (group.signals.empty() || group.axes.empty())
{
error = L"除时间轴外未发现任何数据列。";
return false;
}
return true;
}
// ============================ DataSet ============================
bool DataSet::AddGroup(SignalGroup&& group, int& groupIndex)
{
groupIndex = -1;
if (group.signals.empty() || group.axes.empty())
{
return false;
}
m_groups.push_back(std::move(group));
groupIndex = static_cast<int>(m_groups.size()) - 1;
return true;
}
bool DataSet::InsertDerivedSignal(int groupIndex, int afterSignalIndex, SignalData&& signal,
int& newSignalIndex)
{
newSignalIndex = -1;
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
{
return false;
}
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
if (afterSignalIndex < -1 || afterSignalIndex >= static_cast<int>(group.signals.size()))
{
return false;
}
// 1. 采样值统计与粗粒度包络层:与正常加载路径使用同一套统计逻辑,
// 否则派生信号在大文件下会退化成逐点扫描(包络构建变慢)。
const SignalStats stats = ComputeStats(signal.values, signal.coarseMin, signal.coarseMax);
signal.coarseStride = kCoarseStride;
signal.minValue = stats.minValue;
signal.maxValue = stats.maxValue;
signal.finiteCount = stats.finiteCount;
// 有效点很少时缓存全部有效点;派生信号沿用源信号的时间轴。
CacheSparseSamples(group.AxisAt(signal.axisIndex), signal);
// 2. 显示外观按「全局信号序号」取调色板颜色(与普通新信号完全一致的规则)。
const int insertPos = afterSignalIndex + 1;
{
// 新信号的全局序号 = 本组之前的信号总数 + 它在组内的下标。
std::size_t globalIndex = static_cast<std::size_t>(insertPos);
for (int index = 0; index < groupIndex; ++index)
{
globalIndex += m_groups[static_cast<std::size_t>(index)].signals.size();
}
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
if (palette != nullptr && paletteCount > 0)
{
const std::size_t count = static_cast<std::size_t>(paletteCount);
const std::size_t slot = globalIndex % count;
signal.style.color = palette[slot];
signal.style.customColor = palette[(slot + 1) % count];
}
// 采样点极少的派生信号同样改用「点线」并加粗(与普通加载路径同一规则)。
ApplySinglePointDefaultStyle(stats.finiteCount, signal.style);
}
group.signals.insert(group.signals.begin() + insertPos, std::move(signal));
// 3. 全局选中引用:同一组内位于插入点之后的信号下标整体后移。
if (m_selected.groupIndex == groupIndex && m_selected.signalIndex >= insertPos)
{
++m_selected.signalIndex;
}
newSignalIndex = insertPos;
return true;
}
bool DataSet::RemoveGroup(int groupIndex)
{
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
{
return false;
}
// 用移动到末位后弹出的方式移除,使被移除文件的时间轴与全部信号缓冲区在本调用内
// 立即析构归还操作系统,不再等待后续插入覆盖;最后收缩容量,避免残留空闲块。
const std::size_t position = static_cast<std::size_t>(groupIndex);
for (std::size_t index = position; index + 1 < m_groups.size(); ++index)
{
m_groups[index] = std::move(m_groups[index + 1]);
}
m_groups.pop_back();
m_groups.shrink_to_fit();
return true;
}
bool DataSet::RemoveSignal(int groupIndex, int signalIndex)
{
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
{
return false;
}
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
if (signalIndex < 0 || signalIndex >= static_cast<int>(group.signals.size()))
{
return false;
}
// 与 RemoveGroup 同一思路:先把待移除项移到末位(move 赋值时其原有缓冲当场析构),
// 再 pop_back + shrink_to_fit,使该信号的全部采样内存在本调用内立即归还给系统,
// 而不是留在容量里等后续插入覆盖。
std::vector<SignalData>& signals = group.signals;
const std::size_t position = static_cast<std::size_t>(signalIndex);
for (std::size_t index = position; index + 1 < signals.size(); ++index)
{
signals[index] = std::move(signals[index + 1]);
}
signals.pop_back();
signals.shrink_to_fit();
return true;
}
void DataSet::RemapSelectionAfterSignalRemoval(int groupIndex, int signalIndex)
{
if (groupIndex < 0 || signalIndex < 0 || m_selected.groupIndex != groupIndex)
{
return;
}
if (m_selected.signalIndex == signalIndex)
{
// 选中的信号本身被移除:取消选中。
m_selected = SignalRef();
}
else if (m_selected.signalIndex > signalIndex)
{
// 其后信号整体前移一位。
--m_selected.signalIndex;
}
}
std::size_t DataSet::SignalCount() const
{
std::size_t count = 0;
for (const SignalGroup& group : m_groups)
{
count += group.signals.size();
}
return count;
}
const SignalGroup* DataSet::GroupAt(int groupIndex) const
{
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
{
return nullptr;
}
return &m_groups[static_cast<std::size_t>(groupIndex)];
}
bool DataSet::RenameSignal(int groupIndex, int signalIndex, const std::wstring& name)
{
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
{
return false;
}
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
if (signalIndex < 0 || signalIndex >= static_cast<int>(group.signals.size()))
{
return false;
}
// 统一去除首尾空白;结果为空则忽略(名称是信号的唯一标识性字段,不允许抹空)。
std::wstring trimmed = Trim(name);
if (trimmed.empty())
{
return false;
}
SignalData& signal = group.signals[static_cast<std::size_t>(signalIndex)];
if (trimmed == signal.name)
{
return false;
}
signal.name = std::move(trimmed);
return true;
}
bool DataSet::RenameGroup(int groupIndex, const std::wstring& name)
{
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
{
return false;
}
std::wstring trimmed = Trim(name);
if (trimmed.empty())
{
return false;
}
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
if (trimmed == group.name)
{
return false;
}
group.name = std::move(trimmed);
return true;
}
const SignalData* DataSet::Resolve(const SignalRef& ref) const
{
const SignalGroup* group = GroupAt(ref.groupIndex);
if (group == nullptr)
{
return nullptr;
}
if (ref.signalIndex < 0 || ref.signalIndex >= static_cast<int>(group->signals.size()))
{
return nullptr;
}
return &group->signals[static_cast<std::size_t>(ref.signalIndex)];
}
const TimeAxis* DataSet::Axis(const SignalRef& ref) const
{
const SignalGroup* group = GroupAt(ref.groupIndex);
if (group == nullptr)
{
return nullptr;
}
if (ref.signalIndex < 0 || ref.signalIndex >= static_cast<int>(group->signals.size()) ||
group->axes.empty())
{
return nullptr;
}
return &group->AxisOf(group->signals[static_cast<std::size_t>(ref.signalIndex)]);
}
const SignalStyle* DataSet::Style(const SignalRef& ref) const
{
const SignalData* signal = Resolve(ref);
return (signal != nullptr) ? &signal->style : nullptr;
}
SignalStyle* DataSet::Style(const SignalRef& ref)
{
const SignalGroup* group = GroupAt(ref.groupIndex);
if (group == nullptr)
{
return nullptr;
}
if (ref.signalIndex < 0 || ref.signalIndex >= static_cast<int>(group->signals.size()))
{
return nullptr;
}
// 这里需要写入,因此借助非 const 路径重新定位信号。
SignalGroup& mutableGroup = m_groups[static_cast<std::size_t>(ref.groupIndex)];
return &mutableGroup.signals[static_cast<std::size_t>(ref.signalIndex)].style;
}
double DataSet::MinTime() const
{
double value = std::numeric_limits<double>::infinity();
for (const SignalGroup& group : m_groups)
{
double low = 0.0;
double high = 0.0;
if (group.TimeRange(low, high))
{
value = std::min(value, low);
}
}
return std::isfinite(value) ? value : 0.0;
}
double DataSet::MaxTime() const
{
double value = -std::numeric_limits<double>::infinity();
for (const SignalGroup& group : m_groups)
{
double low = 0.0;
double high = 0.0;
if (group.TimeRange(low, high))
{
value = std::max(value, high);
}
}
return std::isfinite(value) ? value : 1.0;
}
void DataSet::RemapSelectionAfterGroupRemoval(int groupIndex)
{
if (groupIndex < 0)
{
return;
}
if (m_selected.groupIndex == groupIndex)
{
m_selected = SignalRef();
}
else if (m_selected.groupIndex > groupIndex)
{
--m_selected.groupIndex;
}
}
bool DataSet::ValidateSelection()
{
if (!m_selected.IsValid())
{
return false;
}
if (Resolve(m_selected) != nullptr)
{
return false;
}
m_selected = SignalRef();
return true;
}
// ============================ 采样辅助 ============================
namespace
{
/**
* @brief 滑动平均的共用实现(输入只要可转成 double)。
*
* 窗口 = [max(0,i-N), min(count-1,i+N)],每前进一步只增减边界处的两个采样点,
* 整体 O(n),不额外占用前缀和内存。
*/
template <typename T>
void MovingAverageImpl(const std::vector<T>& values, int radius, std::vector<float>& out);
} // namespace
void ComputeMovingAverage(const std::vector<double>& values, int radius, std::vector<float>& out)
{
MovingAverageImpl(values, radius, out);
}
namespace
{
template <typename T>
void MovingAverageImpl(const std::vector<T>& values, int radius, std::vector<float>& out)
{
const int window = std::max(0, radius);
const std::size_t count = values.size();
out.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0 || window == 0)
{
// 半径 0 表示不平滑:原样复制(中心点无效处仍为 NaN)。
for (std::size_t index = 0; index < count; ++index)
{
const double value = static_cast<double>(values[index]);
if (std::isfinite(value))
{
out[index] = static_cast<float>(value);
}
}
return;
}
// 滑动窗口求和:每前进一步只增减边界处的两个采样点,整体 O(n)。
double sum = 0.0;
std::size_t finiteCount = 0;
std::size_t windowLow = 0;
std::size_t windowHigh = 0; // 闭区间右端。
const auto addValue = [&sum, &finiteCount](double value)
{
if (std::isfinite(value))
{
sum += value;
++finiteCount;
}
};
const auto removeValue = [&sum, &finiteCount](double value)
{
if (std::isfinite(value))
{
sum -= value;
--finiteCount;
}
};
for (std::size_t index = 0; index < count; ++index)
{
const std::size_t low = (index > static_cast<std::size_t>(window))
? (index - static_cast<std::size_t>(window))
: 0;
const std::size_t high =
std::min(count - 1, index + static_cast<std::size_t>(window));
if (index == 0)
{
windowLow = low;
windowHigh = high;
for (std::size_t i = windowLow; i <= windowHigh; ++i)
{
addValue(static_cast<double>(values[i]));
}
}
else
{
while (windowLow < low)
{
removeValue(static_cast<double>(values[windowLow]));
++windowLow;
}
while (windowHigh < high)
{
++windowHigh;
addValue(static_cast<double>(values[windowHigh]));
}
}
// 中心点本身无效(NaN)时该处不产生有效结果。
const double center = static_cast<double>(values[index]);
if (finiteCount > 0 && std::isfinite(center))
{
out[index] = static_cast<float>(sum / static_cast<double>(finiteCount));
}
}
}
} // namespace
namespace
{
/**
* @brief `thread_local` 复用缓冲的**保留容量上限**(元素个数,约 4M)。
*
* 这些缓冲用 `thread_local` 复用,是为了省掉"每次调用都分配 + 零初始化上千万元素"
* 的开销(多阶弱峰调整实测 991ms → 577ms)。但**保留容量必须封顶**:
* 后台派生曲线线程池有 4 条线程,每条各自持有一份 `thread_local` 缓冲;
* 一条"整段信号"(VERY_BIG_DATA ≈ 2580 万采样)的弱峰调整需要
* `current` / `next`(float,各 ≈103MB)、极值表与位移表(≈数百 MB),
* 于是**每条线程常驻数百 MB、4 条线程就是 2GB 以上**,而 `thread_local`
* 只在**线程退出**时才析构 → 用户报告"启用预览后内存 1.42GB→4.21GB,
* 关掉预览也不下降,直到关闭整个分组才释放"。
*
* 因此只对"中/小规模"(≤ 上限)保留容量;**超大计算**改为一次性分配、用完即释放 ——
* 这类整段计算本身是秒级的一次性操作(结果会被曲线缓存复用),多一次分配完全可接受,
* 换来的是内存不再随线程数放大。
*/
const std::size_t kReuseBufferMaxElements = 4u * 1024u * 1024u;
/** @brief 容量超过上限就释放缓冲内存(`std::vector` 的容量不会随 `clear()` 归还)。 */
template <typename T>
void TrimReuseBuffer(std::vector<T>& buffer)
{
if (buffer.capacity() > kReuseBufferMaxElements)
{
std::vector<T> empty;
buffer.swap(empty);
}
}
/**
* @brief 作用域守卫:离开作用域时对 `thread_local` 复用缓冲做一次容量裁剪。
*
* 用它(而不是在每个 `return` 前手写一遍)可以保证**所有**退出路径都会裁剪,
* 不会因为新增提前返回而漏掉、导致内存再次失控。
*/
template <typename T>
class ReuseBufferScope
{
public:
explicit ReuseBufferScope(std::vector<T>& buffer) : m_buffer(buffer) {}
~ReuseBufferScope() { TrimReuseBuffer(m_buffer); }
ReuseBufferScope(const ReuseBufferScope&) = delete;
ReuseBufferScope& operator=(const ReuseBufferScope&) = delete;
private:
std::vector<T>& m_buffer;
};
} // namespace
void ComputeMovingAverageIterated(const std::vector<double>& values, int radius, int iterations,
std::vector<float>& out,
const std::atomic<bool>* cancel, bool throttle)
{
const int stages = std::max(1, iterations);
// 第 1 阶:从原始采样值出发。
ComputeMovingAverage(values, radius, out);
if (stages == 1)
{
return;
}
// 后续各阶以上一阶的结果为输入(double 中转,避免 float→double 的精度损失累积)。
// 中转缓冲复用容量(线程本地),避免每阶重新分配上千万元素的向量。
// ⚠️ 用 `ReuseBufferScope` 保证离开作用域时对超大容量做裁剪(见其说明)。
thread_local std::vector<double> bufferBuffer;
ReuseBufferScope<double> reuseScope(bufferBuffer);
std::vector<double>& buffer = bufferBuffer;
buffer.assign(values.size(), std::numeric_limits<double>::quiet_NaN());
std::vector<float> stage;
for (int iteration = 1; iteration < stages; ++iteration)
{
if (cancel != nullptr && cancel->load(std::memory_order_relaxed))
{
return; // 过时任务:立即退出(调用方会丢弃结果)
}
for (std::size_t index = 0; index < out.size(); ++index)
{
buffer[index] = std::isfinite(out[index]) ? static_cast<double>(out[index])
: std::numeric_limits<double>::quiet_NaN();
}
ComputeMovingAverage(buffer, radius, stage);
out.swap(stage);
if (throttle)
{
// 低内存设备的“少量多次”:每阶之间小睡一下,把内存带宽与 CPU 让给前台。
Sleep(1);
}
}
}
namespace
{
/**
* @brief 「弱峰调整」内部使用的极值点(波峰 / 波谷)。
*
* ⚠️ **字段顺序是刻意的**:`double value` 放在最前、`index` 用 `uint32`、`type` 用 `int8`,
* 使结构体只占 **16 字节**(原来的 `size_t+double+int` 是 24 字节,含 7 字节填充)。
* 一个千万级方波信号的极值数量与采样数同量级(编码器信号几乎每个跳变都是极值),
* 这张表就是「弱峰调整」工作缓冲的主导项(24 B/极值 ≈ 24 B/采样)——
* 瘦身后同样规模的整段计算可省下约 1/3 的峰值内存。
* `uint32` 下标对本项目安全:4.29e9 个 `double` 需要 34 GB 采样内存,不可能达到。
*/
struct PeakExtremum
{
double value = 0.0; /**< 采样值(保持 double:改成 float 会让结果与历史版本不一致)。 */
std::uint32_t index = 0; /**< 采样下标。 */
std::int8_t type = 0; /**< +1 = 波峰,-1 = 波谷。 */
};
/**
* @brief 三组识别规则各自的调整幅度(每一组都在 -2 – +2,即面板上的 -200% – +200%)。
*
* 滑动条的 ±400% 会按"梯度"同时驱动三组规则(见 `ComputeWeakPeakAdjust`):
* 越宽松的规则参与得越晚、在同一滑动条位置上的幅度越小。
*/
struct WeakPeakAmounts
{
double first = 0.0; /**< 第一类(严格:两侧均值都要满足)。 */
double second = 0.0; /**< 第二类(其中一侧满足即可)。 */
double third = 0.0; /**< 第三类(更宽松:只与左右同类极值比较)。 */
};
/**
* @brief 「弱峰调整」的单阶处理:把偏弱的波峰上拉 / 偏弱的波谷下拉,
* 或(负幅度)把弱峰本身削弱、压平。
*
* **不做任何前置滤波**(需要平滑时用户可以先用「均值滤波」构建一条滤波后的信号):
* 极值点直接在输入采样上通过单调段的方向翻转识别。三组识别规则(见 `WeakPeakAmounts`)
* 各自有自己的幅度;某个极值若同时满足多组规则,取**幅度绝对值最大**的那一组
*(= 最严格的那一组),这样"越严格越先被推到目标值"。
*
* @param [IN] input 本阶的输入采样值。
* @param [IN] amounts 三组规则的调整幅度(各 -2 – +2,已夹取;0 = 该组不动)。
* @param [OUT] output 本阶的输出(长度与 input 相同;原值为 NaN 处保持 NaN)。
* @param [OUT] extremes 复用的极值表(容量保留,避免每阶重新分配)。
* @param [IN] cancel 可选的取消标志(为 true 时**立即返回 false**)。
* @return 确实有采样点被改动返回 true;没有任何点需要调整时返回 false。
*/
bool ApplyWeakPeakStage(const std::vector<float>& input, const WeakPeakAmounts& amounts,
std::vector<float>& output, std::vector<PeakExtremum>& extremes,
const std::atomic<bool>* cancel)
{
const std::size_t count = input.size();
output.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0)
{
return false;
}
// 先把有效采样原样拷入输出,之后按“极值点之间的平滑偏移”逐点叠加。
for (std::size_t index = 0; index < count; ++index)
{
const float value = input[index];
if (std::isfinite(value))
{
output[index] = value;
}
}
// 1) 识别波峰 / 波谷:扫描单调段,方向翻转处的上一个采样点即为极值点。
// 波峰(先升后降)与波谷(先降后升)必然交替出现,
// 因此同类极值就是极值序列里下标 ±2 的两个。
extremes.clear();
// 适度预留:编码器方波约每 8 个采样一个极值,预留 1/8 能让最常见的密集信号
// 一次到位(避免 push_back 反复扩容带来的 1.5x 峰值内存);宁少勿多,
// 因为 unreserve 的代价(一次 realloc)远小于无谓地占住内存。
if (extremes.capacity() < count / 8)
{
try
{
extremes.reserve(count / 8);
}
catch (const std::bad_alloc&)
{
// 预留失败不影响正确性:让 push_back 自行增长(低内存时不报错)。
}
}
{
int direction = 0; // 当前单调段方向(+1 上升、-1 下降)。
bool hasPrevious = false;
std::size_t previousIndex = 0;
double previousValue = 0.0;
for (std::size_t index = 0; index < count; ++index)
{
// 取消检查的粒度:每 1M 个采样一次(单次检查开销可忽略,
// 但能让“过时任务”在几十毫秒内退出)。
if (cancel != nullptr && (index & 0xFFFFFu) == 0 && cancel->load(std::memory_order_relaxed))
{
return false;
}
const double value = static_cast<double>(input[index]);
if (!std::isfinite(value))
{
// 无效采样作为断点:不跨越它判定极值。
hasPrevious = false;
continue;
}
if (!hasPrevious)
{
hasPrevious = true;
previousIndex = index;
previousValue = value;
continue;
}
const int step = (value > previousValue) ? 1 : ((value < previousValue) ? -1 : 0);
if (step != 0)
{
if (direction == 0)
{
direction = step;
}
else if (step != direction)
{
// 方向在此翻转:上一个采样点就是极值(原方向向上则为波峰)。
PeakExtremum extreme;
extreme.index = static_cast<std::uint32_t>(previousIndex);
extreme.value = previousValue;
extreme.type = static_cast<std::int8_t>(direction);
extremes.push_back(extreme);
direction = step;
}
}
previousIndex = index;
previousValue = value;
}
}
// 识别不到足够多的极值(常量 / 单调 / 采样点太少):本阶无需任何改动。
if (extremes.size() < 5)
{
return false;
}
// 2) 逐个极值点判定它属于哪几类规则点,并算出它自己的目标偏移量。
const std::size_t extremeCount = extremes.size();
thread_local std::vector<double> shiftBuffer;
ReuseBufferScope<double> reuseScope(shiftBuffer);
std::vector<double>& shift = shiftBuffer;
shift.assign(extremeCount, 0.0);
for (std::size_t k = 2; k + 2 < extremeCount; ++k)
{
const PeakExtremum& current = extremes[k];
const PeakExtremum& left = extremes[k - 2];
const PeakExtremum& right = extremes[k + 2];
if (left.type != current.type || right.type != current.type)
{
continue;
}
if (!std::isfinite(current.value) || !std::isfinite(left.value) ||
!std::isfinite(right.value))
{
continue;
}
// 相邻的**异类**极值(波峰旁边的波谷 / 波谷旁边的波峰)。
const PeakExtremum& nearLeft = extremes[k - 1];
const PeakExtremum& nearRight = extremes[k + 1];
if (nearLeft.type == current.type || nearRight.type == current.type)
{
continue; // 极值序列本应交替;异常时不动这一点
}
if (!std::isfinite(nearLeft.value) || !std::isfinite(nearRight.value))
{
continue;
}
const bool isPeak = (current.type > 0);
// 「左侧/右侧相邻的一个同类极值 + 一个异类极值」的均值。
const double leftMean = (left.value + nearLeft.value) * 0.5;
const double rightMean = (nearRight.value + right.value) * 0.5;
// 同类极值里"较温和"的一侧:波峰取较低者、波谷取较高者。
const double milderSame = isPeak ? std::min(left.value, right.value)
: std::max(left.value, right.value);
const double meanSame = (left.value + right.value) * 0.5;
// 异类极值里"较近"的一个:波峰取较高的波谷、波谷取较低的波峰。
const double nearerOpposite = isPeak ? std::max(nearLeft.value, nearRight.value)
: std::min(nearLeft.value, nearRight.value);
const double meanOpposite = (nearLeft.value + nearRight.value) * 0.5;
// 三组识别规则(以弱峰为例,弱谷把"低于"换成"高于"、max/min 互换):
// 第一类:**同时**低于左右两侧的(相邻同类极值 + 相邻异类极值)均值;
// 第二类:低于其中**任意一侧**的该均值(从"且"放宽为"或");
// 第三类:只与左右**同类**极值比较 —— 低于左右两个同类极值即算弱峰。
const bool firstClass = isPeak ? ((current.value < leftMean) && (current.value < rightMean))
: ((current.value > leftMean) && (current.value > rightMean));
const bool secondClass = isPeak ? ((current.value < leftMean) || (current.value < rightMean))
: ((current.value > leftMean) || (current.value > rightMean));
const bool thirdClass = isPeak ? ((current.value < left.value) && (current.value < right.value))
: ((current.value > left.value) && (current.value > right.value));
// 同时满足多组时取**幅度绝对值最大**的那一组(= 最严格的那一组先被推到目标值)。
double amount = 0.0;
if (firstClass)
{
amount = amounts.first;
}
if (secondClass && std::fabs(amounts.second) > std::fabs(amount))
{
amount = amounts.second;
}
if (thirdClass && std::fabs(amounts.third) > std::fabs(amount))
{
amount = amounts.third;
}
if (amount == 0.0)
{
continue; // 不需要调整
}
double destination = current.value;
if (amount > 0.0)
{
// ---- 正幅度(增强)----
// 0 – 100%:当前值 → 左右**同类**极值中较温和的一侧(波峰取较低者、波谷取较高者);
// 100 – 200%:该侧 → 左右**同类**极值的**均值**。
// ⚠️ 波峰从 `milderSame` 走向 `meanSame`(向上)、波谷同样从 `milderSame`
// 走向 `meanSame`(此时 `meanSame` 可能比 `milderSame` **低**),
// 即两者的插值方向一致(都是"milderSame → meanSame"),无需分别取符号。
destination = (amount <= 1.0)
? (current.value + amount * (milderSame - current.value))
: (milderSame + (amount - 1.0) * (meanSame - milderSame));
}
else
{
// ---- 负幅度(削弱)----
// 0 – -100%:当前值 → 相邻**异类**极值中较近的一个
// (波峰取较高的波谷、波谷取较低的波峰);
// -100 – -200%:该侧 → 相邻异类极值的**均值**。
const double magnitude = std::min(2.0, -amount);
destination = (magnitude <= 1.0)
? (current.value + magnitude * (nearerOpposite - current.value))
: (nearerOpposite +
(magnitude - 1.0) * (meanOpposite - nearerOpposite));
// ⚠️ 削弱**不得反向增强**:目标被夹在"不越过当前值"的一侧。
if (isPeak)
{
destination = std::min(destination, current.value);
}
else
{
destination = std::max(destination, current.value);
}
}
shift[k] = destination - current.value;
}
// 3) 把偏移量平滑地摊到极值点之间的数据点上:
// 相邻两个极值点之间用平滑步进过渡(两端导数为 0,波形不会出现折角),
// 数据首尾则从 0 渐变到最近的极值点偏移。
// 注意:极值序列的首尾各两个元素的偏移量恒为 0(缺少同类的左右邻居),
// 因此这里只需要填充 shift 非零的区间。
bool changed = false;
const auto applySpan = [&input, &output, count](std::size_t begin, std::size_t end,
double beginShift, double endShift)
{
if (end <= begin)
{
if (begin < count && std::isfinite(input[begin]))
{
output[begin] = static_cast<float>(input[begin] + beginShift);
}
return;
}
const double span = static_cast<double>(end - begin);
for (std::size_t index = begin; index <= end; ++index)
{
const float value = input[index];
if (!std::isfinite(value))
{
continue;
}
const double t = static_cast<double>(index - begin) / span;
const double weight = t * t * (3.0 - 2.0 * t);
output[index] = static_cast<float>(value + beginShift * (1.0 - weight) +
endShift * weight);
}
};
for (std::size_t k = 0; k + 1 < extremeCount; ++k)
{
if (shift[k] == 0.0 && shift[k + 1] == 0.0)
{
continue;
}
applySpan(extremes[k].index, extremes[k + 1].index, shift[k], shift[k + 1]);
changed = true;
}
return changed;
}
} // namespace
void ComputeWeakPeakAdjust(const std::vector<double>& values, double amount, int iterations,
std::vector<float>& out,
const std::atomic<bool>* cancel, bool throttle)
{
const std::size_t count = values.size();
out.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0)
{
return;
}
// 第 1 阶:从原始采样值出发(非有限值保持 NaN)。
// 工作缓冲用线程本地变量复用容量:单条曲线动辄上千万采样,若每次调用都重新分配
// 并零初始化,单条「弱峰调整」的耗时会被明显放大(内存分配 + page fault)。
// ⚠️ 用 `ReuseBufferScope` 保证**离开作用域时裁剪超大容量**:否则 4 条后台线程
// 会各自常驻数百 MB(thread_local 只在线程退出时才析构),内存随线程数放大。
thread_local std::vector<float> currentBuffer;
thread_local std::vector<float> nextBuffer;
thread_local std::vector<PeakExtremum> extremesBuffer;
ReuseBufferScope<float> currentScope(currentBuffer);
ReuseBufferScope<float> nextScope(nextBuffer);
ReuseBufferScope<PeakExtremum> extremesScope(extremesBuffer);
std::vector<float>& current = currentBuffer;
current.assign(count, std::numeric_limits<float>::quiet_NaN());
for (std::size_t index = 0; index < count; ++index)
{
const double value = values[index];
if (std::isfinite(value))
{
current[index] = static_cast<float>(value);
}
}
// 「迭代次数 N」与「均值滤波」的迭代次数语义一致:把同一处理**重复应用 N 次**,
// 每一阶以上一阶的结果为输入。一阶时改定量已严格等于「幅度」对应的目标值;
// 多次迭代时相邻的弱峰会互相抬高彼此的基准,从而把较宽的弱峰区逐步恢复(或削弱)。
const int stages = std::max(1, std::min(iterations, 1000));
// 滑动条量程 ±400%(`amount` = 百分比 / 100,即 -4 – +4),按"梯度"同时驱动三组规则:
// 第一类:始终参与,幅度 = clamp(|a|, 0, 2);
// 第二类:|a| > 100% 起参与,幅度 = clamp(|a| - 1, 0, 2);
// 第三类:|a| > 200% 起参与,幅度 = clamp(|a| - 2, 0, 2)。
// 于是滑动条 0–100% 只有第一类在动;100–200% 第一类走完 100–200%、第二类开始走 0–100%;
// 200–300% 第一类已到上限、第二类走 100–200%、第三类开始走 0–100%;
// 300–400% 前两类都已到上限,只有第三类在走 100–200%。负值完全对称。
const double clamped = std::max(-4.0, std::min(4.0, amount));
const double magnitude = std::fabs(clamped);
const double sign = (clamped < 0.0) ? -1.0 : 1.0;
WeakPeakAmounts amounts;
amounts.first = sign * std::min(magnitude, 2.0);
amounts.second = sign * std::max(0.0, std::min(2.0, magnitude - 1.0));
amounts.third = sign * std::max(0.0, std::min(2.0, magnitude - 2.0));
std::vector<float>& next = nextBuffer;
for (int stage = 0; stage < stages; ++stage)
{
if (cancel != nullptr && cancel->load(std::memory_order_relaxed))
{
return; // 过时任务:立即退出(`out` 保留为 NaN,调用方会丢弃它)
}
if (!ApplyWeakPeakStage(current, amounts, next, extremesBuffer, cancel))
{
if (cancel != nullptr && cancel->load(std::memory_order_relaxed))
{
return; // 被取消(而非“本阶无改动”)
}
// 这一阶没有任何采样点需要调整:后续迭代次数也不会再有变化,提前结束。
break;
}
current.swap(next);
if (throttle)
{
// 低内存设备的“少量多次”:每阶之间小睡一下,把内存带宽与 CPU 让给前台。
Sleep(1);
}
}
out.swap(current);
}
const Lane::MeanFilterItem* Lane::FindMeanFilterItem(const SignalRef& ref) const
{
for (const MeanFilterItem& item : meanFilterItems)
{
if (item.ref == ref)
{
return &item;
}
}
return nullptr;
}
Lane::MeanFilterItem& Lane::EnsureMeanFilterItem(const SignalRef& ref)
{
for (MeanFilterItem& item : meanFilterItems)
{
if (item.ref == ref)
{
return item;
}
}
MeanFilterItem item;
item.ref = ref;
meanFilterItems.push_back(item);
return meanFilterItems.back();
}
void Lane::RemoveMeanFilterItem(const SignalRef& ref)
{
meanFilterItems.erase(
std::remove_if(meanFilterItems.begin(), meanFilterItems.end(),
[&ref](const MeanFilterItem& item) { return item.ref == ref; }),
meanFilterItems.end());
}
const Lane::EdgeCenterItem* Lane::FindEdgeCenterItem(const SignalRef& ref) const
{
for (const EdgeCenterItem& item : edgeCenterItems)
{
if (item.ref == ref)
{
return &item;
}
}
return nullptr;
}
Lane::EdgeCenterItem& Lane::EnsureEdgeCenterItem(const SignalRef& ref)
{
for (EdgeCenterItem& item : edgeCenterItems)
{
if (item.ref == ref)
{
return item;
}
}
EdgeCenterItem item;
item.ref = ref;
edgeCenterItems.push_back(item);
return edgeCenterItems.back();
}
void Lane::RemoveEdgeCenterItem(const SignalRef& ref)
{
edgeCenterItems.erase(
std::remove_if(edgeCenterItems.begin(), edgeCenterItems.end(),
[&ref](const EdgeCenterItem& item) { return item.ref == ref; }),
edgeCenterItems.end());
}
const Lane::WeakPeakItem* Lane::FindWeakPeakItem(const SignalRef& ref) const
{
for (const WeakPeakItem& item : weakPeakItems)
{
if (item.ref == ref)
{
return &item;
}
}
return nullptr;
}
Lane::WeakPeakItem& Lane::EnsureWeakPeakItem(const SignalRef& ref)
{
for (WeakPeakItem& item : weakPeakItems)
{
if (item.ref == ref)
{
return item;
}
}
WeakPeakItem item;
item.ref = ref;
weakPeakItems.push_back(item);
return weakPeakItems.back();
}
void Lane::RemoveWeakPeakItem(const SignalRef& ref)
{
weakPeakItems.erase(
std::remove_if(weakPeakItems.begin(), weakPeakItems.end(),
[&ref](const WeakPeakItem& item) { return item.ref == ref; }),
weakPeakItems.end());
}
int FindNearestSample(const TimeAxis& axis, double time)
{
const std::size_t count = axis.time.size();
if (count == 0)
{
return -1;
}
if (axis.monotonic)
{
const std::vector<double>::const_iterator lower =
std::lower_bound(axis.time.begin(), axis.time.end(), time);
if (lower == axis.time.begin())
{
return 0;
}
if (lower == axis.time.end())
{
return static_cast<int>(count - 1);
}
const std::size_t rightIndex = static_cast<std::size_t>(lower - axis.time.begin());
const std::size_t leftIndex = rightIndex - 1;
const double leftDistance = std::fabs(time - axis.time[leftIndex]);
const double rightDistance = std::fabs(axis.time[rightIndex] - time);
return (leftDistance <= rightDistance) ? static_cast<int>(leftIndex) : static_cast<int>(rightIndex);
}
int best = 0;
double bestDistance = std::numeric_limits<double>::infinity();
for (std::size_t index = 0; index < count; ++index)
{
if (!std::isfinite(axis.time[index]))
{
continue;
}
const double distance = std::fabs(axis.time[index] - time);
if (distance < bestDistance)
{
bestDistance = distance;
best = static_cast<int>(index);
}
}
return best;
}
bool SampleSignalAt(const TimeAxis& axis, const SignalData& signal, double time, bool interpolate,
double& value)
{
value = kMissing;
if (axis.time.empty() || signal.values.empty())
{
return false;
}
if (time < axis.minTime || time > axis.maxTime)
{
return false;
}
const std::vector<double>& values = signal.values;
if (!axis.monotonic)
{
const int nearest = FindNearestSample(axis, time);
if (nearest < 0 || static_cast<std::size_t>(nearest) >= values.size())
{
return false;
}
value = values[static_cast<std::size_t>(nearest)];
return std::isfinite(value);
}
const std::vector<double>::const_iterator lower = std::lower_bound(axis.time.begin(), axis.time.end(), time);
const std::size_t rightIndex = static_cast<std::size_t>(lower - axis.time.begin());
if (rightIndex == 0)
{
value = values[0];
return std::isfinite(value);
}
if (rightIndex >= axis.time.size())
{
value = values.back();
return std::isfinite(value);
}
const std::size_t leftIndex = rightIndex - 1;
if (rightIndex >= values.size() || leftIndex >= values.size())
{
return false;
}
const double t0 = axis.time[leftIndex];
const double t1 = axis.time[rightIndex];
const double v0 = values[leftIndex];
const double v1 = values[rightIndex];
if (!interpolate || !std::isfinite(v0) || !std::isfinite(v1) || !(t1 > t0))
{
const double leftDistance = std::fabs(time - t0);
const double rightDistance = std::fabs(t1 - time);
value = (leftDistance <= rightDistance) ? v0 : v1;
return std::isfinite(value);
}
const double ratio = (time - t0) / (t1 - t0);
value = v0 + (v1 - v0) * ratio;
return true;
}
bool SampleDisplayedSignalAt(const TimeAxis& axis, const SignalData& signal,
const SignalStyle& style, double time, bool interpolate, double& value)
{
// 未启用水平镜像 / 时间偏移时与 SampleSignalAt 完全等价
//(避免两套实现产生细微差异)。
if (!style.mirrorHorizontal && style.offsetTime == 0.0)
{
return SampleSignalAt(axis, signal, time, interpolate, value);
}
value = kMissing;
if (axis.time.empty() || signal.values.empty())
{
return false;
}
// 绘制位置 = 源时刻 + offsetTime,因此查询源数据要用「绘制时刻 − offsetTime」。
const double query = time - style.offsetTime;
if (query < axis.minTime || query > axis.maxTime)
{
return false;
}
const std::vector<double>& values = signal.values;
const std::size_t count = std::min(axis.time.size(), values.size());
if (count == 0)
{
return false;
}
// 画面上第 i 个位置用源下标 srcIndex(i) 的值(水平镜像 = 数据反序)。
const auto sample = [&](std::size_t index) -> double
{
return values[SourceIndexFor(style, count, index)];
};
if (!axis.monotonic)
{
const int nearest = FindNearestSample(axis, query);
if (nearest < 0 || static_cast<std::size_t>(nearest) >= count)
{
return false;
}
value = sample(static_cast<std::size_t>(nearest));
return std::isfinite(value);
}
const std::vector<double>::const_iterator lower =
std::lower_bound(axis.time.begin(), axis.time.end(), query);
const std::size_t rightIndex = static_cast<std::size_t>(lower - axis.time.begin());
if (rightIndex == 0)
{
value = sample(0);
return std::isfinite(value);
}
if (rightIndex >= count)
{
value = sample(count - 1);
return std::isfinite(value);
}
const std::size_t leftIndex = rightIndex - 1;
const double t0 = axis.time[leftIndex];
const double t1 = axis.time[rightIndex];
const double v0 = sample(leftIndex);
const double v1 = sample(rightIndex);
if (!interpolate || !std::isfinite(v0) || !std::isfinite(v1) || !(t1 > t0))
{
const double leftDistance = std::fabs(query - t0);
const double rightDistance = std::fabs(t1 - query);
value = (leftDistance <= rightDistance) ? v0 : v1;
return std::isfinite(value);
}
const double ratio = (query - t0) / (t1 - t0);
value = v0 + (v1 - v0) * ratio;
return true;
}
} // namespace jdi