实验性功能新增「均值滤波」+ 构建为新信号
- 「实验性功能」二级菜单新增带勾选的「均值滤波」:开启后为本网格每条信号叠加绘制
一条 N 点滑动平均曲线,算法与「边沿计数(中心轴)」的中心平均值曲线**完全一致**
(同一实现 ComputeMovingAverage),配色同样由信号色派生为高对比度颜色、透明度一致。
- 新增均值滤波浮动面板:标题栏可拖拽、✕ 关闭,面板内列出被处理的信号名、
「均值点数 N」内联编辑行,以及底部的「构建为新信号」按钮(悬停高亮 + 手型光标)。
- 「构建为新信号」:把滤波后的曲线作为新信号插入,命名为「<原信号名称> (N点均值)」,
归属到原信号所属组的原信号位置**下方**,成为独立的新信号;其所在网格也移动到
原信号网格的下方。合并网格时一次按钮为该网格内每条可见信号各构建一条。
为此新增 DataSet::InsertDerivedSignal / TabConfig::RemapAfterSignalInsert /
TabConfig::MoveLaneOfSignalBelow / TabConfig::RenameLanes(插入位置之后的信号引用
与可见性列同步重映射)。
- 光标交互与「边沿计数(中心轴)」同构:光标处标注均值滤波曲线的取值
与双光标增量,文案为「值(均值滤波)」/「值(+增量 均值滤波)」;
两条派生曲线的取值胶囊已抽成同一段绘制逻辑。
- 浮动面板种类由 bool centerAxis 改为 EdgePanelKind{COUNT,CENTER,MEAN},
布局/宽度/命中测试/绘制统一走同一套几何。
- 以 VERY_BIG_DATA.CSV 验证:曲线预览、N 编辑、双光标取值标注、
单信号与合并网格两种情况下的新信号命名与插入位置均正确。
This commit is contained in:
1 parent
8ae8d7af4f
commit
f5028b1e18
6 files changed
+885
-210
No files matched your search
+221
-1
@@ -126,6 +126,11 @@ namespace jdi
|
||||
bool edgeCenterPanelOpen = true;
|
||||
double edgeCenterPanelX = -1.0;
|
||||
double edgeCenterPanelY = -1.0;
|
||||
bool meanFilter = false;
|
||||
int meanFilterRadius = 5;
|
||||
bool meanFilterPanelOpen = true;
|
||||
double meanFilterPanelX = -1.0;
|
||||
double meanFilterPanelY = -1.0;
|
||||
};
|
||||
|
||||
// 先记下每个信号原来所在网格的网格自身状态:
|
||||
@@ -146,6 +151,11 @@ namespace jdi
|
||||
carry.edgeCenterPanelOpen = lane.edgeCenterPanelOpen;
|
||||
carry.edgeCenterPanelX = lane.edgeCenterPanelX;
|
||||
carry.edgeCenterPanelY = lane.edgeCenterPanelY;
|
||||
carry.meanFilter = lane.meanFilter;
|
||||
carry.meanFilterRadius = lane.meanFilterRadius;
|
||||
carry.meanFilterPanelOpen = lane.meanFilterPanelOpen;
|
||||
carry.meanFilterPanelX = lane.meanFilterPanelX;
|
||||
carry.meanFilterPanelY = lane.meanFilterPanelY;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
previousCarry.emplace_back(ref, carry);
|
||||
@@ -177,6 +187,11 @@ namespace jdi
|
||||
lane.edgeCenterPanelOpen = carry.edgeCenterPanelOpen;
|
||||
lane.edgeCenterPanelX = carry.edgeCenterPanelX;
|
||||
lane.edgeCenterPanelY = carry.edgeCenterPanelY;
|
||||
lane.meanFilter = carry.meanFilter;
|
||||
lane.meanFilterRadius = carry.meanFilterRadius;
|
||||
lane.meanFilterPanelOpen = carry.meanFilterPanelOpen;
|
||||
lane.meanFilterPanelX = carry.meanFilterPanelX;
|
||||
lane.meanFilterPanelY = carry.meanFilterPanelY;
|
||||
};
|
||||
|
||||
// 收集全部可见信号引用。
|
||||
@@ -622,6 +637,62 @@ namespace jdi
|
||||
return AddPreparedFile(std::move(file), fileIndex);
|
||||
}
|
||||
|
||||
bool DataSet::InsertDerivedSignal(int fileIndex, int afterSignalIndex, SignalData&& signal,
|
||||
int& newSignalIndex)
|
||||
{
|
||||
newSignalIndex = -1;
|
||||
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DataFile& file = m_files[static_cast<std::size_t>(fileIndex)];
|
||||
if (afterSignalIndex < -1 || afterSignalIndex >= static_cast<int>(file.signals.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. 采样值统计与粗粒度包络层:与正常加载路径使用同一套统计逻辑,
|
||||
// 否则派生信号在大文件下会退化成逐点扫描(包络构建变慢)。
|
||||
const SignalStats stats = ComputeStats(signal.values, signal.coarseMin, signal.coarseMax);
|
||||
signal.coarseStride = kCoarseStride;
|
||||
signal.minValue = stats.minValue;
|
||||
signal.maxValue = stats.maxValue;
|
||||
signal.finiteCount = stats.finiteCount;
|
||||
|
||||
// 2. 显示外观按「全局信号序号」取调色板颜色(与普通新信号完全一致的规则)。
|
||||
const int insertPos = afterSignalIndex + 1;
|
||||
{
|
||||
// 新信号的全局序号 = 本文件之前的信号总数 + 它在文件内的下标。
|
||||
std::size_t globalIndex = static_cast<std::size_t>(insertPos);
|
||||
for (int index = 0; index < fileIndex; ++index)
|
||||
{
|
||||
globalIndex += m_files[static_cast<std::size_t>(index)].signals.size();
|
||||
}
|
||||
|
||||
int paletteCount = 0;
|
||||
const std::uint32_t* palette = SignalPalette(paletteCount);
|
||||
if (palette != nullptr && paletteCount > 0)
|
||||
{
|
||||
const std::size_t count = static_cast<std::size_t>(paletteCount);
|
||||
const std::size_t slot = globalIndex % count;
|
||||
signal.style.color = palette[slot];
|
||||
signal.style.customColor = palette[(slot + 1) % count];
|
||||
}
|
||||
}
|
||||
|
||||
file.signals.insert(file.signals.begin() + insertPos, std::move(signal));
|
||||
|
||||
// 3. 全局选中引用:同一文件内位于插入点之后的信号下标整体后移。
|
||||
if (m_selected.fileIndex == fileIndex && m_selected.signalIndex >= insertPos)
|
||||
{
|
||||
++m_selected.signalIndex;
|
||||
}
|
||||
|
||||
newSignalIndex = insertPos;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DataSet::RemoveFile(int fileIndex)
|
||||
{
|
||||
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
|
||||
@@ -796,6 +867,86 @@ namespace jdi
|
||||
|
||||
// ============================ 采样辅助 ============================
|
||||
|
||||
void ComputeMovingAverage(const std::vector<double>& values, int radius, std::vector<float>& out)
|
||||
{
|
||||
const int window = std::max(0, radius);
|
||||
const std::size_t count = values.size();
|
||||
out.assign(count, std::numeric_limits<float>::quiet_NaN());
|
||||
if (count == 0 || window == 0)
|
||||
{
|
||||
// 半径 0 表示不平滑:原样复制(中心点无效处仍为 NaN)。
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
if (std::isfinite(values[index]))
|
||||
{
|
||||
out[index] = static_cast<float>(values[index]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 滑动窗口求和:窗口 = [max(0,i-N), min(count-1,i+N)],
|
||||
// 每前进一步只增减边界处的两个采样点,整体 O(n),不额外占用前缀和内存。
|
||||
double sum = 0.0;
|
||||
std::size_t finiteCount = 0;
|
||||
std::size_t windowLow = 0;
|
||||
std::size_t windowHigh = 0; // 闭区间右端。
|
||||
|
||||
const auto addValue = [&sum, &finiteCount](double value)
|
||||
{
|
||||
if (std::isfinite(value))
|
||||
{
|
||||
sum += value;
|
||||
++finiteCount;
|
||||
}
|
||||
};
|
||||
const auto removeValue = [&sum, &finiteCount](double value)
|
||||
{
|
||||
if (std::isfinite(value))
|
||||
{
|
||||
sum -= value;
|
||||
--finiteCount;
|
||||
}
|
||||
};
|
||||
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const std::size_t low = (index > static_cast<std::size_t>(window))
|
||||
? (index - static_cast<std::size_t>(window))
|
||||
: 0;
|
||||
const std::size_t high = std::min(count - 1, index + static_cast<std::size_t>(window));
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
windowLow = low;
|
||||
windowHigh = high;
|
||||
for (std::size_t i = windowLow; i <= windowHigh; ++i)
|
||||
{
|
||||
addValue(values[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (windowLow < low)
|
||||
{
|
||||
removeValue(values[windowLow]);
|
||||
++windowLow;
|
||||
}
|
||||
while (windowHigh < high)
|
||||
{
|
||||
++windowHigh;
|
||||
addValue(values[windowHigh]);
|
||||
}
|
||||
}
|
||||
|
||||
// 中心点本身无效(NaN)时该处不产生有效结果。
|
||||
if (finiteCount > 0 && std::isfinite(values[index]))
|
||||
{
|
||||
out[index] = static_cast<float>(sum / static_cast<double>(finiteCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int FindNearestSample(const DataFile& file, double time)
|
||||
{
|
||||
const std::size_t count = file.time.size();
|
||||
@@ -1247,6 +1398,11 @@ namespace jdi
|
||||
lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen;
|
||||
lane.edgeCenterPanelX = source.edgeCenterPanelX;
|
||||
lane.edgeCenterPanelY = source.edgeCenterPanelY;
|
||||
lane.meanFilter = source.meanFilter;
|
||||
lane.meanFilterRadius = source.meanFilterRadius;
|
||||
lane.meanFilterPanelOpen = source.meanFilterPanelOpen;
|
||||
lane.meanFilterPanelX = source.meanFilterPanelX;
|
||||
lane.meanFilterPanelY = source.meanFilterPanelY;
|
||||
lane.signals.push_back(ref);
|
||||
pieces.push_back(std::move(lane));
|
||||
}
|
||||
@@ -1263,8 +1419,72 @@ namespace jdi
|
||||
return laneIndex;
|
||||
}
|
||||
|
||||
void TabConfig::RemapAfterSignalInsert(int fileIndex, int insertPos)
|
||||
{
|
||||
if (fileIndex < 0 || insertPos < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 可见性表:在插入点补一列(新信号默认不可见,由调用方按需勾选)。
|
||||
const std::size_t slot = static_cast<std::size_t>(fileIndex);
|
||||
if (slot < m_visible.size() && static_cast<std::size_t>(insertPos) <= m_visible[slot].size())
|
||||
{
|
||||
m_visible[slot].insert(m_visible[slot].begin() + insertPos, false);
|
||||
}
|
||||
|
||||
// 网格内的信号引用:同一文件中位于插入点之后的引用下标整体后移。
|
||||
for (Lane& lane : m_lanes)
|
||||
{
|
||||
for (SignalRef& ref : lane.signals)
|
||||
{
|
||||
if (ref.fileIndex == fileIndex && ref.signalIndex >= insertPos)
|
||||
{
|
||||
++ref.signalIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool TabConfig::MoveLaneOfSignalBelow(const SignalRef& moved, const SignalRef& anchor)
|
||||
{
|
||||
const int from = LaneOfSignal(moved);
|
||||
if (from < 0 || LaneOfSignal(anchor) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Lane lane = std::move(m_lanes[static_cast<std::size_t>(from)]);
|
||||
m_lanes.erase(m_lanes.begin() + from);
|
||||
|
||||
// erase 之后锚点网格的下标可能变化,必须用信号引用重新定位。
|
||||
const int target = LaneOfSignal(anchor);
|
||||
if (target < 0)
|
||||
{
|
||||
m_lanes.push_back(std::move(lane));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_lanes.insert(m_lanes.begin() + target + 1, std::move(lane));
|
||||
}
|
||||
|
||||
// 手动排布 → 切换到自定义划分策略,避免后续重建网格时顺序被还原。
|
||||
m_laneMode = LaneMode::CUSTOM;
|
||||
RenameLanes();
|
||||
return true;
|
||||
}
|
||||
|
||||
void TabConfig::RenameLanes()
|
||||
{
|
||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||
{
|
||||
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
bool TabConfig::IsCollapsed(int fileIndex) const
|
||||
{ if (fileIndex < 0 || static_cast<std::size_t>(fileIndex) >= m_collapsed.size())
|
||||
{
|
||||
if (fileIndex < 0 || static_cast<std::size_t>(fileIndex) >= m_collapsed.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user