Files
Just-DataInspector/Utilities/Src/DataModel.cpp
T
DeepSeek V4.1 Flash 5182bd8aa3 实验性功能完善:面板交互修正 + 新增「边沿计数(中心轴)」
一、实验性功能浮动面板的交互修正(「边沿计数」与「边沿计数(中心轴)」共用)
- 面板只有**标题栏**可以拖拽:鼠标在标题栏显示抓握光标(IDC_SIZEALL),
  在面板主体(含统计行与底部编辑行)保持原始箭头,按下主体也不再拖动面板(仅消费点击,
  不会穿透到底层的平移/选区/信号拖拽)。
- ✕ 关闭按钮:鼠标悬停时高亮(danger 色底 + danger 色叉),但**不改变鼠标光标样式**。
- 为此新增 `EdgePanelLayout`(panel / title / close / edit / rows)与
  `EdgePanelLayoutFor(laneIndex, centerAxis)`,`QueryCursor`、`OnMouseMove`、`OnLButtonDown`
  与绘制统一按该布局判定。

二、新增「实验性功能 → 边沿计数(中心轴)」(带 √ 的切换项,按网格独立)
- 开启后为网格内**每条信号**叠加绘制一条"中心平均值曲线":
  每个数据点 = 自身与左右各 N 个数据点(共 2N+1 点,边界处按实际窗口)的平均值。
  用滑动窗口求和构建,整体 O(n),不额外占用前缀和内存;结果按 (信号, N) 缓存
  (deque 保证引用有效),数据层变化或 N 改变时清空。
- 该曲线只是叠加显示,**不会**加入左侧信号列表;
  为与波形区分,颜色按背景明暗把信号色向黑/白方向拉远(浅色主题加深、深色主题提亮),
  线宽略粗于原信号。
- 纵向光标仍会标注这条曲线的取值:在原取值胶囊之外额外绘制一个「中 <值>」胶囊
  (与曲线同色、半透明),并复用统一的避让逻辑(PlaceCursorLabel,已抽成函数)。
- 统计规则:**信号从下往上越过中心平均值曲线 → 上升沿;从上往下穿过 → 下降沿**
  (比较 `signal[i] - center[i]` 的符号变化,NaN 视为断点)。
- 浮动面板标题为「中心轴边沿计数」,样式与「边沿计数」面板一致(GX 徽标 + 标题 + ✕ +
  每信号一行名称与「上升沿 N / 下降沿 M」,数字完整不省略、面板宽度按文本实测自适应);
  面板**底部固定一行「平滑点数 N」内联编辑设置**(新增 EditFilter::NUMBER 编辑目标
  PlotEditTarget::EDGE_CENTER_SMOOTH,夹在 0–1000000),始终位于面板最下方。
- 与双光标模式联动:双光标时标题追加「(光标区间内)」,只统计左右光标之间的区间;
  区间端点恒取两光标时间的 min/max,因此把任一光标拖到另一侧(左右交换)后依然正确。
- ✕ 关闭面板的同时关闭该网格的「边沿计数(中心轴)」功能(面板与曲线一并消失)。

验证(TestData/VERY_BIG_DATA.CSV,编码器A相,125%、1725x996):
- 面板光标形态探测:主体 = ARROW、标题栏 = SIZEALL、✕ = ARROW;
  从主体拖动面板不发生位移(截图核对)。
- ✕ 悬停高亮可见。
- 「边沿计数(中心轴)」:面板标题「中心轴边沿计数」、底部「平滑点数 N = 5」;
  中心平均值曲线(深蓝)清晰可见;把 N 改为 3000 后计数同步更新。
  用独立脚本 .preflight/centerprobe.ps1 按同一规则复算,与界面**逐位一致**:
  N=5 → 上升沿 2712920 / 下降沿 2712913;N=1000 → 2373176 / 2373176。
- 双光标联动:区间 [48.0211, 64.4051](面板标题带「(光标区间内)」)→ 349220 / 349220;
  把光标 1 的时间改到 100(越过光标 2 的 64.4051)后,区间变为 [64.4051, 100],
  计数 778085 / 778084,与按该区间复算的结果一致(±2 为光标时间显示值取整导致的边界差)。
