#include "SignalSet.hpp" #include "Theme.hpp" #include "Utf8Util.hpp" #include #include #include #include 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 剥离列名中的 "[n]" 序号前缀,得到显示名称。 * @param [IN] header 原始列名。 * @return 显示名称。 */ std::wstring MakeDisplayName(const std::wstring& header) { const std::wstring text = Trim(header); if (text.size() >= 3 && text[0] == L'[') { const std::size_t close = text.find(L']'); if (close != std::wstring::npos && close > 1 && (close + 1) < text.size()) { bool numeric = true; for (std::size_t i = 1; i < close; ++i) { if (!iswdigit(text[i])) { numeric = false; break; } } if (numeric) { const std::wstring rest = Trim(text.substr(close + 1)); if (!rest.empty()) { return rest; } } } } return text.empty() ? header : text; } /** * @brief 从列名末尾的括号中提取单位(仅接受形如 mA / V / deg/s 的短标记)。 * @param [IN] header 原始列名。 * @return 单位文本,无法识别时返回空字符串。 */ std::wstring ExtractUnit(const std::wstring& header) { const std::wstring text = Trim(header); 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; } } const wchar_t* DrawModeName(DrawMode mode) { switch (mode) { case DrawMode::LINE: return L"线段"; case DrawMode::SCATTER: return L"散点"; case DrawMode::LINE_SCATTER: return L"点线"; case DrawMode::STEP: return L"阶梯"; default: return L"线段"; } } SignalSet::SignalSet() = default; void SignalSet::Clear() { m_time.clear(); m_signals.clear(); m_lanes.clear(); m_timeColumnName.clear(); m_filePath.clear(); m_fileName.clear(); m_minTime = 0.0; m_maxTime = 0.0; m_timeMonotonic = true; m_selectedSignal = -1; } bool SignalSet::BuildFromTable(const CsvTable& table, const std::wstring& filePath, std::wstring& error) { Clear(); error.clear(); if (!table.HasData()) { error = L"数据表为空。"; return false; } const std::size_t columnCount = table.headers.size(); const std::size_t rowCount = table.rows.size(); // 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; } m_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)]; m_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; } m_timeColumnName = table.headers[static_cast(timeColumn)]; m_timeMonotonic = monotonic; m_filePath = filePath; const std::size_t slashPos = filePath.find_last_of(L"\\/"); m_fileName = (slashPos == std::wstring::npos) ? filePath : filePath.substr(slashPos + 1); // 2. 构建信号列。 m_signals.reserve(columnCount); for (std::size_t column = 0; column < columnCount; ++column) { if (static_cast(column) == timeColumn) { continue; } Signal signal; signal.rawHeader = table.headers[column]; signal.name = MakeDisplayName(signal.rawHeader); signal.unit = ExtractUnit(signal.rawHeader); signal.color = PickDefaultColor(m_signals.size()); signal.lineWidth = static_cast(kDefaultLineWidth); signal.mode = DrawMode::LINE; signal.visible = true; 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; m_signals.push_back(std::move(signal)); } if (m_signals.empty()) { error = L"除时间轴外未发现任何数据列。"; Clear(); return false; } // 3. 统计时间范围。 double tMin = std::numeric_limits::infinity(); double tMax = -std::numeric_limits::infinity(); for (const double value : m_time) { if (std::isfinite(value)) { tMin = std::min(tMin, value); tMax = std::max(tMax, value); } } m_minTime = tMin; m_maxTime = tMax; // 4. 默认每个信号独占一个网格,便于区分量纲差异较大的数据。 m_lanes.clear(); m_lanes.resize(m_signals.size()); for (std::size_t index = 0; index < m_signals.size(); ++index) { m_lanes[index].name = L"网格 " + std::to_wstring(index + 1); m_lanes[index].signalIndices.push_back(static_cast(index)); m_lanes[index].autoRange = true; } m_selectedSignal = m_signals.empty() ? -1 : 0; return true; } void SignalSet::SetSelectedSignal(int index) { if (index < 0 || index >= static_cast(m_signals.size())) { m_selectedSignal = -1; return; } m_selectedSignal = index; } int SignalSet::LaneOfSignal(int signalIndex) const { for (std::size_t lane = 0; lane < m_lanes.size(); ++lane) { const std::vector& indices = m_lanes[lane].signalIndices; if (std::find(indices.begin(), indices.end(), signalIndex) != indices.end()) { return static_cast(lane); } } return -1; } int SignalSet::MoveSignalToLane(int signalIndex, int laneIndex) { if (signalIndex < 0 || signalIndex >= static_cast(m_signals.size())) { return -1; } for (Lane& lane : m_lanes) { std::vector& indices = lane.signalIndices; indices.erase(std::remove(indices.begin(), indices.end(), signalIndex), indices.end()); } if (laneIndex < 0 || laneIndex >= static_cast(m_lanes.size())) { Lane lane; lane.name = L"网格 " + std::to_wstring(m_lanes.size() + 1); lane.autoRange = true; lane.signalIndices.push_back(signalIndex); m_lanes.push_back(std::move(lane)); } else { m_lanes[static_cast(laneIndex)].signalIndices.push_back(signalIndex); } PruneEmptyLanes(); return LaneOfSignal(signalIndex); } void SignalSet::MergeIntoSingleLane() { m_lanes.clear(); Lane lane; lane.name = L"网格 1"; lane.autoRange = true; lane.signalIndices.reserve(m_signals.size()); for (std::size_t index = 0; index < m_signals.size(); ++index) { lane.signalIndices.push_back(static_cast(index)); } m_lanes.push_back(std::move(lane)); } void SignalSet::SplitIntoLanes() { m_lanes.clear(); m_lanes.resize(m_signals.size()); for (std::size_t index = 0; index < m_signals.size(); ++index) { Lane& lane = m_lanes[index]; lane.name = L"网格 " + std::to_wstring(index + 1); lane.autoRange = true; lane.signalIndices.push_back(static_cast(index)); } } void SignalSet::SetAllVisible(bool visible) { for (Signal& signal : m_signals) { signal.visible = visible; } } void SignalSet::SoloSignal(int signalIndex) { for (std::size_t index = 0; index < m_signals.size(); ++index) { m_signals[index].visible = (static_cast(index) == signalIndex); } } void SignalSet::ComputeLaneRange(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 int signalIndex : lane.signalIndices) { if (signalIndex < 0 || signalIndex >= static_cast(m_signals.size())) { continue; } const Signal& signal = m_signals[static_cast(signalIndex)]; if (!signal.visible || 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; } int SignalSet::FindNearestSample(double time) const { const std::size_t count = m_time.size(); if (count == 0) { return -1; } if (m_timeMonotonic) { const std::vector::const_iterator lower = std::lower_bound(m_time.begin(), m_time.end(), time); if (lower == m_time.begin()) { return 0; } if (lower == m_time.end()) { return static_cast(count - 1); } const std::size_t rightIndex = static_cast(lower - m_time.begin()); const std::size_t leftIndex = rightIndex - 1; const double leftDistance = std::fabs(time - m_time[leftIndex]); const double rightDistance = std::fabs(m_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(m_time[index])) { continue; } const double distance = std::fabs(m_time[index] - time); if (distance < bestDistance) { bestDistance = distance; best = static_cast(index); } } return best; } bool SignalSet::SampleAt(int signalIndex, double time, bool interpolate, double& value) const { value = kMissing; if (signalIndex < 0 || signalIndex >= static_cast(m_signals.size())) { return false; } if (m_time.empty() || time < m_minTime || time > m_maxTime) { return false; } const std::vector& samples = m_signals[static_cast(signalIndex)].values; if (!m_timeMonotonic) { const int nearest = FindNearestSample(time); if (nearest < 0) { return false; } value = samples[static_cast(nearest)]; return std::isfinite(value); } const std::vector::const_iterator lower = std::lower_bound(m_time.begin(), m_time.end(), time); const std::size_t rightIndex = static_cast(lower - m_time.begin()); if (rightIndex == 0) { value = samples[0]; return std::isfinite(value); } if (rightIndex >= m_time.size()) { value = samples.back(); return std::isfinite(value); } const std::size_t leftIndex = rightIndex - 1; const double t0 = m_time[leftIndex]; const double t1 = m_time[rightIndex]; const double v0 = samples[leftIndex]; const double v1 = samples[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; } void SignalSet::PruneEmptyLanes() { if (m_lanes.size() <= 1) { return; } std::vector kept; kept.reserve(m_lanes.size()); for (Lane& lane : m_lanes) { if (!lane.signalIndices.empty()) { kept.push_back(std::move(lane)); } } if (kept.empty()) { return; } m_lanes = std::move(kept); for (std::size_t index = 0; index < m_lanes.size(); ++index) { m_lanes[index].name = L"网格 " + std::to_wstring(index + 1); } } std::uint32_t SignalSet::PickDefaultColor(std::size_t index) const { int paletteCount = 0; const std::uint32_t* palette = SignalPalette(paletteCount); if (palette == nullptr || paletteCount <= 0) { return 0xFF268BD2; } return palette[index % static_cast(paletteCount)]; } } // namespace jdi