Files
Just-DataInspector/Utilities/Src/DataModel.cpp
T
DeepSeek V4.1 Flash d4206a8d67 实验性功能新增「差异放大」,并修正「构建为新信号」按钮的间距
1. 新增「实验性功能 → 差异放大」(与其它实验功能一样是带勾选的开关)
   - 用途:纹波类信号某个周期(甚至半个周期)的 M 形下凹明显偏弱时,
     把这段下凹按比例拉回周边振幅,恢复正常周期的对比度,便于看出波形异常。
   - 算法(全 O(n),不额外占用与信号长度成正比以外的内存):
     以半径约半个纹波周期的滑动平均作为纹波中心线,用邻域内「交流分量的最大值」
     作为局部纹波振幅,取全信号振幅的 75 分位作为目标振幅,
     逐点增益 = 目标振幅 / 局部振幅(夹在 1 到放大倍数之间),
     输出 = 中心线 + 增益 × 交流分量,再夹在周边包络内。
   - 关键性质(均已用合成正弦逐点验证):
     * 增益下界恒为 1,因此**只有被判定为"偏弱"的采样点会改变**,
       其余采样点输出与原值逐位相同 —— 实测逐周期表格中只有一个周期发生变化;
     * 放大结果夹在周边包络内,因此**任何一点都不超出原有的振幅范围**,
       整体值域与其它周期的峰峰值都不变;
     * 端点自动收敛到不放大,避免记录首尾被整片抬升。
   - 参数「窗口点数 X」(窗口长度,建议略大于纹波周期的一半)与
     「放大倍数 N」(增益上限)都可在浮动面板内按信号编辑。

2. 浮动面板
   - 与「均值滤波」完全同一套排版:自上而下逐信号条目,
     第一排信号名与复选框、第二排两个参数输入框;取消勾选即不显示该信号的参数与预览曲线;
     启用时默认只勾选网格内第一个可见信号。
   - 新增「构建为新信号」按钮,按各信号自己的参数把结果插入到源信号下方,
     与「均值滤波」的构建机制完全一致(统一走同一套请求与插入流程)。

3. 光标交互
   - 纵向光标额外标注差异放大曲线在该时刻的取值,配色与原始信号一致,
     数值尾部括号内容为「差异化」,双光标时同样带增量。

4. 修复「构建为新信号」按钮与面板下边界的间距
   - 原按钮行直接贴着面板下边框,与面板左右内边距及内容间距都不协调;
     现在按钮上方与下方各保留一个与左右一致的内边距(均值滤波与差异放大同时生效)。

测试:离屏验证套件新增两组检查(合成正弦上算法只改动一个周期、放大不越界、
参数上限生效、面板排版与按钮间距;再写一份合成纹波文件真正加载渲染并核对曲线数值),
合计 289 项检查全部通过;验证过程不显示窗口、不移动鼠标键盘、不抓屏。
2026-10-10 17:54:27 +08:00

