feat(resample): 实验性功能新增「重采样」(位于「自由标记」上方)
- Lane::ResampleItem + resample/resampleItems/resamplePanel/resampleScroll, 并在 LaneCarry / SplitLane / PruneLaneSignalItems / RemapAfterSignalInsert|Removal 同步 - 面板:可滚动分组列表(信号色条目栏 + 复选框 + 展开三角),展开后 4 行: 最大/最小数据间隔(只读,勾选时一次性缓存,绘制不扫描采样)+ 重采样间隔/频率(输入) - 输入框联动:改间隔同步频率,改频率同步间隔(输入单位分别为秒 / Hz) - 「构建为新信号」:DataSet::AppendGroupAxis 追加新时间轴 + InsertDerivedSignal, 采样值先按公式/系数/绝对值/镜像/Y偏移变换再等间隔线性插值,时间轴偏移不烘值 - 新信号名 <原信号> (重采样 <自适应单位频率>);Utf8Util::FormatFrequency 为唯一实现 - 非单调轴 / 点太少 / 间隔过大 / 点数超 2000 万 ⇒ 拒绝构建并给出自绘提示 - 顺带加入 FormulaEval(公式编译/求值,供 ApplyDrawTransform 与后续 UI 使用)
This commit is contained in:
1 parent
30e9d65c30
commit
1e1c2432ae
19 files changed
+1979
-56
No files matched your search
@@ -775,6 +775,173 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::BuildResampledSignal(int groupIndex, int signalIndex, double interval)
|
||||
{
|
||||
const SignalGroup* group = m_dataSet.GroupAt(groupIndex);
|
||||
if (group == nullptr || signalIndex < 0 ||
|
||||
signalIndex >= static_cast<int>(group->signals.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!std::isfinite(interval) || interval <= 0.0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const SignalData& source = group->signals[static_cast<std::size_t>(signalIndex)];
|
||||
const TimeAxis& axis = group->AxisOf(source);
|
||||
const std::wstring baseName = source.name;
|
||||
|
||||
// 0. 快照源信号的「调整」变换(插入会移动信号存储,之后取 `source` 会悬垂)。
|
||||
SignalStyle sourceStyle;
|
||||
bool hasSourceStyle = false;
|
||||
{
|
||||
const SignalStyle* style = m_dataSet.Style(SignalRef{groupIndex, signalIndex});
|
||||
if (style != nullptr)
|
||||
{
|
||||
sourceStyle = *style;
|
||||
hasSourceStyle = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 收集「变换后」的采样点(升序)。变换与绘制一致:
|
||||
// 公式映射 → 系数 → 绝对值 → 垂直镜像 → Y 偏移(时间轴偏移不烘值)。
|
||||
const std::size_t count = std::min(axis.time.size(), source.values.size());
|
||||
std::vector<double> sourceTimes;
|
||||
std::vector<double> sourceValues;
|
||||
sourceTimes.reserve(count);
|
||||
sourceValues.reserve(count);
|
||||
bool monotonic = true;
|
||||
double previous = 0.0;
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const double time = axis.time[index];
|
||||
const double value = source.values[index];
|
||||
if (!std::isfinite(time) || !std::isfinite(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!sourceTimes.empty())
|
||||
{
|
||||
if (time < previous)
|
||||
{
|
||||
monotonic = false;
|
||||
break;
|
||||
}
|
||||
if (time == previous)
|
||||
{
|
||||
continue; // 重复时刻只保留第一个
|
||||
}
|
||||
}
|
||||
previous = time;
|
||||
sourceTimes.push_back(time);
|
||||
sourceValues.push_back(hasSourceStyle ? ApplyDrawTransform(sourceStyle, value) : value);
|
||||
}
|
||||
if (!monotonic || sourceTimes.size() < 2)
|
||||
{
|
||||
m_plot.ShowPersistentHint(!monotonic ? L"重采样失败:该信号的时间轴不是单调递增的。"
|
||||
: L"重采样失败:有效采样点不足两个。");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 生成目标时间刻度(等间隔)。
|
||||
const double t0 = sourceTimes.front();
|
||||
const double t1 = sourceTimes.back();
|
||||
const double span = t1 - t0;
|
||||
if (!(span > 0.0))
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"重采样失败:该信号的时间跨度为 0。");
|
||||
return false;
|
||||
}
|
||||
// 采样点数上限:防止用户把间隔输得太小导致内存爆炸。
|
||||
constexpr double kMaxResamplePoints = 20000000.0;
|
||||
const double stepsExact = std::floor(span / interval + 1.0e-9) + 1.0;
|
||||
if (!(stepsExact >= 2.0))
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"重采样失败:重采样间隔大于信号时间跨度。");
|
||||
return false;
|
||||
}
|
||||
if (stepsExact > kMaxResamplePoints)
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"重采样失败:目标采样点过多(超过 2000 万),请增大重采样间隔。");
|
||||
return false;
|
||||
}
|
||||
const std::size_t targetCount = static_cast<std::size_t>(stepsExact);
|
||||
|
||||
TimeAxis newAxis;
|
||||
newAxis.name = axis.name;
|
||||
newAxis.time.resize(targetCount);
|
||||
newAxis.monotonic = true;
|
||||
std::vector<double> resampled(targetCount);
|
||||
std::size_t cursor = 0;
|
||||
const std::size_t last = sourceTimes.size() - 1;
|
||||
for (std::size_t index = 0; index < targetCount; ++index)
|
||||
{
|
||||
const double time = std::min(t1, t0 + static_cast<double>(index) * interval);
|
||||
newAxis.time[index] = time;
|
||||
// 单调推进游标(O(n) 总代价)。
|
||||
while (cursor < last && sourceTimes[cursor + 1] < time)
|
||||
{
|
||||
++cursor;
|
||||
}
|
||||
if (cursor >= last)
|
||||
{
|
||||
resampled[index] = sourceValues[last];
|
||||
continue;
|
||||
}
|
||||
const double ta = sourceTimes[cursor];
|
||||
const double tb = sourceTimes[cursor + 1];
|
||||
if (time <= ta)
|
||||
{
|
||||
resampled[index] = sourceValues[cursor];
|
||||
continue;
|
||||
}
|
||||
if (tb <= ta)
|
||||
{
|
||||
resampled[index] = sourceValues[cursor];
|
||||
continue;
|
||||
}
|
||||
const double ratio = (time - ta) / (tb - ta);
|
||||
const double a = sourceValues[cursor];
|
||||
const double b = sourceValues[cursor + 1];
|
||||
resampled[index] = a + (b - a) * ratio;
|
||||
}
|
||||
newAxis.minTime = t0;
|
||||
newAxis.maxTime = newAxis.time.back();
|
||||
newAxis.finiteCount = targetCount;
|
||||
|
||||
// 3. 追加新时间轴(重采样后的采样刻度无法复用源信号的时间轴)。
|
||||
std::size_t axisIndex = 0;
|
||||
if (!m_dataSet.AppendGroupAxis(groupIndex, std::move(newAxis), axisIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 组装新信号并插入到源信号下方。
|
||||
SignalData derived;
|
||||
derived.name = baseName + L" (重采样 " + FormatFrequency(1.0 / interval) + L")";
|
||||
derived.axisIndex = axisIndex;
|
||||
derived.values = std::move(resampled);
|
||||
|
||||
int newIndex = -1;
|
||||
if (!InsertSignalBelowSource(groupIndex, signalIndex, std::move(derived), true, newIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 时间轴偏移**不烘值**:留在新信号的样式上(与其它派生信号同一规则)。
|
||||
if (hasSourceStyle && sourceStyle.offsetTime != 0.0)
|
||||
{
|
||||
if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{groupIndex, newIndex}))
|
||||
{
|
||||
derivedStyle->offsetTime = sourceStyle.offsetTime;
|
||||
}
|
||||
}
|
||||
|
||||
SyncTabsToDataSet();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::InsertSignalBelowSource(int groupIndex, int sourceIndex, SignalData&& signal,
|
||||
bool makeVisible, int& newIndex)
|
||||
{
|
||||
@@ -902,6 +1069,43 @@ namespace jdi
|
||||
|
||||
bool Application::ApplyPendingDerivedBuild()
|
||||
{
|
||||
// 「重采样」的构建流程与均值/弱峰不同(要新建时间轴),单独处理。
|
||||
{
|
||||
int laneIndex = -1;
|
||||
std::vector<PlotView::ResampleBuild> items;
|
||||
if (m_plot.TakePendingResampleBuild(laneIndex, items) && !items.empty())
|
||||
{
|
||||
// **同一文件内必须按下标降序处理**(每次插入只让插入点之后的信号下标 +1)。
|
||||
std::sort(items.begin(), items.end(),
|
||||
[](const PlotView::ResampleBuild& a, const PlotView::ResampleBuild& b)
|
||||
{
|
||||
if (a.groupIndex != b.groupIndex)
|
||||
{
|
||||
return a.groupIndex < b.groupIndex;
|
||||
}
|
||||
return a.signalIndex > b.signalIndex;
|
||||
});
|
||||
int built = 0;
|
||||
double lastInterval = 0.0;
|
||||
for (const PlotView::ResampleBuild& item : items)
|
||||
{
|
||||
if (BuildResampledSignal(item.groupIndex, item.signalIndex, item.interval))
|
||||
{
|
||||
++built;
|
||||
lastInterval = item.interval;
|
||||
}
|
||||
}
|
||||
if (built > 0)
|
||||
{
|
||||
SetStatusMessage(L"已构建 " + std::to_wstring(built) + L" 条重采样信号(频率 " +
|
||||
FormatFrequency(lastInterval > 0.0 ? 1.0 / lastInterval : 0.0) +
|
||||
L")。",
|
||||
false);
|
||||
}
|
||||
return built > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 两类「构建为新信号」请求(均值滤波 / 弱峰调整)机制完全一致,
|
||||
// 统一整理成同一份构建清单后按下标降序处理。
|
||||
std::vector<DerivedSignalRequest> requests;
|
||||
@@ -1127,6 +1331,13 @@ namespace jdi
|
||||
freeMarker.label = L"自由标记";
|
||||
freeMarker.id = PLOT_CMD_LANE_FREE_MARKER;
|
||||
freeMarker.checked = m_plot.LaneFreeMarker(laneIndex);
|
||||
|
||||
// 「重采样」位于「自由标记」**上方**(用户 2026-11-02 要求)。
|
||||
MenuItem resample;
|
||||
resample.label = L"重采样";
|
||||
resample.id = PLOT_CMD_LANE_RESAMPLE;
|
||||
resample.checked = m_plot.LaneResample(laneIndex);
|
||||
experimental.submenu.push_back(resample);
|
||||
experimental.submenu.push_back(freeMarker);
|
||||
}
|
||||
items.push_back(experimental);
|
||||
@@ -1355,6 +1566,15 @@ namespace jdi
|
||||
case MenuScope::SIGNAL_PANEL:
|
||||
{
|
||||
const bool needRelayout = m_panel.OnMenuCommand(result);
|
||||
// 「公式映射」等就地输入可能产生**非模态**警告(不使用 Windows 消息框),
|
||||
// 由绘图区底部的自绘提示浮层显示(与加载失败提示同一套)。
|
||||
{
|
||||
std::wstring hint;
|
||||
if (m_panel.TakePendingHint(hint))
|
||||
{
|
||||
m_plot.ShowPersistentHint(hint);
|
||||
}
|
||||
}
|
||||
if (needRelayout)
|
||||
{
|
||||
Relayout();
|
||||
@@ -1418,6 +1638,10 @@ namespace jdi
|
||||
// 「实验性功能 → 自由标记」:切换网格内跟随鼠标的纵向光标与固定数据点面板。
|
||||
m_plot.ToggleLaneFreeMarker(laneIndex);
|
||||
break;
|
||||
case PLOT_CMD_LANE_RESAMPLE:
|
||||
// 「实验性功能 → 重采样」:切换重采样参数面板。
|
||||
m_plot.ToggleLaneResample(laneIndex);
|
||||
break;
|
||||
case PLOT_CMD_LANE_SPLIT:
|
||||
// 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。
|
||||
m_plot.SplitLane(laneIndex);
|
||||
|
||||
Reference in new issue
Block a user