输入框(TextEdit,全组件共用): - 修复退格一次删除两个字符:WM_CHAR 与 WM_KEYDOWN 重复处理了退格/删除, 现统一由 OnEditKey 处理,WM_CHAR 忽略全部控制字符。 - 进入编辑(整段选中)后按右键:取消全选并把插入符移到文本最右侧。 - 存在选区时松开 Shift 再按左右方向键,插入符收拢到选区的左/右端。 - 修复整段选中后按 Home/End 不收拢选区的问题。 标签页: - 按加粗字体测量宽度,避免激活页长名称被提前省略(预览过短)。 - 重命名过程中按正在输入的文本测量并加宽上限(320px),标签随输入增长, 解决长内容绘制错误;输入溢出时整体等比收缩,保证「+」始终可见。 信号显示外观跨标签页同步: - 外观(颜色/自定义色记忆/透明度/线宽/样式)从 TabConfig 上移到 DataSet 的 SignalData,成为信号的全局属性;可见性仍按标签页独立保存。 - 因此任一视图中修改某信号的颜色/线宽/透明度/样式,其它视图自动一致。 工具栏: - 移除「全选信号」按钮;「清空勾选」改为「清空网格」, 「拆分信号网格」改为「拆分网格」。 自适应: - 无可见信号(未勾选/全部取消/清空网格)时复位 ViewInitialized, 下次勾选第一个信号会重新自适应,而不是停留在旧视图范围。 绘图区标尺与网格: - 幅值刻度密度按网格高度自适应(3-8 档),各网格刻度数量一致; 网格较矮时同步收紧标签行距,提高可读密度。 - 网格线与五等分估读线加深,与网格边界(蓝灰)保持明显区分。 - 时间轴列名补上单位,显示为 time (s)。
1089 lines
28 KiB
C++
1089 lines
28 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)
|
|
{
|
|
// 收集全部可见信号引用。
|
|
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;
|
|
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;
|
|
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;
|
|
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"线段";
|
|
}
|
|
}
|
|
|
|
// ============================ DataSet ============================
|
|
|
|
void DataSet::Clear()
|
|
{
|
|
m_files.clear();
|
|
}
|
|
|
|
bool DataSet::AddFromTable(const CsvTable& table, const std::wstring& filePath, std::wstring& error,
|
|
int& fileIndex)
|
|
{
|
|
error.clear();
|
|
fileIndex = -1;
|
|
|
|
if (!table.HasData())
|
|
{
|
|
error = L"数据表为空。";
|
|
return false;
|
|
}
|
|
|
|
const std::size_t columnCount = table.headers.size();
|
|
const std::size_t rowCount = table.rows.size();
|
|
|
|
DataFile file;
|
|
|
|
// 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.resize(rowCount);
|
|
std::size_t finiteTimeCount = 0;
|
|
bool monotonic = true;
|
|
double previousTime = 0.0;
|
|
bool hasPrevious = false;
|
|
|
|
for (std::size_t row = 0; row < rowCount; ++row)
|
|
{
|
|
const double value = table.rows[row][static_cast<std::size_t>(timeColumn)];
|
|
file.time[row] = value;
|
|
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. 构建信号列。
|
|
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.resize(rowCount);
|
|
double minValue = std::numeric_limits<double>::infinity();
|
|
double maxValue = -std::numeric_limits<double>::infinity();
|
|
std::size_t finiteCount = 0;
|
|
|
|
for (std::size_t row = 0; row < rowCount; ++row)
|
|
{
|
|
const double value = table.rows[row][column];
|
|
signal.values[row] = value;
|
|
if (std::isfinite(value))
|
|
{
|
|
++finiteCount;
|
|
minValue = std::min(minValue, value);
|
|
maxValue = std::max(maxValue, value);
|
|
}
|
|
}
|
|
|
|
if (finiteCount == 0)
|
|
{
|
|
signal.minValue = 0.0;
|
|
signal.maxValue = 0.0;
|
|
}
|
|
else
|
|
{
|
|
signal.minValue = minValue;
|
|
signal.maxValue = maxValue;
|
|
}
|
|
signal.finiteCount = finiteCount;
|
|
|
|
// 显示外观随信号一起初始化:调色板按「全局信号序号」取色,
|
|
// 使同一信号在任意标签页中都保持同一默认颜色。
|
|
{
|
|
int paletteCount = 0;
|
|
const std::uint32_t* palette = SignalPalette(paletteCount);
|
|
const std::size_t globalIndex = SignalCount() + file.signals.size();
|
|
signal.style.color =
|
|
(palette != nullptr && paletteCount > 0)
|
|
? palette[globalIndex % static_cast<std::size_t>(paletteCount)]
|
|
: 0xFF268BD2u;
|
|
signal.style.customColor = signal.style.color;
|
|
signal.style.opacity = 1.0f;
|
|
signal.style.lineWidth = 1.6f;
|
|
signal.style.mode = DrawMode::LINE;
|
|
}
|
|
|
|
file.signals.push_back(std::move(signal));
|
|
}
|
|
|
|
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;
|
|
|
|
m_files.push_back(std::move(file));
|
|
fileIndex = static_cast<int>(m_files.size()) - 1;
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// ============================ 采样辅助 ============================
|
|
|
|
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);
|
|
}
|
|
|
|
// 清理已失效的选中状态与网格引用。
|
|
if (m_selected.IsValid() && dataSet.Resolve(m_selected) == nullptr)
|
|
{
|
|
m_selected = SignalRef();
|
|
}
|
|
|
|
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());
|
|
|
|
if (m_selected.fileIndex == fileIndex)
|
|
{
|
|
m_selected = SignalRef();
|
|
}
|
|
else if (m_selected.fileIndex > fileIndex)
|
|
{
|
|
--m_selected.fileIndex;
|
|
}
|
|
}
|
|
|
|
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
|