2282 lines
68 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.
#include "DataModel.hpp"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cwctype>
#include <deque>
#include <limits>
#include "Theme.hpp"
#include "Utf8Util.hpp"
namespace jdi
{
namespace
{
const double kMissing = std::numeric_limits<double>::quiet_NaN(); /**< 缺失值占位。 */
const double kDefaultLineWidth = 1.6; /**< 默认线宽。 */
/**
* @brief 判断列名是否为时间轴候选列。
* @param [IN] header 原始列名。
* @return 可能为时间轴返回 true。
*/
bool LooksLikeTimeHeader(const std::wstring& header)
{
const std::wstring text = ToLowerAscii(Trim(header));
if (text.empty())
{
return false;
}
if (text == L"t" || text == L"time" || text == L"timestamp" || text == L"simulation time" ||
text == L"时间" || text == L"时间轴")
{
return true;
}
return ContainsInsensitive(text, L"time") || ContainsInsensitive(text, L"时间");
}
/**
* @brief 从名称末尾的括号中提取单位(仅接受形如 mA / V / deg/s 的短标记)。
* @param [IN] text 列名(已去除首尾空白)。
* @return 单位文本,无法识别时返回空字符串。
*/
std::wstring ExtractUnit(const std::wstring& text)
{
if (text.size() < 3 || text.back() != L')')
{
return std::wstring();
}
const std::size_t open = text.rfind(L'(');
if (open == std::wstring::npos || open + 2 >= text.size())
{
return std::wstring();
}
const std::wstring inner = Trim(text.substr(open + 1, text.size() - open - 2));
if (inner.empty() || inner.size() > 10)
{
return std::wstring();
}
for (const wchar_t c : inner)
{
const bool allowed = iswalpha(c) || iswdigit(c) || c == L'%' || c == L'/' || c == L'^' ||
c == L'-' || c == L'.' || c == L'\u00B0' || c == L'\u00B5' ||
c == L'\u03A9' || c == L'\u00B2' || c == L'\u00B3' || c == L'\u00B7';
if (!allowed)
{
return std::wstring();
}
}
return inner;
}
/**
* @brief 为网格范围补充留白,避免波形紧贴边界。
* @param [IN, OUT] yMin 范围下限。
* @param [IN, OUT] yMax 范围上限。
*/
void PadRange(double& yMin, double& yMax)
{
if (!std::isfinite(yMin) || !std::isfinite(yMax))
{
yMin = 0.0;
yMax = 1.0;
return;
}
const double span = yMax - yMin;
if (span <= 0.0)
{
const double magnitude = std::max(1.0, std::fabs(yMin) * 0.1);
yMin -= magnitude;
yMax += magnitude;
return;
}
const double pad = span * 0.06;
yMin -= pad;
yMax += pad;
}
/**
* @brief 依据全部可见信号重新生成网格划分。
* @param [IN] visible 各信号的可见性(按 [文件][信号] 索引)。
* @param [IN] lanes 网格集合(原地更新)。
* @param [IN] mode 网格划分策略。
*/
void RebuildLanes(const std::vector<std::vector<bool>>& visible, std::vector<Lane>& lanes,
LaneMode mode)
{
// 随网格携带的“网格自身状态”(按信号继承):重建网格时不能丢。
struct LaneCarry
{
bool fitYToSelection = false;
bool edgeCount = false;
double edgeValue = 0.0;
bool edgeValueValid = false;
bool edgePanelOpen = true;
double edgePanelX = -1.0;
double edgePanelY = -1.0;
bool edgeCenter = false;
std::vector<Lane::EdgeCenterItem> edgeCenterItems;
bool edgeCenterPanelOpen = true;
double edgeCenterPanelX = -1.0;
double edgeCenterPanelY = -1.0;
bool meanFilter = false;
std::vector<Lane::MeanFilterItem> meanFilterItems;
bool meanFilterPanelOpen = true;
double meanFilterPanelX = -1.0;
double meanFilterPanelY = -1.0;
bool diffAmp = false;
std::vector<Lane::DiffAmpItem> diffAmpItems;
bool diffAmpPanelOpen = true;
double diffAmpPanelX = -1.0;
double diffAmpPanelY = -1.0;
};
// 先记下每个信号原来所在网格的网格自身状态:
// 勾选可见性会重建网格,若不携带,用户刚打开的开关会被悄悄清掉。
std::vector<std::pair<SignalRef, LaneCarry>> previousCarry;
for (const Lane& lane : lanes)
{
LaneCarry carry;
carry.fitYToSelection = lane.fitYToSelection;
carry.edgeCount = lane.edgeCount;
carry.edgeValue = lane.edgeValue;
carry.edgeValueValid = lane.edgeValueValid;
carry.edgePanelOpen = lane.edgePanelOpen;
carry.edgePanelX = lane.edgePanelX;
carry.edgePanelY = lane.edgePanelY;
carry.edgeCenter = lane.edgeCenter;
carry.edgeCenterItems = lane.edgeCenterItems;
carry.edgeCenterPanelOpen = lane.edgeCenterPanelOpen;
carry.edgeCenterPanelX = lane.edgeCenterPanelX;
carry.edgeCenterPanelY = lane.edgeCenterPanelY;
carry.meanFilter = lane.meanFilter;
carry.meanFilterItems = lane.meanFilterItems;
carry.meanFilterPanelOpen = lane.meanFilterPanelOpen;
carry.meanFilterPanelX = lane.meanFilterPanelX;
carry.meanFilterPanelY = lane.meanFilterPanelY;
carry.diffAmp = lane.diffAmp;
carry.diffAmpItems = lane.diffAmpItems;
carry.diffAmpPanelOpen = lane.diffAmpPanelOpen;
carry.diffAmpPanelX = lane.diffAmpPanelX;
carry.diffAmpPanelY = lane.diffAmpPanelY;
for (const SignalRef& ref : lane.signals)
{
previousCarry.emplace_back(ref, carry);
}
}
const auto inheritedCarry = [&previousCarry](const SignalRef& ref)
{
for (const std::pair<SignalRef, LaneCarry>& entry : previousCarry)
{
if (entry.first == ref)
{
return entry.second;
}
}
return LaneCarry();
};
const auto applyCarry = [&inheritedCarry](Lane& lane, const SignalRef& ref)
{
const LaneCarry carry = inheritedCarry(ref);
lane.fitYToSelection = carry.fitYToSelection;
lane.edgeCount = carry.edgeCount;
lane.edgeValue = carry.edgeValue;
lane.edgeValueValid = carry.edgeValueValid;
lane.edgePanelOpen = carry.edgePanelOpen;
lane.edgePanelX = carry.edgePanelX;
lane.edgePanelY = carry.edgePanelY;
lane.edgeCenter = carry.edgeCenter;
lane.edgeCenterItems = carry.edgeCenterItems;
lane.edgeCenterPanelOpen = carry.edgeCenterPanelOpen;
lane.edgeCenterPanelX = carry.edgeCenterPanelX;
lane.edgeCenterPanelY = carry.edgeCenterPanelY;
lane.meanFilter = carry.meanFilter;
lane.meanFilterItems = carry.meanFilterItems;
lane.meanFilterPanelOpen = carry.meanFilterPanelOpen;
lane.meanFilterPanelX = carry.meanFilterPanelX;
lane.meanFilterPanelY = carry.meanFilterPanelY;
lane.diffAmp = carry.diffAmp;
lane.diffAmpItems = carry.diffAmpItems;
lane.diffAmpPanelOpen = carry.diffAmpPanelOpen;
lane.diffAmpPanelX = carry.diffAmpPanelX;
lane.diffAmpPanelY = carry.diffAmpPanelY;
};
// 收集全部可见信号引用。
std::vector<SignalRef> visibleRefs;
for (std::size_t fileIndex = 0; fileIndex < visible.size(); ++fileIndex)
{
for (std::size_t signalIndex = 0; signalIndex < visible[fileIndex].size(); ++signalIndex)
{
if (visible[fileIndex][signalIndex])
{
SignalRef ref;
ref.fileIndex = static_cast<int>(fileIndex);
ref.signalIndex = static_cast<int>(signalIndex);
visibleRefs.push_back(ref);
}
}
}
// 判断某引用对应的信号是否可见(越界视为不可见)。
const auto isVisibleRef = [&visible](const SignalRef& ref)
{
if (ref.fileIndex < 0 || static_cast<std::size_t>(ref.fileIndex) >= visible.size())
{
return false;
}
const std::vector<bool>& fileVisible = visible[static_cast<std::size_t>(ref.fileIndex)];
if (ref.signalIndex < 0 || static_cast<std::size_t>(ref.signalIndex) >= fileVisible.size())
{
return false;
}
return fileVisible[static_cast<std::size_t>(ref.signalIndex)];
};
// 某信号当前所属的网格下标(-1 表示尚未分配网格)。
const auto laneIndexOf = [&lanes](const SignalRef& ref)
{
for (std::size_t index = 0; index < lanes.size(); ++index)
{
const std::vector<SignalRef>& refs = lanes[index].signals;
if (std::find(refs.begin(), refs.end(), ref) != refs.end())
{
return static_cast<int>(index);
}
}
return -1;
};
if (mode == LaneMode::PER_SIGNAL)
{
// 每条可见信号独占一个网格:网格数量严格跟随可见信号数量。
// **顺序按"原有网格位置"优先**:已经分配过网格的信号保持其相对次序,
// 新勾选(此前没有网格)的信号统一追加到最下方。
// 若直接按 visibleRefs(文件/列序)重建,新勾选的信号会按其列序
// 插到中间,位置看起来"不确定"。
std::vector<SignalRef> ordered;
ordered.reserve(visibleRefs.size());
for (const SignalRef& ref : visibleRefs)
{
if (laneIndexOf(ref) >= 0)
{
ordered.push_back(ref);
}
}
std::stable_sort(ordered.begin(), ordered.end(),
[&laneIndexOf](const SignalRef& a, const SignalRef& b)
{ return laneIndexOf(a) < laneIndexOf(b); });
for (const SignalRef& ref : visibleRefs)
{
if (laneIndexOf(ref) < 0)
{
ordered.push_back(ref);
}
}
lanes.clear();
lanes.reserve(ordered.size());
for (const SignalRef& ref : ordered)
{
Lane lane;
lane.autoRange = true;
applyCarry(lane, ref);
lane.signals.push_back(ref);
lanes.push_back(std::move(lane));
}
}
else if (mode == LaneMode::MERGED)
{
// 全部可见信号合并到同一网格;无可见信号时不保留空网格。
bool previousAutoRange = true;
double previousYMin = -1.0;
double previousYMax = 1.0;
if (lanes.size() == 1)
{
previousAutoRange = lanes[0].autoRange;
previousYMin = lanes[0].yMin;
previousYMax = lanes[0].yMax;
}
lanes.clear();
if (!visibleRefs.empty())
{
Lane lane;
lane.autoRange = previousAutoRange;
lane.yMin = previousYMin;
lane.yMax = previousYMax;
applyCarry(lane, visibleRefs.front());
lane.signals = visibleRefs;
lanes.push_back(std::move(lane));
}
}
else
{
// 自定义划分:仅移除不可见信号与由此产生的空网格,保持用户排布。
for (Lane& lane : lanes)
{
std::vector<SignalRef>& refs = lane.signals;
refs.erase(std::remove_if(refs.begin(), refs.end(),
[&isVisibleRef](const SignalRef& ref)
{ return !isVisibleRef(ref); }),
refs.end());
}
lanes.erase(std::remove_if(lanes.begin(), lanes.end(),
[](const Lane& lane) { return lane.signals.empty(); }),
lanes.end());
// 未分配网格的可见信号各自独占一个新网格。
// 注意:不能把它们塞进已有的第一个网格——那会导致“拖拽合并后新勾选的信号
// 被悄悄并入合并网格”,用户看不到新增的独立泳道。
for (const SignalRef& ref : visibleRefs)
{
bool placed = false;
for (const Lane& lane : lanes)
{
if (std::find(lane.signals.begin(), lane.signals.end(), ref) != lane.signals.end())
{
placed = true;
break;
}
}
if (placed)
{
continue;
}
Lane lane;
lane.autoRange = true;
applyCarry(lane, ref);
lane.signals.push_back(ref);
lanes.push_back(std::move(lane));
}
}
// 统一重命名,保证编号连续。
for (std::size_t index = 0; index < lanes.size(); ++index)
{
lanes[index].name = L"网格 " + std::to_wstring(index + 1);
}
}
}
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"线段";
}
}
// ============================ DataFile 构建 ============================
/**
* @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 统计一列采样的最小/最大值与有效点数量,并顺带构建粗粒度包络层。
*
* 两者在同一个循环内完成,避免为粗粒度层多扫一遍数据(大文件下那是一整趟内存遍历)。
*
* @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 BuildDataFile(CsvTable&& table, const std::wstring& filePath, std::size_t paletteBase, DataFile& file,
std::wstring& error, const CsvProgress& progress)
{
error.clear();
file = DataFile();
if (!table.HasData())
{
error = L"数据表为空。";
return false;
}
const std::size_t columnCount = table.headers.size();
if (table.columns.size() < columnCount)
{
error = L"数据表列结构不完整。";
return false;
}
// 1. 识别时间轴列:优先取列名匹配的列,否则回退到第一列。
int timeColumn = -1;
for (std::size_t column = 0; column < columnCount; ++column)
{
if (LooksLikeTimeHeader(table.headers[column]))
{
timeColumn = static_cast<int>(column);
break;
}
}
if (timeColumn < 0)
{
timeColumn = 0;
}
// 时间列整列移动,不做逐元素拷贝。
file.time = std::move(table.columns[static_cast<std::size_t>(timeColumn)]);
std::size_t finiteTimeCount = 0;
bool monotonic = true;
double previousTime = 0.0;
bool hasPrevious = false;
for (const double value : file.time)
{
if (std::isfinite(value))
{
++finiteTimeCount;
if (hasPrevious && value < previousTime)
{
monotonic = false;
}
previousTime = value;
hasPrevious = true;
}
}
if (finiteTimeCount < 2)
{
error = L"时间轴列(" + table.headers[static_cast<std::size_t>(timeColumn)] + L")有效数值不足。";
return false;
}
file.timeColumnName = table.headers[static_cast<std::size_t>(timeColumn)];
file.timeMonotonic = monotonic;
file.filePath = filePath;
const std::size_t slashPos = filePath.find_last_of(L"\\/");
file.fileName = (slashPos == std::wstring::npos) ? filePath : filePath.substr(slashPos + 1);
// 2. 构建信号列:每列整块移动,随后就地统计取值范围。
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
file.signals.reserve(columnCount);
for (std::size_t column = 0; column < columnCount; ++column)
{
if (static_cast<int>(column) == timeColumn)
{
continue;
}
SignalData signal;
signal.rawHeader = table.headers[column];
{
// 名称完整保留文件内的列名(含 "[n]" 序号与括号描述),仅用于展示不做改写。
signal.name = Trim(signal.rawHeader);
signal.unit = ExtractUnit(signal.name);
}
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;
// 显示外观随信号一起初始化:调色板按「全局信号序号」取色,
// 使同一信号在任意标签页中都保持同一默认颜色。
{
const std::size_t globalIndex = paletteBase + file.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;
}
file.signals.push_back(std::move(signal));
if (progress)
{
CsvProgressInfo info;
info.ratio = 0.75 + 0.25 * static_cast<double>(column + 1) / static_cast<double>(columnCount);
if (!progress(info))
{
error = L"已取消加载。";
return false;
}
}
}
if (file.signals.empty())
{
error = L"除时间轴外未发现任何数据列。";
return false;
}
// 3. 统计时间范围(时间列为空或全部无效时回退到行数上界)。
double tMin = std::numeric_limits<double>::infinity();
double tMax = -std::numeric_limits<double>::infinity();
for (const double value : file.time)
{
if (std::isfinite(value))
{
tMin = std::min(tMin, value);
tMax = std::max(tMax, value);
}
}
file.minTime = tMin;
file.maxTime = tMax;
return true;
}
// ============================ DataSet ============================
bool DataSet::AddDataFile(DataFile&& file, int& fileIndex)
{
fileIndex = -1;
if (file.signals.empty())
{
return false;
}
m_files.push_back(std::move(file));
fileIndex = static_cast<int>(m_files.size()) - 1;
return true;
}
bool DataSet::InsertDerivedSignal(int fileIndex, int afterSignalIndex, SignalData&& signal,
int& newSignalIndex)
{
newSignalIndex = -1;
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
{
return false;
}
DataFile& file = m_files[static_cast<std::size_t>(fileIndex)];
if (afterSignalIndex < -1 || afterSignalIndex >= static_cast<int>(file.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;
// 2. 显示外观按「全局信号序号」取调色板颜色(与普通新信号完全一致的规则)。
const int insertPos = afterSignalIndex + 1;
{
// 新信号的全局序号 = 本文件之前的信号总数 + 它在文件内的下标。
std::size_t globalIndex = static_cast<std::size_t>(insertPos);
for (int index = 0; index < fileIndex; ++index)
{
globalIndex += m_files[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];
}
}
file.signals.insert(file.signals.begin() + insertPos, std::move(signal));
// 3. 全局选中引用:同一文件内位于插入点之后的信号下标整体后移。
if (m_selected.fileIndex == fileIndex && m_selected.signalIndex >= insertPos)
{
++m_selected.signalIndex;
}
newSignalIndex = insertPos;
return true;
}
bool DataSet::RemoveFile(int fileIndex)
{
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
{
return false;
}
// 用移动到末位后弹出的方式移除,使被移除文件的时间轴与全部信号缓冲区在本调用内
// 立即析构归还操作系统,不再等待后续插入覆盖;最后收缩容量,避免残留空闲块。
const std::size_t position = static_cast<std::size_t>(fileIndex);
for (std::size_t index = position; index + 1 < m_files.size(); ++index)
{
m_files[index] = std::move(m_files[index + 1]);
}
m_files.pop_back();
m_files.shrink_to_fit();
return true;
}
bool DataSet::RemoveSignal(int fileIndex, int signalIndex)
{
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
{
return false;
}
DataFile& file = m_files[static_cast<std::size_t>(fileIndex)];
if (signalIndex < 0 || signalIndex >= static_cast<int>(file.signals.size()))
{
return false;
}
// 与 RemoveFile 同一思路:先把待移除项移到末位(move 赋值时其原有缓冲当场析构),
// 再 pop_back + shrink_to_fit,使该信号的全部采样内存在本调用内立即归还给系统,
// 而不是留在容量里等后续插入覆盖。
std::vector<SignalData>& signals = file.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 fileIndex, int signalIndex)
{
if (fileIndex < 0 || signalIndex < 0 || m_selected.fileIndex != fileIndex)
{
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 DataFile& file : m_files)
{
count += file.signals.size();
}
return count;
}
const DataFile* DataSet::FileAt(int fileIndex) const
{
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
{
return nullptr;
}
return &m_files[static_cast<std::size_t>(fileIndex)];
}
bool DataSet::SetSignalDisplayName(int fileIndex, int signalIndex, const std::wstring& name)
{
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
{
return false;
}
DataFile& file = m_files[static_cast<std::size_t>(fileIndex)];
if (signalIndex < 0 || signalIndex >= static_cast<int>(file.signals.size()))
{
return false;
}
// 统一去除首尾空白;结果为空则视为恢复原始列名。
std::wstring trimmed = Trim(name);
SignalData& signal = file.signals[static_cast<std::size_t>(signalIndex)];
if (trimmed == signal.displayName)
{
return false;
}
signal.displayName = std::move(trimmed);
return true;
}
bool DataSet::SetFileDisplayName(int fileIndex, const std::wstring& name)
{
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
{
return false;
}
std::wstring trimmed = Trim(name);
DataFile& file = m_files[static_cast<std::size_t>(fileIndex)];
if (trimmed == file.displayName)
{
return false;
}
file.displayName = std::move(trimmed);
return true;
}
const SignalData* DataSet::Resolve(const SignalRef& ref) const
{
const DataFile* file = FileAt(ref.fileIndex);
if (file == nullptr)
{
return nullptr;
}
if (ref.signalIndex < 0 || ref.signalIndex >= static_cast<int>(file->signals.size()))
{
return nullptr;
}
return &file->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 DataFile* file = FileAt(ref.fileIndex);
if (file == nullptr)
{
return nullptr;
}
if (ref.signalIndex < 0 || ref.signalIndex >= static_cast<int>(file->signals.size()))
{
return nullptr;
}
// 这里需要写入,因此借助非 const 路径重新定位信号。
DataFile& mutableFile = m_files[static_cast<std::size_t>(ref.fileIndex)];
return &mutableFile.signals[static_cast<std::size_t>(ref.signalIndex)].style;
}
double DataSet::MinTime() const
{
double value = std::numeric_limits<double>::infinity();
for (const DataFile& file : m_files)
{
if (std::isfinite(file.minTime))
{
value = std::min(value, file.minTime);
}
}
return std::isfinite(value) ? value : 0.0;
}
double DataSet::MaxTime() const
{
double value = -std::numeric_limits<double>::infinity();
for (const DataFile& file : m_files)
{
if (std::isfinite(file.maxTime))
{
value = std::max(value, file.maxTime);
}
}
return std::isfinite(value) ? value : 1.0;
}
void DataSet::RemapSelectionAfterFileRemoval(int fileIndex)
{
if (fileIndex < 0)
{
return;
}
if (m_selected.fileIndex == fileIndex)
{
m_selected = SignalRef();
}
else if (m_selected.fileIndex > fileIndex)
{
--m_selected.fileIndex;
}
}
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),不额外占用前缀和内存。“差异放大”需要在 float 缓冲上再做一次
* 滑动平均,若先转成 double 会因为亿级采样多占一倍内存,因此做成模板。
*/
template <typename T>
void MovingAverageImpl(const std::vector<T>& values, int radius, std::vector<float>& out);
/**
* @brief 滑动窗口最大值(只要最大值)。
*
* 与 `SlidingExtrema` 同一个单调队列思路,但**不额外分配最小值数组**:
* 亿级采样下一次全量计算会多占 100 MB 以上,而“差异放大”只需要最大值。
*
* @param [IN] values 采样值。
* @param [IN] radius 邻域半径(窗口 = [i-radius, i+radius],两端按实际长度)。
* @param [OUT] outMax 邻域最大值(整窗都是 NaN 时为 NaN)。
*/
template <typename T>
void SlidingMax(const std::vector<T>& values, std::size_t radius, std::vector<float>& outMax)
{
const std::size_t count = values.size();
outMax.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0)
{
return;
}
std::deque<std::size_t> maxQueue; // 递减:队首最大。
std::size_t filled = 0;
for (std::size_t index = 0; index < count; ++index)
{
const std::size_t wantHigh = std::min(count - 1, index + radius);
while (filled <= wantHigh)
{
const double incoming = values[filled];
if (std::isfinite(incoming))
{
while (!maxQueue.empty() && values[maxQueue.back()] <= incoming)
{
maxQueue.pop_back();
}
maxQueue.push_back(filled);
}
++filled;
}
const std::size_t low = (index > radius) ? (index - radius) : 0;
while (!maxQueue.empty() && maxQueue.front() < low)
{
maxQueue.pop_front();
}
if (!maxQueue.empty())
{
outMax[index] = static_cast<float>(values[maxQueue.front()]);
}
}
}
} // namespace
void ComputeMovingAverage(const std::vector<double>& values, int radius, std::vector<float>& out)
{
MovingAverageImpl(values, radius, out);
}
void ComputeMovingAverage(const std::vector<float>& 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));
}
}
}
/**
* @brief 滑动窗口极值:一次求出每个中心点邻域(±radius)内的最小值与最大值。
*
* 两个单调队列(最小值队列递增、最大值队列递减)同步滑动,O(n) 时间,
* 内存只有窗口长度。NaN 不参与(整窗都是 NaN 时输出 NaN)。
*
* @param [IN] values 采样值。
* @param [IN] radius 邻域半径(窗口 = [i-radius, i+radius],两端按实际长度)。
* @param [OUT] outMin 邻域最小值。
* @param [OUT] outMax 邻域最大值。
*/
template <typename T>
void SlidingExtrema(const std::vector<T>& values, std::size_t radius,
std::vector<float>& outMin, std::vector<float>& outMax)
{
const std::size_t count = values.size();
outMin.assign(count, std::numeric_limits<float>::quiet_NaN());
outMax.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0)
{
return;
}
std::deque<std::size_t> minQueue; // 递增:队首最小。
std::deque<std::size_t> maxQueue; // 递减:队首最大。
std::size_t filled = 0;
for (std::size_t index = 0; index < count; ++index)
{
const std::size_t wantHigh = std::min(count - 1, index + radius);
while (filled <= wantHigh)
{
const double incoming = values[filled];
if (std::isfinite(incoming))
{
while (!minQueue.empty() && values[minQueue.back()] >= incoming)
{
minQueue.pop_back();
}
minQueue.push_back(filled);
while (!maxQueue.empty() && values[maxQueue.back()] <= incoming)
{
maxQueue.pop_back();
}
maxQueue.push_back(filled);
}
++filled;
}
const std::size_t low = (index > radius) ? (index - radius) : 0;
while (!minQueue.empty() && minQueue.front() < low)
{
minQueue.pop_front();
}
while (!maxQueue.empty() && maxQueue.front() < low)
{
maxQueue.pop_front();
}
if (!minQueue.empty())
{
outMin[index] = static_cast<float>(values[minQueue.front()]);
}
if (!maxQueue.empty())
{
outMax[index] = static_cast<float>(values[maxQueue.front()]);
}
}
}
} // namespace
void ComputeMovingAverageIterated(const std::vector<double>& values, int radius, int order,
std::vector<float>& out)
{
const int stages = std::max(1, order);
// 第 1 阶:从原始采样值出发。
ComputeMovingAverage(values, radius, out);
if (stages == 1)
{
return;
}
// 后续各阶以上一阶的结果为输入(double 中转,避免 float→double 的精度损失累积)。
std::vector<double> buffer(values.size(), std::numeric_limits<double>::quiet_NaN());
std::vector<float> stage;
for (int iteration = 1; iteration < stages; ++iteration)
{
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);
}
}
void ComputeDiffAmplify(const std::vector<double>& values, int window, int gain,
std::vector<float>& out)
{
const std::size_t count = values.size();
out.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0)
{
return;
}
// 窗口点数 X:局部“差异强度”的统计**窗口长度**(点数),窗口 = [i−r, i+r],
// r = (X−1)/2。窗口长度**应略大于纹波周期的一半**:
// 只有当窗口跨越的相位达到 180° 时,窗口内才必定包含一个波峰或波谷,
// “局部 |交流分量| 的峰值”因而恒等于纹波振幅 A,与相位无关
//(正常区域恒为 A、弱区恒为 A/2,比值恰为 2);窗口再小就会跟随相位起伏
//(把正常区域的波峰也一起放大),远大于一个周期时又会把弱区与邻近正常区一起统计掉。
// 窗口越短、混叠区(弱区边界各 ±r)越窄,对“只有一两个周期(甚至半周期)变弱”的
// 情形越有效。
const int span = std::max(3, std::min(window, 1000000));
const int strengthRadius = std::max(1, (span - 1) / 2);
// 趋势窗口取局部窗口的 2 倍(≈ 一个完整纹波周期):只有按整周期取平均,
// 趋势线才恰好是纹波的中心线(交流分量均值为零),待放大的弱区也就**不会**
// 因为自己变“扁”而把趋势线一起拉偏。
const int trendRadius = std::max(1, strengthRadius * 2);
// 包络窗口固定取局部窗口的 8 倍:用来给出“周边本来的振幅范围”,
// 既作为放大基准(中心值)也作为输出的夹取上下界。
const std::size_t envelopeRadius =
static_cast<std::size_t>(std::max(1, std::min(strengthRadius * 8, 16384)));
const double maxGain = static_cast<double>(std::max(1, gain));
// 1) 周边包络:邻域内采样值的最小/最大值(单调队列,O(n))。
// 邻域**必须明显大于一个纹波周期**,否则包络会跟着纹波起伏,
// 放大后会把相邻正常区域的相位也一并扭曲。
std::vector<float> envMin;
std::vector<float> envMax;
SlidingExtrema(values, envelopeRadius, envMin, envMax);
// 2) 局部差异强度 = 邻域内 |采样值 − 趋势线| 的**最大值**(即局部纹波振幅)。
//
// 这里刻意**不用**另外两种看起来更自然的做法:
// * “|采样值 − 趋势线| 的平均”:趋势线是低频量,会被待放大的弱区本身拉偏,
// 于是弱区周边本来正常的采样点也会被判成“差异偏小”而被一起放大;
// * “原始采样值的峰峰值”:峰峰值要同时覆盖波峰与波谷才与相位无关,
// 这要求窗口跨越 360°,窗口会大到把弱区与邻近正常区平均掉
// (实测一个周期宽的弱区只能恢复三分之一)。
// |交流分量| 的最大值只需要窗口跨越 180°(窗口约半个周期)就与相位无关,
// 既能准确识别弱区,又因为窗口很短而具备足够好的定位精度。
//
// 又因为增益下界恒为 1(见第 4 步),正常区域的增益恰好是 1,
// 输出与输入**逐位相同**,因此对其它数据点真正没有任何影响。
//
// 极值窗口按**实际长度**(端点截断)统计,端点因此会低估振幅、
// 导致增益被高估;这一端点效应由第 4 步的收敛系数消除。
std::vector<float> trend;
ComputeMovingAverage(values, trendRadius, trend);
std::vector<float> strength;
{
// `ac` 只在求局部最大值时用到:单独放在一个作用域里,
// 亿级采样下这一份 100 MB 级的中转数组能尽早归还给系统。
std::vector<float> ac(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) && std::isfinite(trend[index]))
{
ac[index] = static_cast<float>(std::fabs(value - static_cast<double>(trend[index])));
}
}
SlidingMax(ac, static_cast<std::size_t>(strengthRadius), strength);
}
// 3) 目标强度:差异强度的 75 分位。用直方图统计 → O(1) 额外内存,
// 且不会被个别尖峰抬高(用均值会),也不会被最尖处完全主导(用最大值会)。
//
// ⚠️ 统计时**排除首尾的端点收敛区**(见第 4 步):那里的趋势窗口被截断,
// “|交流分量| 的最大值”整体偏大,会把分位数抬高、导致目标强度偏大。
// 待统计的采样点数不足时退回全段统计。
const std::size_t edgeGuard =
static_cast<std::size_t>(2) * static_cast<std::size_t>(trendRadius);
// 剩下足够多的采样点时才有必要排除端点区(短信号就直接全段统计)。
constexpr std::size_t kEdgeGuardMinimum = 256;
const bool useGuard = (count > edgeGuard * 2 + kEdgeGuardMinimum);
const auto inGuard = [count, edgeGuard, useGuard](std::size_t index)
{
if (!useGuard)
{
return true;
}
return (index >= edgeGuard) && ((count - 1 - index) >= edgeGuard);
};
float maxStrength = 0.0f;
for (std::size_t index = 0; index < count; ++index)
{
const float value = strength[index];
if (inGuard(index) && std::isfinite(value) && value > maxStrength)
{
maxStrength = value;
}
}
if (!(maxStrength > 0.0f))
{
// 完全平坦(或全为 NaN)的信号:无差异可放大,等价于原样输出。
for (std::size_t index = 0; index < count; ++index)
{
const double value = values[index];
if (std::isfinite(value))
{
out[index] = static_cast<float>(value);
}
}
return;
}
constexpr int kBinCount = 2048;
std::vector<int> histogram(static_cast<std::size_t>(kBinCount), 0);
std::size_t sampleCount = 0;
for (std::size_t index = 0; index < count; ++index)
{
const float value = strength[index];
if (!inGuard(index) || !std::isfinite(value))
{
continue;
}
int bin = static_cast<int>(static_cast<double>(value) /
static_cast<double>(maxStrength) * kBinCount);
bin = std::max(0, std::min(bin, kBinCount - 1));
++histogram[static_cast<std::size_t>(bin)];
++sampleCount;
}
double target = static_cast<double>(maxStrength);
if (sampleCount > 0)
{
const std::size_t wanted = (sampleCount * 75) / 100;
std::size_t accumulated = 0;
for (int bin = 0; bin < kBinCount; ++bin)
{
accumulated += static_cast<std::size_t>(histogram[static_cast<std::size_t>(bin)]);
if (accumulated > wanted)
{
// 取桶中心,避免目标强度贴到桶的下边界而整体偏小。
target = (static_cast<double>(bin) + 0.5) / kBinCount *
static_cast<double>(maxStrength);
break;
}
}
}
// 4) 逐点增益并合成输出。
// * 基准 = 趋势线(纹波的中心线,与信号整体同步,纹波在其中上下摆动);
// * 增益下界为 1 → 局部振幅不比目标值小的采样点**输出与原值逐位相同**
// (基准被 1 倍增益完全消掉,因此对其它数据点没有任何影响);
// * 放大后的取值再夹到周边包络内 → **任何一点都不会超出原有的振幅范围**
// (尤其重要:被放大区域两侧自身的振幅本来就很大,不应被继续抬升)。
for (std::size_t index = 0; index < count; ++index)
{
const double value = values[index];
if (!std::isfinite(value))
{
continue;
}
const bool hasEnvelope = std::isfinite(envMin[index]) && std::isfinite(envMax[index]);
const double base =
std::isfinite(trend[index]) ? static_cast<double>(trend[index]) : value;
double pointGain = 1.0;
const float local = strength[index];
if (std::isfinite(local) && local > 0.0f)
{
pointGain = target / static_cast<double>(local);
}
pointGain = std::max(1.0, std::min(pointGain, maxGain));
// 端点收敛:首尾各 2r 个采样点的**趋势窗口与极值窗口都被截断**
//(趋势窗口半径 2r、极值窗口半径 r),局部振幅与中心线都被系统性估偏。
// 因此这些采样点一律保持原样(增益恰为 1),再在接下来的 2r 个点内
// 平滑过渡到完整增益,避免记录首尾被整片抬升。
//
// ⚠️ 不能靠“镜像延拓”解决:镜像后的窗口覆盖的真实下标集合与截断窗口相同
//(例如 i=16、r=40 时两者都只覆盖真实下标 0..56),因此不会带来任何新信息。
const double taperStart = static_cast<double>(trendRadius);
const double taperSpan = std::max(1.0, static_cast<double>(trendRadius));
const double distanceToEdge =
static_cast<double>(std::min(index, count - 1 - index));
const double taper =
std::max(0.0, std::min(1.0, (distanceToEdge - taperStart) / taperSpan));
pointGain = 1.0 + (pointGain - 1.0) * taper;
double amplified = base + pointGain * (value - base);
if (hasEnvelope)
{
amplified = std::max(static_cast<double>(envMin[index]),
std::min(amplified, static_cast<double>(envMax[index])));
}
out[index] = static_cast<float>(amplified);
}
}
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::DiffAmpItem* Lane::FindDiffAmpItem(const SignalRef& ref) const
{
for (const DiffAmpItem& item : diffAmpItems)
{
if (item.ref == ref)
{
return &item;
}
}
return nullptr;
}
Lane::DiffAmpItem& Lane::EnsureDiffAmpItem(const SignalRef& ref)
{
for (DiffAmpItem& item : diffAmpItems)
{
if (item.ref == ref)
{
return item;
}
}
DiffAmpItem item;
item.ref = ref;
// 默认参数:窗口点数 X = 80(长度)、放大倍数 N = 2。
// 窗口长度需略大于纹波周期的一半(见 ComputeDiffAmplify 的说明),
// 80 点可覆盖周期约 160 采样以内的纹波。
item.points = 80;
item.order = 2;
diffAmpItems.push_back(item);
return diffAmpItems.back();
}
void Lane::RemoveDiffAmpItem(const SignalRef& ref)
{
diffAmpItems.erase(
std::remove_if(diffAmpItems.begin(), diffAmpItems.end(),
[&ref](const DiffAmpItem& item) { return item.ref == ref; }),
diffAmpItems.end());
}
int FindNearestSample(const DataFile& file, double time)
{
const std::size_t count = file.time.size();
if (count == 0)
{
return -1;
}
if (file.timeMonotonic)
{
const std::vector<double>::const_iterator lower =
std::lower_bound(file.time.begin(), file.time.end(), time);
if (lower == file.time.begin())
{
return 0;
}
if (lower == file.time.end())
{
return static_cast<int>(count - 1);
}
const std::size_t rightIndex = static_cast<std::size_t>(lower - file.time.begin());
const std::size_t leftIndex = rightIndex - 1;
const double leftDistance = std::fabs(time - file.time[leftIndex]);
const double rightDistance = std::fabs(file.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(file.time[index]))
{
continue;
}
const double distance = std::fabs(file.time[index] - time);
if (distance < bestDistance)
{
bestDistance = distance;
best = static_cast<int>(index);
}
}
return best;
}
bool SampleSignalAt(const DataFile& file, const SignalData& signal, double time, bool interpolate,
double& value)
{
value = kMissing;
if (file.time.empty() || signal.values.empty())
{
return false;
}
if (time < file.minTime || time > file.maxTime)
{
return false;
}
const std::vector<double>& values = signal.values;
if (!file.timeMonotonic)
{
const int nearest = FindNearestSample(file, 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(file.time.begin(), file.time.end(), time);
const std::size_t rightIndex = static_cast<std::size_t>(lower - file.time.begin());
if (rightIndex == 0)
{
value = values[0];
return std::isfinite(value);
}
if (rightIndex >= file.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 = file.time[leftIndex];
const double t1 = file.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;
}
// ============================ TabConfig ============================
TabConfig::TabConfig()
: m_name(L"视图 1")
{
}
void TabConfig::SyncToDataSet(const DataSet& dataSet)
{
const std::size_t fileCount = dataSet.FileCount();
m_visible.resize(fileCount);
m_collapsed.resize(fileCount, false);
for (std::size_t fileIndex = 0; fileIndex < fileCount; ++fileIndex)
{
const DataFile* file = dataSet.FileAt(static_cast<int>(fileIndex));
if (file == nullptr)
{
continue;
}
// 新增信号默认为未勾选(由用户自行决定查看哪些信号),
// 其显示外观已在 DataSet 中初始化完毕。
m_visible[fileIndex].resize(file->signals.size(), false);
}
// 清理已失效的网格引用(选中状态由 DataSet 统一维护)。
for (Lane& lane : m_lanes)
{
std::vector<SignalRef>& refs = lane.signals;
refs.erase(std::remove_if(refs.begin(), refs.end(),
[&dataSet](const SignalRef& ref) { return dataSet.Resolve(ref) == nullptr; }),
refs.end());
}
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
[](const Lane& lane) { return lane.signals.empty(); }),
m_lanes.end());
SyncLanes();
}
void TabConfig::RemapAfterFileRemoval(int fileIndex)
{
if (fileIndex < 0)
{
return;
}
const std::size_t slot = static_cast<std::size_t>(fileIndex);
if (slot < m_visible.size())
{
m_visible.erase(m_visible.begin() + fileIndex);
}
if (slot < m_collapsed.size())
{
m_collapsed.erase(m_collapsed.begin() + fileIndex);
}
for (Lane& lane : m_lanes)
{
std::vector<SignalRef>& refs = lane.signals;
refs.erase(std::remove_if(refs.begin(), refs.end(),
[fileIndex](const SignalRef& ref)
{ return ref.fileIndex == fileIndex; }),
refs.end());
for (SignalRef& ref : refs)
{
if (ref.fileIndex > fileIndex)
{
--ref.fileIndex;
}
}
}
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
[](const Lane& lane) { return lane.signals.empty(); }),
m_lanes.end());
}
void TabConfig::RemapAfterSignalRemoval(int fileIndex, int signalIndex)
{
if (fileIndex < 0 || signalIndex < 0)
{
return;
}
// 可见性表:删掉该信号对应的那一列。
const std::size_t slot = static_cast<std::size_t>(fileIndex);
if (slot < m_visible.size())
{
std::vector<bool>& fileVisible = m_visible[slot];
if (static_cast<std::size_t>(signalIndex) < fileVisible.size())
{
fileVisible.erase(fileVisible.begin() + signalIndex);
}
}
// 网格归属:删掉该信号本身,并把同一文件内位于其后的引用下标前移一位。
for (Lane& lane : m_lanes)
{
std::vector<SignalRef>& refs = lane.signals;
refs.erase(std::remove_if(refs.begin(), refs.end(),
[fileIndex, signalIndex](const SignalRef& ref)
{
return ref.fileIndex == fileIndex &&
ref.signalIndex == signalIndex;
}),
refs.end());
for (SignalRef& ref : refs)
{
if (ref.fileIndex == fileIndex && ref.signalIndex > signalIndex)
{
--ref.signalIndex;
}
}
// 均值滤波 / 中心轴边沿计数的条目同样持有信号引用,必须一起删除/前移。
const auto purgeRefs = [fileIndex, signalIndex](std::vector<Lane::MeanFilterItem>& items)
{
items.erase(std::remove_if(items.begin(), items.end(),
[fileIndex, signalIndex](const Lane::MeanFilterItem& item)
{
return item.ref.fileIndex == fileIndex &&
item.ref.signalIndex == signalIndex;
}),
items.end());
for (Lane::MeanFilterItem& item : items)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex > signalIndex)
{
--item.ref.signalIndex;
}
}
};
purgeRefs(lane.meanFilterItems);
purgeRefs(lane.edgeCenterItems);
purgeRefs(lane.diffAmpItems);
}
// 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
[](const Lane& lane) { return lane.signals.empty(); }),
m_lanes.end());
}
bool TabConfig::IsVisible(const SignalRef& ref) const
{
if (ref.fileIndex < 0 || static_cast<std::size_t>(ref.fileIndex) >= m_visible.size())
{
return false;
}
const std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(ref.fileIndex)];
if (ref.signalIndex < 0 || static_cast<std::size_t>(ref.signalIndex) >= fileVisible.size())
{
return false;
}
return fileVisible[static_cast<std::size_t>(ref.signalIndex)];
}
void TabConfig::SetVisible(const SignalRef& ref, bool visible)
{
if (ref.fileIndex < 0 || static_cast<std::size_t>(ref.fileIndex) >= m_visible.size())
{
return;
}
std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(ref.fileIndex)];
if (ref.signalIndex < 0 || static_cast<std::size_t>(ref.signalIndex) >= fileVisible.size())
{
return;
}
if (fileVisible[static_cast<std::size_t>(ref.signalIndex)] == visible)
{
return;
}
fileVisible[static_cast<std::size_t>(ref.signalIndex)] = visible;
// 合并模式下新勾选的信号独占一个新网格:切到自定义划分后,
// RebuildLanes 会把尚未分配网格的可见信号各自放入新网格。
if (visible && m_laneMode == LaneMode::MERGED)
{
m_laneMode = LaneMode::CUSTOM;
}
SyncLanes();
}
void TabConfig::ToggleVisible(const SignalRef& ref)
{
SetVisible(ref, !IsVisible(ref));
}
void TabConfig::SetFileVisible(int fileIndex, bool visible)
{
if (fileIndex < 0 || static_cast<std::size_t>(fileIndex) >= m_visible.size())
{
return;
}
std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(fileIndex)];
// 合并模式下“新增”的信号同样应各自独占网格(与 SetVisible 一致):
// 先切到自定义划分,RebuildLanes 会把尚未分配网格的可见信号追加到最下方。
// 只有确实存在“原本不可见”的信号时才切模式,避免重复点击默默改掉划分策略。
const bool anyHidden =
std::find(fileVisible.begin(), fileVisible.end(), false) != fileVisible.end();
if (visible && anyHidden && m_laneMode == LaneMode::MERGED)
{
m_laneMode = LaneMode::CUSTOM;
}
std::fill(fileVisible.begin(), fileVisible.end(), visible);
SyncLanes();
}
void TabConfig::SetAllVisible(bool visible)
{
bool anyHidden = false;
for (const std::vector<bool>& fileVisible : m_visible)
{
if (std::find(fileVisible.begin(), fileVisible.end(), false) != fileVisible.end())
{
anyHidden = true;
break;
}
}
// 同上:合并模式下新增的信号各自独占网格,且排列在最下方。
if (visible && anyHidden && m_laneMode == LaneMode::MERGED)
{
m_laneMode = LaneMode::CUSTOM;
}
for (std::vector<bool>& fileVisible : m_visible)
{
std::fill(fileVisible.begin(), fileVisible.end(), visible);
}
SyncLanes();
}
void TabConfig::SoloSignal(const SignalRef& ref)
{
for (std::vector<bool>& fileVisible : m_visible)
{
std::fill(fileVisible.begin(), fileVisible.end(), false);
}
SetVisible(ref, true);
}
bool TabConfig::FileHasVisible(int fileIndex) const
{
if (fileIndex < 0 || static_cast<std::size_t>(fileIndex) >= m_visible.size())
{
return false;
}
const std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(fileIndex)];
return std::find(fileVisible.begin(), fileVisible.end(), true) != fileVisible.end();
}
bool TabConfig::FileAllVisible(int fileIndex) const
{
if (fileIndex < 0 || static_cast<std::size_t>(fileIndex) >= m_visible.size())
{
return false;
}
const std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(fileIndex)];
if (fileVisible.empty())
{
return false;
}
return std::find(fileVisible.begin(), fileVisible.end(), false) == fileVisible.end();
}
bool TabConfig::AnyVisible() const
{
for (const std::vector<bool>& fileVisible : m_visible)
{
if (std::find(fileVisible.begin(), fileVisible.end(), true) != fileVisible.end())
{
return true;
}
}
return false;
}
int TabConfig::VisibleCount() const
{
int count = 0;
for (const std::vector<bool>& fileVisible : m_visible)
{
count += static_cast<int>(std::count(fileVisible.begin(), fileVisible.end(), true));
}
return count;
}
void TabConfig::SetMode(LaneMode mode)
{
m_laneMode = mode;
SyncLanes();
}
void TabConfig::SyncLanes()
{
RebuildLanes(m_visible, m_lanes, m_laneMode);
}
int TabConfig::LaneOfSignal(const SignalRef& ref) const
{
for (std::size_t lane = 0; lane < m_lanes.size(); ++lane)
{
const std::vector<SignalRef>& refs = m_lanes[lane].signals;
if (std::find(refs.begin(), refs.end(), ref) != refs.end())
{
return static_cast<int>(lane);
}
}
return -1;
}
int TabConfig::MoveSignalToLane(const DataSet& dataSet, const SignalRef& ref, int laneIndex)
{
if (dataSet.Resolve(ref) == nullptr)
{
return -1;
}
const int sourceLane = LaneOfSignal(ref);
if (laneIndex >= 0 && laneIndex == sourceLane)
{
// 拖回原网格:无需变动,避免因网格回收导致顺序抖动。
return sourceLane;
}
// 先把目标网格的“锚信号”固定下来:移除信号后空网格会被回收,
// 下标随之变化,必须用不依赖下标的引用找回真正的目标网格
// (否则会变成“与原网格交换位置”而不是合并进去)。
SignalRef targetAnchor;
bool hasTarget = (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()));
if (hasTarget)
{
const std::vector<SignalRef>& targetSignals = m_lanes[static_cast<std::size_t>(laneIndex)].signals;
for (const SignalRef& candidate : targetSignals)
{
if (!(candidate == ref))
{
targetAnchor = candidate;
break;
}
}
}
// 手动指定归属意味着切换到自定义划分策略。
m_laneMode = LaneMode::CUSTOM;
for (Lane& lane : m_lanes)
{
std::vector<SignalRef>& refs = lane.signals;
refs.erase(std::remove(refs.begin(), refs.end(), ref), refs.end());
}
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
[](const Lane& lane) { return lane.signals.empty(); }),
m_lanes.end());
int destination = -1;
if (hasTarget && targetAnchor.IsValid())
{
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
const std::vector<SignalRef>& refs = m_lanes[index].signals;
if (std::find(refs.begin(), refs.end(), targetAnchor) != refs.end())
{
destination = static_cast<int>(index);
break;
}
}
}
if (destination >= 0)
{
m_lanes[static_cast<std::size_t>(destination)].signals.push_back(ref);
}
else
{
Lane lane;
lane.autoRange = true;
lane.signals.push_back(ref);
m_lanes.push_back(std::move(lane));
}
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
}
return LaneOfSignal(ref);
}
int TabConfig::SplitLane(int laneIndex)
{
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
{
return -1;
}
const std::vector<SignalRef> signals = m_lanes[static_cast<std::size_t>(laneIndex)].signals;
if (signals.size() <= 1)
{
// 只有一条信号时无需拆分(本就独占一个网格)。
return -1;
}
// 手动排布 → 切换到自定义划分策略。
m_laneMode = LaneMode::CUSTOM;
// 用原网格的“网格自身状态”(Y 轴自适应/边沿计数等)作为每一份的初始状态,
// 这样拆分出来的网格保持原网格的显示设置。
const Lane source = m_lanes[static_cast<std::size_t>(laneIndex)];
std::vector<Lane> pieces;
pieces.reserve(signals.size());
for (const SignalRef& ref : signals)
{
Lane lane;
lane.autoRange = source.autoRange;
lane.yMin = source.yMin;
lane.yMax = source.yMax;
lane.fitYToSelection = source.fitYToSelection;
lane.edgeCount = source.edgeCount;
lane.edgeValue = source.edgeValue;
lane.edgeValueValid = source.edgeValueValid;
lane.edgePanelOpen = source.edgePanelOpen;
lane.edgePanelX = source.edgePanelX;
lane.edgePanelY = source.edgePanelY;
lane.edgeCenter = source.edgeCenter;
lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen;
lane.edgeCenterPanelX = source.edgeCenterPanelX;
lane.edgeCenterPanelY = source.edgeCenterPanelY;
lane.diffAmp = source.diffAmp;
lane.diffAmpPanelOpen = source.diffAmpPanelOpen;
lane.diffAmpPanelX = source.diffAmpPanelX;
lane.diffAmpPanelY = source.diffAmpPanelY;
lane.meanFilter = source.meanFilter;
lane.meanFilterPanelOpen = source.meanFilterPanelOpen;
lane.meanFilterPanelX = source.meanFilterPanelX;
lane.meanFilterPanelY = source.meanFilterPanelY;
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 差异放大条目。
lane.meanFilterItems.clear();
lane.edgeCenterItems.clear();
lane.diffAmpItems.clear();
for (const Lane::MeanFilterItem& item : source.meanFilterItems)
{
if (item.ref == ref)
{
lane.meanFilterItems.push_back(item);
}
}
for (const Lane::EdgeCenterItem& item : source.edgeCenterItems)
{
if (item.ref == ref)
{
lane.edgeCenterItems.push_back(item);
}
}
for (const Lane::DiffAmpItem& item : source.diffAmpItems)
{
if (item.ref == ref)
{
lane.diffAmpItems.push_back(item);
}
}
lane.signals.push_back(ref);
pieces.push_back(std::move(lane));
}
// 用 pieces 替换原网格(插入到原位置,因此其后方的网格整体顺次后移)。
m_lanes.erase(m_lanes.begin() + laneIndex);
m_lanes.insert(m_lanes.begin() + laneIndex, pieces.begin(), pieces.end());
// 统一重命名,保证编号连续(GX 编号在各显示位置均由此位置推导)。
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
}
return laneIndex;
}
void TabConfig::RemapAfterSignalInsert(int fileIndex, int insertPos)
{
if (fileIndex < 0 || insertPos < 0)
{
return;
}
// 可见性表:在插入点补一列(新信号默认不可见,由调用方按需勾选)。
const std::size_t slot = static_cast<std::size_t>(fileIndex);
if (slot < m_visible.size() && static_cast<std::size_t>(insertPos) <= m_visible[slot].size())
{
m_visible[slot].insert(m_visible[slot].begin() + insertPos, false);
}
// 网格内的信号引用:同一文件中位于插入点之后的引用下标整体后移。
for (Lane& lane : m_lanes)
{
for (SignalRef& ref : lane.signals)
{
if (ref.fileIndex == fileIndex && ref.signalIndex >= insertPos)
{
++ref.signalIndex;
}
}
// 均值滤波 / 中心轴条目同样持有信号引用,必须一起后移。
for (Lane::MeanFilterItem& item : lane.meanFilterItems)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex >= insertPos)
{
++item.ref.signalIndex;
}
}
for (Lane::EdgeCenterItem& item : lane.edgeCenterItems)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex >= insertPos)
{
++item.ref.signalIndex;
}
}
for (Lane::DiffAmpItem& item : lane.diffAmpItems)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex >= insertPos)
{
++item.ref.signalIndex;
}
}
}
}
bool TabConfig::MoveLaneOfSignalBelow(const SignalRef& moved, const SignalRef& anchor)
{
const int from = LaneOfSignal(moved);
if (from < 0 || LaneOfSignal(anchor) < 0)
{
return false;
}
Lane lane = std::move(m_lanes[static_cast<std::size_t>(from)]);
m_lanes.erase(m_lanes.begin() + from);
// erase 之后锚点网格的下标可能变化,必须用信号引用重新定位。
const int target = LaneOfSignal(anchor);
if (target < 0)
{
m_lanes.push_back(std::move(lane));
}
else
{
m_lanes.insert(m_lanes.begin() + target + 1, std::move(lane));
}
// 手动排布 → 切换到自定义划分策略,避免后续重建网格时顺序被还原。
m_laneMode = LaneMode::CUSTOM;
RenameLanes();
return true;
}
bool TabConfig::MoveLane(int from, int target)
{
const int count = static_cast<int>(m_lanes.size());
if (from < 0 || from >= count || target < 0 || target > count)
{
return false;
}
// `target` 语义 = 「插入到该下标之前」;向后拖时扣除自身占位后再比较,
// 保证"拖到自己原来的位置"不会产生无意义的移动。
int destination = target;
if (destination > from)
{
--destination;
}
destination = std::max(0, std::min(destination, count - 1));
if (destination == from)
{
return false;
}
Lane moved = std::move(m_lanes[static_cast<std::size_t>(from)]);
m_lanes.erase(m_lanes.begin() + from);
m_lanes.insert(m_lanes.begin() + destination, std::move(moved));
// 手动排布 → 切换到自定义划分策略,避免后续重建网格时顺序被还原。
m_laneMode = LaneMode::CUSTOM;
RenameLanes();
return true;
}
void TabConfig::RenameLanes()
{
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
}
}
bool TabConfig::IsCollapsed(int fileIndex) const
{
if (fileIndex < 0 || static_cast<std::size_t>(fileIndex) >= m_collapsed.size())
{
return false;
}
return m_collapsed[static_cast<std::size_t>(fileIndex)];
}
void TabConfig::ToggleCollapsed(int fileIndex)
{
if (fileIndex < 0)
{
return;
}
if (static_cast<std::size_t>(fileIndex) >= m_collapsed.size())
{
m_collapsed.resize(static_cast<std::size_t>(fileIndex) + 1, false);
}
m_collapsed[static_cast<std::size_t>(fileIndex)] = !m_collapsed[static_cast<std::size_t>(fileIndex)];
}
void TabConfig::ComputeLaneRange(const DataSet& dataSet, int laneIndex, double& yMin, double& yMax) const
{
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
{
yMin = 0.0;
yMax = 1.0;
return;
}
const Lane& lane = m_lanes[static_cast<std::size_t>(laneIndex)];
if (!lane.autoRange)
{
yMin = lane.yMin;
yMax = lane.yMax;
if (!(yMax > yMin))
{
yMin = 0.0;
yMax = 1.0;
}
return;
}
double low = std::numeric_limits<double>::infinity();
double high = -std::numeric_limits<double>::infinity();
for (const SignalRef& ref : lane.signals)
{
if (!IsVisible(ref))
{
continue;
}
const SignalData* signal = dataSet.Resolve(ref);
if (signal == nullptr || signal->finiteCount == 0)
{
continue;
}
low = std::min(low, signal->minValue);
high = std::max(high, signal->maxValue);
}
if (!std::isfinite(low) || !std::isfinite(high))
{
low = 0.0;
high = 1.0;
}
PadRange(low, high);
yMin = low;
yMax = high;
}
bool TabConfig::ComputeVisibleTimeBounds(const DataSet& dataSet, double& minTime, double& maxTime) const
{
double low = std::numeric_limits<double>::infinity();
double high = -std::numeric_limits<double>::infinity();
for (std::size_t fileIndex = 0; fileIndex < m_visible.size(); ++fileIndex)
{
const std::vector<bool>& fileVisible = m_visible[fileIndex];
if (std::find(fileVisible.begin(), fileVisible.end(), true) == fileVisible.end())
{
continue;
}
const DataFile* file = dataSet.FileAt(static_cast<int>(fileIndex));
if (file == nullptr)
{
continue;
}
if (std::isfinite(file->minTime))
{
low = std::min(low, file->minTime);
}
if (std::isfinite(file->maxTime))
{
high = std::max(high, file->maxTime);
}
}
if (!std::isfinite(low) || !std::isfinite(high) || !(high > low))
{
return false;
}
minTime = low;
maxTime = high;
return true;
}
} // namespace jdi