1. 新增「实验性功能 → 差异放大」(与其它实验功能一样是带勾选的开关)
- 用途:纹波类信号某个周期(甚至半个周期)的 M 形下凹明显偏弱时,
把这段下凹按比例拉回周边振幅,恢复正常周期的对比度,便于看出波形异常。
- 算法(全 O(n),不额外占用与信号长度成正比以外的内存):
以半径约半个纹波周期的滑动平均作为纹波中心线,用邻域内「交流分量的最大值」
作为局部纹波振幅,取全信号振幅的 75 分位作为目标振幅,
逐点增益 = 目标振幅 / 局部振幅(夹在 1 到放大倍数之间),
输出 = 中心线 + 增益 × 交流分量,再夹在周边包络内。
- 关键性质(均已用合成正弦逐点验证):
* 增益下界恒为 1,因此**只有被判定为"偏弱"的采样点会改变**,
其余采样点输出与原值逐位相同 —— 实测逐周期表格中只有一个周期发生变化;
* 放大结果夹在周边包络内,因此**任何一点都不超出原有的振幅范围**,
整体值域与其它周期的峰峰值都不变;
* 端点自动收敛到不放大,避免记录首尾被整片抬升。
- 参数「窗口点数 X」(窗口长度,建议略大于纹波周期的一半)与
「放大倍数 N」(增益上限)都可在浮动面板内按信号编辑。
2. 浮动面板
- 与「均值滤波」完全同一套排版:自上而下逐信号条目,
第一排信号名与复选框、第二排两个参数输入框;取消勾选即不显示该信号的参数与预览曲线;
启用时默认只勾选网格内第一个可见信号。
- 新增「构建为新信号」按钮,按各信号自己的参数把结果插入到源信号下方,
与「均值滤波」的构建机制完全一致(统一走同一套请求与插入流程)。
3. 光标交互
- 纵向光标额外标注差异放大曲线在该时刻的取值,配色与原始信号一致,
数值尾部括号内容为「差异化」,双光标时同样带增量。
4. 修复「构建为新信号」按钮与面板下边界的间距
- 原按钮行直接贴着面板下边框,与面板左右内边距及内容间距都不协调;
现在按钮上方与下方各保留一个与左右一致的内边距(均值滤波与差异放大同时生效)。
测试:离屏验证套件新增两组检查(合成正弦上算法只改动一个周期、放大不越界、
参数上限生效、面板排版与按钮间距;再写一份合成纹波文件真正加载渲染并核对曲线数值),
合计 289 项检查全部通过;验证过程不显示窗口、不移动鼠标键盘、不抓屏。
2282 lines
68 KiB
C++
2282 lines
68 KiB
C++
#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
|