#include "DataModel.hpp" #include #include #include #include #include "Theme.hpp" #include "Utf8Util.hpp" namespace jdi { namespace { const double kMissing = std::numeric_limits::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] globalIndex 全局信号序号。 * @return 0xAARRGGBB 颜色值。 */ std::uint32_t PaletteColor(std::size_t globalIndex) { int paletteCount = 0; const std::uint32_t* palette = SignalPalette(paletteCount); if (palette == nullptr || paletteCount <= 0) { return 0xFF268BD2; } return palette[globalIndex % static_cast(paletteCount)]; } /** * @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] styles 显示配置。 * @param [IN] lanes 网格集合(原地更新)。 * @param [IN] mode 网格划分策略。 */ void RebuildLanes(const std::vector>& styles, std::vector& lanes, LaneMode mode) { // 收集全部可见信号引用。 std::vector visible; for (std::size_t fileIndex = 0; fileIndex < styles.size(); ++fileIndex) { for (std::size_t signalIndex = 0; signalIndex < styles[fileIndex].size(); ++signalIndex) { if (styles[fileIndex][signalIndex].visible) { SignalRef ref; ref.fileIndex = static_cast(fileIndex); ref.signalIndex = static_cast(signalIndex); visible.push_back(ref); } } } if (mode == LaneMode::PER_SIGNAL) { // 每条可见信号独占一个网格:网格数量严格跟随可见信号数量。 lanes.clear(); lanes.reserve(visible.size()); for (const SignalRef& ref : visible) { 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 (!visible.empty()) { Lane lane; lane.autoRange = previousAutoRange; lane.yMin = previousYMin; lane.yMax = previousYMax; lane.signals = visible; lanes.push_back(std::move(lane)); } } else { // 自定义划分:仅移除不可见信号与由此产生的空网格,保持用户排布。 for (Lane& lane : lanes) { std::vector& refs = lane.signals; refs.erase(std::remove_if(refs.begin(), refs.end(), [&styles](const SignalRef& ref) { if (ref.fileIndex < 0 || static_cast(ref.fileIndex) >= styles.size()) { return true; } const std::vector& fileStyles = styles[static_cast(ref.fileIndex)]; if (ref.signalIndex < 0 || static_cast(ref.signalIndex) >= fileStyles.size()) { return true; } return !fileStyles[static_cast(ref.signalIndex)].visible; }), refs.end()); } lanes.erase(std::remove_if(lanes.begin(), lanes.end(), [](const Lane& lane) { return lane.signals.empty(); }), lanes.end()); // 未分配网格的可见信号统一追加到第一个网格。 for (const SignalRef& ref : visible) { 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; } if (lanes.empty()) { Lane lane; lane.autoRange = true; lanes.push_back(std::move(lane)); } lanes[0].signals.push_back(ref); } } // 统一重命名,保证编号连续。 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(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(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(timeColumn)] + L")有效数值不足。"; return false; } file.timeColumnName = table.headers[static_cast(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(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::infinity(); double maxValue = -std::numeric_limits::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; file.signals.push_back(std::move(signal)); } if (file.signals.empty()) { error = L"除时间轴外未发现任何数据列。"; return false; } // 3. 统计时间范围。 double tMin = std::numeric_limits::infinity(); double tMax = -std::numeric_limits::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(m_files.size()) - 1; return true; } bool DataSet::RemoveFile(int fileIndex) { if (fileIndex < 0 || fileIndex >= static_cast(m_files.size())) { return false; } // 用移动到末位后弹出的方式移除,使被移除文件的时间轴与全部信号缓冲区在本调用内 // 立即析构归还操作系统,不再等待后续插入覆盖;最后收缩容量,避免残留空闲块。 const std::size_t position = static_cast(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(m_files.size())) { return nullptr; } return &m_files[static_cast(fileIndex)]; } 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(file->signals.size())) { return nullptr; } return &file->signals[static_cast(ref.signalIndex)]; } double DataSet::MinTime() const { double value = std::numeric_limits::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::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::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(count - 1); } const std::size_t rightIndex = static_cast(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(leftIndex) : static_cast(rightIndex); } int best = 0; double bestDistance = std::numeric_limits::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(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& values = signal.values; if (!file.timeMonotonic) { const int nearest = FindNearestSample(file, time); if (nearest < 0 || static_cast(nearest) >= values.size()) { return false; } value = values[static_cast(nearest)]; return std::isfinite(value); } const std::vector::const_iterator lower = std::lower_bound(file.time.begin(), file.time.end(), time); const std::size_t rightIndex = static_cast(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::EnsureStyle(const DataSet& dataSet, int fileIndex, int signalIndex) { (void)dataSet; if (fileIndex < 0 || signalIndex < 0) { return; } const std::size_t fileSlot = static_cast(fileIndex); if (fileSlot >= m_styles.size()) { return; } std::vector& fileStyles = m_styles[fileSlot]; if (static_cast(signalIndex) >= fileStyles.size()) { return; } // 若样式仍为默认状态且颜色为默认色,则按全局序号重新取色。 SignalStyle& style = fileStyles[static_cast(signalIndex)]; std::size_t globalIndex = 0; for (std::size_t index = 0; index < fileSlot; ++index) { if (index < m_styles.size()) { globalIndex += m_styles[index].size(); } } globalIndex += static_cast(signalIndex); style.color = PaletteColor(globalIndex); style.lineWidth = static_cast(kDefaultLineWidth); } void TabConfig::SyncToDataSet(const DataSet& dataSet) { const std::size_t fileCount = dataSet.FileCount(); m_styles.resize(fileCount); m_collapsed.resize(fileCount, false); for (std::size_t fileIndex = 0; fileIndex < fileCount; ++fileIndex) { const DataFile* file = dataSet.FileAt(static_cast(fileIndex)); if (file == nullptr) { continue; } std::vector& fileStyles = m_styles[fileIndex]; const std::size_t previousSize = fileStyles.size(); fileStyles.resize(file->signals.size()); // 新增信号补默认配置(默认不可见,颜色取自调色板)。 for (std::size_t signalIndex = previousSize; signalIndex < file->signals.size(); ++signalIndex) { EnsureStyle(dataSet, static_cast(fileIndex), static_cast(signalIndex)); } } // 清理已失效的选中状态与网格引用。 if (m_selected.IsValid() && dataSet.Resolve(m_selected) == nullptr) { m_selected = SignalRef(); } for (Lane& lane : m_lanes) { std::vector& 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(fileIndex); if (slot < m_styles.size()) { m_styles.erase(m_styles.begin() + fileIndex); } if (slot < m_collapsed.size()) { m_collapsed.erase(m_collapsed.begin() + fileIndex); } for (Lane& lane : m_lanes) { std::vector& 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; } } const SignalStyle* TabConfig::Style(const SignalRef& ref) const { if (ref.fileIndex < 0 || static_cast(ref.fileIndex) >= m_styles.size()) { return nullptr; } const std::vector& fileStyles = m_styles[static_cast(ref.fileIndex)]; if (ref.signalIndex < 0 || static_cast(ref.signalIndex) >= fileStyles.size()) { return nullptr; } return &fileStyles[static_cast(ref.signalIndex)]; } SignalStyle* TabConfig::Style(const SignalRef& ref) { return const_cast(static_cast(this)->Style(ref)); } bool TabConfig::IsVisible(const SignalRef& ref) const { const SignalStyle* style = Style(ref); return (style != nullptr) && style->visible; } void TabConfig::SetVisible(const DataSet& dataSet, const SignalRef& ref, bool visible) { SignalStyle* style = Style(ref); if (style == nullptr) { return; } if (style->visible == visible) { return; } style->visible = visible; SyncLanes(dataSet); } void TabConfig::ToggleVisible(const DataSet& dataSet, const SignalRef& ref) { const SignalStyle* style = Style(ref); if (style == nullptr) { return; } SetVisible(dataSet, ref, !style->visible); } void TabConfig::SetFileVisible(const DataSet& dataSet, int fileIndex, bool visible) { if (fileIndex < 0 || static_cast(fileIndex) >= m_styles.size()) { return; } for (SignalStyle& style : m_styles[static_cast(fileIndex)]) { style.visible = visible; } SyncLanes(dataSet); } void TabConfig::SetAllVisible(const DataSet& dataSet, bool visible) { for (std::vector& fileStyles : m_styles) { for (SignalStyle& style : fileStyles) { style.visible = visible; } } SyncLanes(dataSet); } void TabConfig::SoloSignal(const DataSet& dataSet, const SignalRef& ref) { for (std::vector& fileStyles : m_styles) { for (SignalStyle& style : fileStyles) { style.visible = false; } } if (SignalStyle* style = Style(ref); style != nullptr) { style->visible = true; } SyncLanes(dataSet); } bool TabConfig::FileHasVisible(const DataSet& dataSet, int fileIndex) const { (void)dataSet; if (fileIndex < 0 || static_cast(fileIndex) >= m_styles.size()) { return false; } for (const SignalStyle& style : m_styles[static_cast(fileIndex)]) { if (style.visible) { return true; } } return false; } bool TabConfig::FileAllVisible(const DataSet& dataSet, int fileIndex) const { (void)dataSet; if (fileIndex < 0 || static_cast(fileIndex) >= m_styles.size()) { return false; } const std::vector& fileStyles = m_styles[static_cast(fileIndex)]; if (fileStyles.empty()) { return false; } for (const SignalStyle& style : fileStyles) { if (!style.visible) { return false; } } return true; } bool TabConfig::AnyVisible() const { for (const std::vector& fileStyles : m_styles) { for (const SignalStyle& style : fileStyles) { if (style.visible) { return true; } } } return false; } int TabConfig::VisibleCount() const { int count = 0; for (const std::vector& fileStyles : m_styles) { for (const SignalStyle& style : fileStyles) { if (style.visible) { ++count; } } } 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_styles, 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& refs = m_lanes[lane].signals; if (std::find(refs.begin(), refs.end(), ref) != refs.end()) { return static_cast(lane); } } return -1; } int TabConfig::MoveSignalToLane(const DataSet& dataSet, const SignalRef& ref, int laneIndex) { if (dataSet.Resolve(ref) == nullptr) { return -1; } // 手动指定归属意味着切换到自定义划分策略。 m_laneMode = LaneMode::CUSTOM; for (Lane& lane : m_lanes) { std::vector& 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()); if (laneIndex < 0 || laneIndex >= static_cast(m_lanes.size())) { Lane lane; lane.autoRange = true; lane.signals.push_back(ref); m_lanes.push_back(std::move(lane)); } else { m_lanes[static_cast(laneIndex)].signals.push_back(ref); } 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(fileIndex) >= m_collapsed.size()) { return false; } return m_collapsed[static_cast(fileIndex)]; } void TabConfig::ToggleCollapsed(int fileIndex) { if (fileIndex < 0) { return; } if (static_cast(fileIndex) >= m_collapsed.size()) { m_collapsed.resize(static_cast(fileIndex) + 1, false); } m_collapsed[static_cast(fileIndex)] = !m_collapsed[static_cast(fileIndex)]; } void TabConfig::ComputeLaneRange(const DataSet& dataSet, int laneIndex, double& yMin, double& yMax) const { if (laneIndex < 0 || laneIndex >= static_cast(m_lanes.size())) { yMin = 0.0; yMax = 1.0; return; } const Lane& lane = m_lanes[static_cast(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::infinity(); double high = -std::numeric_limits::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::infinity(); double high = -std::numeric_limits::infinity(); for (std::size_t fileIndex = 0; fileIndex < m_styles.size(); ++fileIndex) { bool fileUsed = false; for (const SignalStyle& style : m_styles[fileIndex]) { if (style.visible) { fileUsed = true; break; } } if (!fileUsed) { continue; } const DataFile* file = dataSet.FileAt(static_cast(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