- 纵向光标读数:右光标显示 `4.9417 (-0.071945)` + `中 4.94199`,
  左光标显示 `中 2.9165`(中心曲线取值单独标注)。
- 左侧信号列表仍只有原信号,未新增曲线条目。
- 回归:[1].CSV 上「边沿计数(光标区间内)」正常(Δt:151ms 200us);
  「边沿计数(中心轴)」在 [1].CSV 上曲线清晰可见(上升沿 199 / 下降沿 199)。
- make clean && make all 无警告无错误。
2026-10-09 11:23:15 +08:00

1281 lines
35 KiB
C++

#include "DataModel.hpp"
#include <algorithm>
#include <cmath>
#include <cwctype>
#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;
int edgeCenterSmooth = 5;
bool edgeCenterPanelOpen = true;
double edgeCenterPanelX = -1.0;
double edgeCenterPanelY = -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.edgeCenterSmooth = lane.edgeCenterSmooth;
carry.edgeCenterPanelOpen = lane.edgeCenterPanelOpen;
carry.edgeCenterPanelX = lane.edgeCenterPanelX;
carry.edgeCenterPanelY = lane.edgeCenterPanelY;
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.edgeCenterSmooth = carry.edgeCenterSmooth;
lane.edgeCenterPanelOpen = carry.edgeCenterPanelOpen;
lane.edgeCenterPanelX = carry.edgeCenterPanelX;
lane.edgeCenterPanelY = carry.edgeCenterPanelY;
};
// 收集全部可见信号引用。
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)];
};
if (mode == LaneMode::PER_SIGNAL)
{
// 每条可见信号独占一个网格:网格数量严格跟随可见信号数量。
lanes.clear();
lanes.reserve(visibleRefs.size());
for (const SignalRef& ref : visibleRefs)
{
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();
const std::size_t rowCount = table.rowCount;
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 && !progress(0.75 + 0.25 * static_cast<double>(column + 1) /
static_cast<double>(columnCount)))
{
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;
(void)rowCount;
return true;
}
// ============================ DataSet ============================
void DataSet::Clear()
{
m_files.clear();
}
bool DataSet::AddPreparedFile(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::AddFromTable(const CsvTable& table, const std::wstring& filePath, std::wstring& error,
int& fileIndex)
{
fileIndex = -1;
// 兼容入口:复制一份表格后走与后台加载完全一致的构建流程。
CsvTable local = table;
DataFile file;
if (!BuildDataFile(std::move(local), filePath, SignalCount(), file, error))
{
return false;
}
return AddPreparedFile(std::move(file), fileIndex);
}
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;
}
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;
}
// ============================ 采样辅助 ============================
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 SampleFileAt(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(dataSet);
}
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());
}
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 DataSet& dataSet, 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(dataSet);
}
void TabConfig::ToggleVisible(const DataSet& dataSet, const SignalRef& ref)
{
SetVisible(dataSet, ref, !IsVisible(ref));
}
void TabConfig::SetFileVisible(const DataSet& dataSet, 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)];
std::fill(fileVisible.begin(), fileVisible.end(), visible);
SyncLanes(dataSet);
}
void TabConfig::SetAllVisible(const DataSet& dataSet, bool visible)
{
for (std::vector<bool>& fileVisible : m_visible)
{
std::fill(fileVisible.begin(), fileVisible.end(), visible);
}
SyncLanes(dataSet);
}
void TabConfig::SoloSignal(const DataSet& dataSet, const SignalRef& ref)
{
for (std::vector<bool>& fileVisible : m_visible)
{
std::fill(fileVisible.begin(), fileVisible.end(), false);
}
SetVisible(dataSet, ref, true);
}
bool TabConfig::FileHasVisible(const DataSet& dataSet, int fileIndex) const
{
(void)dataSet;
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(const DataSet& dataSet, int fileIndex) const
{
(void)dataSet;
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(const DataSet& dataSet, LaneMode mode)
{
m_laneMode = mode;
SyncLanes(dataSet);
}
void TabConfig::SyncLanes(const DataSet& dataSet)
{
(void)dataSet;
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);
}
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