geopro/src/app/panels/chart/RawDataChartView.cpp

991 lines
47 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "panels/chart/RawDataChartView.hpp"
#include "ColorScaleConfigDialog.hpp"
#include "panels/chart/ChartTheme.hpp"
#include "panels/chart/ColorBarWidget.hpp"
#include "panels/chart/GridWizardDialog.hpp"
#include "panels/chart/InversionFormDialog.hpp"
#include "panels/chart/SaveAsDialog.hpp"
#include "panels/chart/ScatterDataOps.hpp"
#include "panels/chart/ScatterFilterDialog.hpp"
#include "panels/chart/ScatterHoverTip.hpp"
#include "panels/chart/ScatterMarqueePicker.hpp"
#include "panels/chart/ScatterPlotItem.hpp"
#include <utility>
#include "repo/IDatasetCommandRepository.hpp"
#include <QByteArray>
#include <QComboBox>
#include "EmptyAwareComboBox.hpp"
#include <QCursor>
#include <QEvent>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QIcon>
#include <QJsonArray>
#include <QJsonObject>
#include <QLabel>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPen>
#include <QPixmap>
#include <QPointer>
#include <QPushButton>
#include <QSaveFile>
#include <QSignalBlocker>
#include <QToolButton>
#include <QToolTip>
#include <QVBoxLayout>
#include <qwt_scale_map.h>
#include <qwt_plot.h>
#include <qwt_plot_canvas.h>
#include <qwt_plot_marker.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_rescaler.h>
#include <algorithm>
#include <cmath>
#include <limits>
#include "panels/chart/LivePanner.hpp"
#include "Theme.hpp"
#include "ToastOverlay.hpp" // showToast统一成功轻提示规范 §7.7
namespace geopro::app {
RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) {
auto* lay = new QVBoxLayout(this);
lay->setContentsMargins(0, 0, 0, 0);
lay->setSpacing(0);
rootLay_ = lay;
// ---- 工具条(默认 = 反演原数据measurement 到来时 buildMeasurementToolbar 替换)----
auto* toolbar = new QWidget(this);
toolbar_ = toolbar;
auto* tbLay = new QHBoxLayout(toolbar);
tbLay->setContentsMargins(4, 4, 4, 4);
tbLay->setSpacing(4);
auto* btnGrid = new QToolButton(toolbar);
btnGrid->setText(QStringLiteral("网格"));
auto* btnColorScale = new QToolButton(toolbar);
btnColorScale->setText(QStringLiteral("色阶配置"));
auto* lblCurrentChart = new QLabel(QStringLiteral("当前图形:"), toolbar);
chartTypeCombo_ = new EmptyAwareComboBox(toolbar);
chartTypeCombo_->addItem(QStringLiteral("散点图"));
auto* btnSaveAs = new QToolButton(toolbar);
btnSaveAs->setText(QStringLiteral("另存为"));
tbLay->addWidget(btnGrid);
tbLay->addWidget(btnColorScale);
tbLay->addWidget(lblCurrentChart);
tbLay->addWidget(chartTypeCombo_);
// 原版 .right-buttons margin-left:auto另存为 右对齐。
tbLay->addStretch();
tbLay->addWidget(btnSaveAs);
// 反演原数据默认工具条交互O1 网格 / O2 色阶配置 / O3 另存为)。
connect(btnGrid, &QToolButton::clicked, this, [this, btnGrid]() { openGridWizard(btnGrid); });
connect(btnColorScale, &QToolButton::clicked, this,
[this, btnColorScale]() { openInversionColorScale(btnColorScale); });
connect(btnSaveAs, &QToolButton::clicked, this,
[this, btnSaveAs]() { openInversionSaveAs(btnSaveAs); });
lay->addWidget(toolbar);
// ---- QwtPlotstretch 填满剩余空间)----
plot_ = new QwtPlot(this);
plot_->setObjectName(QStringLiteral("rawPlotArea"));
// x 轴顶部,关闭底部 x 轴
plot_->enableAxis(QwtPlot::xTop, true);
plot_->enableAxis(QwtPlot::xBottom, false);
plot_->enableAxis(QwtPlot::yLeft, true);
// 底色/轴字/网格/零线配色由 applyChartPlotTheme 按主题统一设置(见 ctor 末尾 + 主题热切换)。
// 浅色与原版 web 一致(白底深灰字);暗色改深色画布避免刺眼白底。
// 横纵网格线(浅灰,暗色下由 applyChartPlotTheme 重着色)。
auto* grid = new QwtPlotGrid();
grid->setMajorPen(QColor(225, 225, 225), 1.0, Qt::SolidLine);
grid->setMinorPen(QColor(240, 240, 240), 1.0, Qt::DotLine);
grid->enableXMin(false);
grid->enableYMin(false);
grid->setXAxis(QwtPlot::xTop);
grid->setYAxis(QwtPlot::yLeft);
grid->attach(plot_);
// 过原点零线(对齐原版 zerolinex=0 竖线 + y=0 横线 → "四象限"观感)。
auto* zeroX = new QwtPlotMarker();
zeroX->setLineStyle(QwtPlotMarker::VLine);
zeroX->setXValue(0.0);
zeroX->setLinePen(QColor(180, 180, 180), 1.0);
zeroX->setXAxis(QwtPlot::xTop);
zeroX->attach(plot_);
auto* zeroY = new QwtPlotMarker();
zeroY->setLineStyle(QwtPlotMarker::HLine);
zeroY->setYValue(0.0);
zeroY->setLinePen(QColor(180, 180, 180), 1.0);
zeroY->attach(plot_);
// 交互LivePanner 统一处理左键实时平移 + 滚轮缩放(并消费滚轮事件,不冒泡触发滚动条)。
new LivePanner(plot_, QwtPlot::xTop, QwtPlot::yLeft, this);
// 散点 hover 提示X/Y/值)。后装(晚于 LivePanner→ 事件链中先收到 MouseMove
// 无按键时弹提示且不消费;有按键(拖动)跳过,交给 LivePanner。数据地址稳定装配期绑定一次。
hoverTip_ = new ScatterHoverTip(plot_, QwtPlot::xTop, QwtPlot::yLeft, this);
hoverTip_->setField(&data_.scatter);
// M14 框选拾取器(最后装 → 事件链最先收到active 时优先消费拖拽,禁用平移)。默认关闭。
marquee_ = new ScatterMarqueePicker(plot_, QwtPlot::xTop, QwtPlot::yLeft, this);
marquee_->setField(&data_.scatter);
marquee_->setOnSelected([this](const std::vector<int>& idx) { onMarqueeSelected(idx); });
// 允许随停靠面板自由收缩(不强制最小宽度)。
plot_->setMinimumSize(0, 0);
// 锁定 x:y 真实比尺用户选定1 数据单位 x = 1 数据单位 y像素
// 参考轴 xTop、Expanding 策略——resize/缩放时维持等比,剖面呈真实"宽扁"形状。
rescaler_ = new QwtPlotRescaler(plot_->canvas(), QwtPlot::xTop, QwtPlotRescaler::Expanding);
rescaler_->setAspectRatio(1.0);
rescaler_->setEnabled(true);
// ---- 图表行plotstretch+ 右侧竖向色阶条measurement 用,默认隐藏)----
auto* plotRow = new QWidget(this);
auto* plotRowLay = new QHBoxLayout(plotRow);
plotRowLay->setContentsMargins(0, 0, 0, 0);
plotRowLay->setSpacing(0);
plotRowLay->addWidget(plot_, 1);
colorBarV_ = new ColorBarWidget(plotRow, ColorBarWidget::Orientation::Vertical);
colorBarV_->setObjectName(QStringLiteral("rawColorScaleBarV"));
colorBarV_->setVisible(false); // 默认(反演原数据)用底部横条
plotRowLay->addWidget(colorBarV_);
lay->addWidget(plotRow, 1);
// ---- 底部独立色阶条(固定高 36px反演原数据用----
colorBar_ = new ColorBarWidget(this);
colorBar_->setObjectName(QStringLiteral("rawColorScaleBar"));
lay->addWidget(colorBar_);
// 主题配色:当前主题套一次 + 监听切换热更新(暗色给深底,浅色保持白底=原版)。
applyChartPlotTheme(plot_);
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, plot_,
[this]() { applyChartPlotTheme(plot_); });
}
RawDataChartView::~RawDataChartView() {
// colorSvc_ 非 QObject、无 parent需手动释放plot_ 的 item 由 QwtPlot autoDelete 处理)。
delete colorSvc_;
}
QWidget* RawDataChartView::plotArea() const {
return plot_;
}
namespace {
// 把一组 FieldOption 填进下拉,并按 defaultCode或 defaultName 回退)选中默认项。
void fillCombo(QComboBox* combo, const std::vector<geopro::core::FieldOption>& opts,
const QString& defaultCode, const QString& defaultName) {
for (const auto& o : opts) {
// 用户可见名为 nameuserData 存 fieldCode重绘/识别用)。
combo->addItem(o.name, o.code);
}
// 默认选中:优先匹配 fieldCode否则匹配 namemethod 的 code 全为 null用 name
int idx = defaultCode.isEmpty() ? -1 : combo->findData(defaultCode);
if (idx < 0 && !defaultName.isEmpty()) idx = combo->findText(defaultName);
if (idx >= 0) combo->setCurrentIndex(idx);
}
// ── 工具条占位图标线性风格2× 超采样 → HiDPI 清晰)────────────────────
// 原版用 Arco line iconsviewBox 0 0 48 48stroke-width 4stroke=currentColor
// 这里以 QPainter 画细线风格,逻辑边长 logical px按钮 setIconSize 用同值),
// 像素 2× 渲染并 setDevicePixelRatio(2) 保证缩放下不糊。
constexpr int kToolIconPx = 16; // 逻辑图标边长(与 setIconSize 对齐)
constexpr qreal kToolIconScale = 2.0; // 超采样倍率HiDPI 清晰)
// measurement 工具条下拉固定宽度(对照原版 datasetTool.vue 各 a-select 的 width
constexpr int kComboW_X = 120; // X 下拉 width:120px
constexpr int kComboW_Y = 160; // Y 下拉 width:160px
constexpr int kComboW_V = 160; // V 值下拉 width:160px
constexpr int kComboW_ValueType = 120; // 值类型下拉 width:120px
QPixmap makeToolIconCanvas(QPainter& p) {
// 调用方在 [0,kToolIconPx] 逻辑坐标系下作画;返回前缩放 + 设 dpr。
const int dim = qRound(kToolIconPx * kToolIconScale);
QPixmap pm(dim, dim);
pm.fill(Qt::transparent);
p.begin(&pm);
p.setRenderHint(QPainter::Antialiasing, true);
p.setRenderHint(QPainter::SmoothPixmapTransform, true);
p.scale(kToolIconScale, kToolIconScale); // 之后用逻辑 px 坐标
return pm;
}
// 信息图标:细描边圆 + 内部 "i"(圆点 + 竖线),品牌蓝(两主题一致,对应原版小蓝圈-i
QIcon makeInfoIcon() {
const QColor accent = tokenColor("accent/primary");
QPainter p;
QPixmap pm = makeToolIconCanvas(p);
const qreal s = kToolIconPx;
QPen pen(accent, 1.4);
pen.setCapStyle(Qt::RoundCap);
p.setPen(pen);
p.setBrush(Qt::NoBrush);
// 外圈(留 1.5px 边距,避免描边被裁)。
const qreal m = 1.5;
p.drawEllipse(QRectF(m, m, s - 2 * m, s - 2 * m));
// "i":上点 + 下竖线(居中)。
const qreal cx = s / 2.0;
p.setBrush(accent);
p.setPen(Qt::NoPen);
p.drawEllipse(QPointF(cx, s * 0.34), 0.9, 0.9); // 点
QPen stem(accent, 1.4);
stem.setCapStyle(Qt::RoundCap);
p.setPen(stem);
p.drawLine(QPointF(cx, s * 0.46), QPointF(cx, s * 0.70)); // 竖
p.end();
pm.setDevicePixelRatio(kToolIconScale);
return QIcon(pm);
}
// 框选图标:虚线方框(选区)+ 右下角小箭头光标。描边随主题(次要文本色,两主题可见)。
QIcon makeMarqueeIcon() {
const QColor stroke = tokenColor("text/secondary");
QPainter p;
QPixmap pm = makeToolIconCanvas(p);
const qreal s = kToolIconPx;
// 虚线选区框(左上偏移,给右下角箭头让位)。
QPen dash(stroke, 1.3);
dash.setCapStyle(Qt::FlatCap);
dash.setJoinStyle(Qt::MiterJoin);
QVector<qreal> pattern{2.0, 1.6};
dash.setDashPattern(pattern);
p.setPen(dash);
p.setBrush(Qt::NoBrush);
p.drawRect(QRectF(2.0, 2.0, s - 6.0, s - 6.0));
// 右下角小箭头光标(实线填充,指向右下)。
QPainterPath cur;
const qreal ax = s - 5.0, ay = s - 5.0;
cur.moveTo(ax, ay);
cur.lineTo(ax + 4.6, ay + 1.8);
cur.lineTo(ax + 1.9, ay + 2.6);
cur.lineTo(ax + 1.1, ay + 4.9);
cur.closeSubpath();
p.setPen(Qt::NoPen);
p.setBrush(stroke);
p.drawPath(cur);
p.end();
pm.setDevicePixelRatio(kToolIconScale);
return QIcon(pm);
}
// 把占位 QToolButton 配成图标按钮:清文字、设图标 + 固定尺寸(与工具条其它按钮一致高度)。
void styleToolIconButton(QToolButton* btn, const QIcon& icon) {
btn->setText(QString());
btn->setIcon(icon);
btn->setIconSize(QSize(kToolIconPx, kToolIconPx));
btn->setAutoRaise(true);
btn->setFixedSize(QSize(28, 28)); // 与下拉/按钮行高协调;不再过小或锯齿
btn->setCursor(Qt::PointingHandCursor);
}
// core::Rgba → colorBar 颜色串(与 ColorScaleConfigDialog::rgbaToCss 同格式:不透明 #RRGGBB
// 半透明 rgba(r,g,b,a∈0..1)),与后端 colorBar 互通。
QString rgbaToColorBarCss(const geopro::core::Rgba& c) {
if (c.a >= 255)
return QStringLiteral("#%1%2%3")
.arg(c.r, 2, 16, QLatin1Char('0'))
.arg(c.g, 2, 16, QLatin1Char('0'))
.arg(c.b, 2, 16, QLatin1Char('0'))
.toUpper();
return QStringLiteral("rgba(%1, %2, %3, %4)")
.arg(c.r)
.arg(c.g)
.arg(c.b)
.arg(QString::number(c.a / 255.0, 'g', 3));
}
// 组装色阶 propertiescolorBar + lineConfig + labelConfig与原版散点路径
// newLvlColorLevel 一致battery/scatters 仅发这三块,不含 lvlSchemeType 等等值面专属字段)。
QJsonObject buildColorScaleProperties(const geopro::core::ColorScale& scale,
const ContourLineConfig& lineCfg) {
QJsonArray colorBar;
for (const auto& [value, color] : scale.stops())
colorBar.append(QJsonArray{QString::number(value, 'f', 2), rgbaToColorBarCss(color)});
QJsonObject lineConfig{
{QStringLiteral("showLines"), lineCfg.lineShow},
{QStringLiteral("color"), rgbaToColorBarCss(lineCfg.lineColor)},
{QStringLiteral("lineType"),
lineCfg.dashed ? QStringLiteral("dashed") : QStringLiteral("solid")}};
QJsonObject labelConfig{{QStringLiteral("showLabels"), lineCfg.labelShow},
{QStringLiteral("color"), rgbaToColorBarCss(lineCfg.labelColor)}};
return QJsonObject{{QStringLiteral("colorBar"), colorBar},
{QStringLiteral("lineConfig"), lineConfig},
{QStringLiteral("labelConfig"), labelConfig}};
}
} // namespace
void RawDataChartView::showNotImplemented(QWidget* anchor) {
// 轻提示:占位按钮/下拉点击 → 暂未实现(不阻塞,不弹窗)。
const QPoint pos = anchor ? anchor->mapToGlobal(QPoint(0, anchor->height()))
: QCursor::pos();
QToolTip::showText(pos, QStringLiteral("暂未实现"), anchor);
}
void RawDataChartView::setCommandRepo(geopro::data::IDatasetCommandRepository* repo,
std::function<QString()> dsIdGetter,
std::function<QString()> projectIdGetter) {
cmdRepo_ = repo;
dsIdGetter_ = std::move(dsIdGetter);
projectIdGetter_ = std::move(projectIdGetter);
}
void RawDataChartView::setColorTemplateRepo(geopro::data::IColorTemplateRepository* repo) {
colorTplRepo_ = repo;
}
void RawDataChartView::openInversionDialog(bool apparentResistivity, QWidget* anchor) {
// 无仓储/无 dsId 取值回调 → 退化占位(与未注入时一致)。
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) {
showNotImplemented(anchor);
return;
}
const auto mode = apparentResistivity ? InversionFormDialog::Mode::ApparentResistivity
: InversionFormDialog::Mode::Inversion;
InversionFormDialog dlg(mode, cmdRepo_, dsId, projectId, this);
dlg.exec(); // 提交成功/失败由对话框内部反馈;本视图无需后续刷新(原版亦仅提示)。
}
void RawDataChartView::openGridWizard(QWidget* anchor) {
// O1网格化向导与网格视图 I1 共用 GridWizardDialog。成功后散点视图无法渲染网格
// 故仅提示成功(用户切到网格页签查看,与原版「生成新网格数据后刷新」语义一致)。
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(anchor); return; }
GridWizardDialog dlg(cmdRepo_, dsId, this);
if (dlg.exec() == QDialog::Accepted)
QMessageBox::information(this, QStringLiteral("网格化"), QStringLiteral("网格化成功!"));
}
void RawDataChartView::openInversionColorScale(QWidget* anchor) {
// O2反演原数据散点色阶type1businessCode 空串,对照原版 originPage
if (data_.scale.empty()) { showNotImplemented(anchor); return; }
double vMin = std::numeric_limits<double>::max();
double vMax = std::numeric_limits<double>::lowest();
for (double v : data_.scatter.v) {
if (!std::isfinite(v)) continue;
if (v < vMin) vMin = v;
if (v > vMax) vMax = v;
}
if (vMin > vMax) { vMin = 0.0; vMax = 1.0; }
std::vector<double> samples = data_.scatter.v;
// 接通色阶模板库:注入仓储 + 当前 projectId + 载荷 templateId另存为/打开/覆盖 可用)。
ColorScaleConfigDialog dlg(data_.scale, vMin, vMax, std::move(samples), {}, colorTplRepo_,
projectIdGetter_ ? projectIdGetter_() : QString(), data_.templateId,
this);
if (dlg.exec() != QDialog::Accepted) return;
// 本地重建上色重绘。
data_.scale = dlg.colorScale();
delete colorSvc_;
colorSvc_ = new ColorMapService(data_.scale);
redrawScatter();
colorBar_->setColorScale(data_.scale);
showToast(this, QStringLiteral("色阶应用成功")); // M8 成功提示(对照原版 Message.success
// 持久化businessCode 空,对照原版 originPage newLvlColorLevel businessCode:'')。
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) return;
QJsonObject body{
{QStringLiteral("dsObjectId"), dsId},
{QStringLiteral("templateId"), data_.templateId}, // 读取到的色阶模板 id对照原版可空
{QStringLiteral("businessCode"), QString()},
{QStringLiteral("projectId"), projectId},
{QStringLiteral("properties"), buildColorScaleProperties(data_.scale, dlg.lineConfig())},
};
QPointer<RawDataChartView> self(this);
cmdRepo_->saveColorGradation(body, [self](bool ok, QString msg) {
if (!self || ok) return;
QMessageBox::warning(self, QStringLiteral("色阶配置"),
msg.isEmpty() ? QStringLiteral("色阶保存失败") : msg);
});
}
void RawDataChartView::openInversionSaveAs(QWidget* anchor) {
// O3另存为复用 SaveAsDialog::Inversion → saveInversionAsData
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(anchor); return; }
SaveAsDialog dlg(SaveAsDialog::Mode::Inversion, cmdRepo_, dsId, this);
dlg.exec();
}
QString RawDataChartView::currentVFieldCode() const {
if (vCombo_ && vCombo_->currentIndex() >= 0) {
const QString code = vCombo_->currentData().toString();
if (!code.isEmpty()) return code;
}
return QStringLiteral("R0"); // 默认视电阻率(与初始加载一致)
}
void RawDataChartView::redrawScatter() {
// 用当前 data_.scatter + colorSvc_ 重绘M7/M8 本地变换/色阶变更后复用)。
if (!scatterItem_ || !colorSvc_) return;
// 数据范围跟随当前 v 有限值 min/maxcauto与初始上色一致
double vMin = std::numeric_limits<double>::max();
double vMax = std::numeric_limits<double>::lowest();
for (double v : data_.scatter.v) {
if (!std::isfinite(v)) continue;
if (v < vMin) vMin = v;
if (v > vMax) vMax = v;
}
if (vMin <= vMax) colorSvc_->setDataRange(vMin, vMax);
scatterItem_->setData(data_.scatter, colorSvc_);
if (hoverTip_) hoverTip_->setField(&data_.scatter);
plot_->replot();
}
void RawDataChartView::onShowHide(bool hide) {
// popconfirm 确认(原版 a-popconfirm复刻确认文案。
const QString text = hide ? QStringLiteral("该操作将会把已选择的散点进行隐藏?")
: QStringLiteral("该操作将会显示所有已经隐藏的散点?");
const auto ans = QMessageBox::question(this, QStringLiteral("提示"), text,
QMessageBox::Ok | QMessageBox::Cancel);
if (ans != QMessageBox::Ok) return;
// 选区联动M14↔M1隐藏且有选区 → 只对选中点(原版 getSelectedPointIds
// 其余(隐藏无选区 / 显示)维持全部(原版显示恒为全部隐藏点)。
const bool selective = hide && scatterItem_ && scatterItem_->hasSelection();
// 本地切换可见性。selective逐点改 displayStatus仅选中点隐藏否则整体显隐全部方块。
auto localToggle = [this, hide, selective]() {
if (!scatterItem_) return;
if (selective) {
scatterItem_->setData(data_.scatter, colorSvc_); // 重读 displayStatus 逐点生效
scatterItem_->clearSelection();
} else {
for (int& s : data_.scatter.displayStatus) s = hide ? 1 : 0;
scatterItem_->setScatterVisible(!hide);
if (!hide) scatterItem_->setData(data_.scatter, colorSvc_); // 显示全部:清逐点隐藏
}
plot_->replot();
};
// 收集要持久化的点 idselective → 选中点 id否则隐藏取可见点 / 显示取隐藏点。
QJsonArray ids;
if (selective) {
for (const QString& id : scatterItem_->getSelectedIds()) ids.append(id);
// 先把选中点的 displayStatus 标为隐藏(本地,供 localToggle 重读生效)。
const auto sel = scatterItem_->getSelectedIds();
for (int i = 0; i < static_cast<int>(data_.scatter.id.size()); ++i) {
const QString id = QString::fromStdString(data_.scatter.id[i]);
if (!id.isEmpty() && sel.end() != std::find(sel.begin(), sel.end(), id))
data_.scatter.displayStatus[i] = 1;
}
} else {
ids = collectScatterIds(data_.scatter, hide);
}
// 无仓储/无 dsId → 仅本地切换(退化,不持久化)。
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { localToggle(); return; }
const int status = hide ? 1 : 0;
QPointer<RawDataChartView> self(this);
cmdRepo_->saveDisplayStatus(dsId, ids, status, [self, hide, selective, localToggle](bool ok, QString msg) {
if (!self) return;
if (!ok) {
QMessageBox::warning(self, QStringLiteral("提示"),
msg.isEmpty() ? QStringLiteral("操作失败") : msg);
return;
}
// selective 时本地 displayStatus 已在请求前更新;非 selective 同步整体状态。
if (!selective)
for (int& s : self->data_.scatter.displayStatus) s = hide ? 1 : 0;
localToggle();
});
}
void RawDataChartView::openFilterDialog(QWidget* anchor) {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(anchor); return; }
// 传当前 V 值数组驱动分布直方图(与图上散点同源,反映当前值类型变换后的分布)。
ScatterFilterDialog dlg(cmdRepo_, dsId, currentVFieldCode(), data_.scatter.v, this);
dlg.exec(); // 成功/失败由对话框内部反馈(生成过滤数据集为后端动作)。
}
void RawDataChartView::reloadForVValue() {
// V 值切换:重新请求散点 + 色阶(原版 vValueType change
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(vCombo_); return; }
QPointer<RawDataChartView> self(this);
const QString vCode = currentVFieldCode();
cmdRepo_->loadMeasurementScatter(
dsId, vCode, [self](bool ok, geopro::core::ScatterPayload payload, QString msg) {
if (!self) return;
if (!ok) {
QMessageBox::warning(self, QStringLiteral("提示"),
msg.isEmpty() ? QStringLiteral("加载失败") : msg);
return;
}
// 保留已建 measurement 工具条(不重建):只换数据 + 色阶。setData 会重置 baseV_。
self->setData(payload);
// 切 V 后值类型回到线性(与原版重新请求后默认线性一致)。
if (self->valueTypeCombo_) {
const QSignalBlocker block(self->valueTypeCombo_);
self->valueTypeCombo_->setCurrentIndex(0);
}
});
}
void RawDataChartView::applyValueType() {
// 本地值类型变换(线性/倒数/对数):从 baseV_ 算,重新上色重绘(无后端)。
if (!valueTypeCombo_ || baseV_.empty()) return;
const ScatterValueType type = scatterValueTypeFromCode(valueTypeCombo_->currentData().toString());
data_.scatter.v = applyScatterValueType(baseV_, type);
redrawScatter();
}
void RawDataChartView::openScatterColorScale(QWidget* anchor) {
if (data_.scale.empty()) { showNotImplemented(anchor); return; }
// 数据范围取当前 v 有限值 min/max散点上色 cauto 区间)。
double vMin = std::numeric_limits<double>::max();
double vMax = std::numeric_limits<double>::lowest();
for (double v : data_.scatter.v) {
if (!std::isfinite(v)) continue;
if (v < vMin) vMin = v;
if (v > vMax) vMax = v;
}
if (vMin > vMax) { vMin = 0.0; vMax = 1.0; }
std::vector<double> samples = data_.scatter.v; // 直方图/等积分层用原始标量
// 接通色阶模板库:注入仓储 + 当前 projectId + 载荷 templateId另存为/打开/覆盖 可用)。
ColorScaleConfigDialog dlg(data_.scale, vMin, vMax, std::move(samples), {}, colorTplRepo_,
projectIdGetter_ ? projectIdGetter_() : QString(), data_.templateId,
this);
if (dlg.exec() != QDialog::Accepted) return;
// 本地重建 colorSvc_ 重绘散点M8 即时生效)。
data_.scale = dlg.colorScale();
delete colorSvc_;
colorSvc_ = new ColorMapService(data_.scale);
redrawScatter();
// 同步右侧竖条/底部横条色阶图例。
if (data_.verticalLegend) colorBarV_->setColorScale(data_.scale);
else colorBar_->setColorScale(data_.scale);
showToast(this, QStringLiteral("色阶应用成功")); // M8 成功提示(对照原版 Message.success
// 持久化到后端saveColorGradationbusinessCode=当前 V 值type=3 散点路径)。
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) return; // 无仓储 → 仅本地生效(不阻塞)
QJsonObject body{
{QStringLiteral("dsObjectId"), dsId},
{QStringLiteral("templateId"), data_.templateId}, // 读取到的色阶模板 id对照原版可空
{QStringLiteral("businessCode"), currentVFieldCode()},
{QStringLiteral("projectId"), projectId},
{QStringLiteral("properties"), buildColorScaleProperties(data_.scale, dlg.lineConfig())},
};
QPointer<RawDataChartView> self(this);
cmdRepo_->saveColorGradation(body, [self](bool ok, QString msg) {
if (!self || ok) return;
QMessageBox::warning(self, QStringLiteral("色阶配置"),
msg.isEmpty() ? QStringLiteral("色阶保存失败") : msg);
});
}
void RawDataChartView::openSaveAs(QWidget* anchor) {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(anchor); return; }
SaveAsDialog dlg(SaveAsDialog::Mode::RawData, cmdRepo_, dsId, this);
dlg.exec(); // 成功/失败由对话框内部反馈。
}
void RawDataChartView::exportDat() {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
// 参数与原版 exportScatterData2Dat 一致electrodePosition=2, ipDataMark=0, typeMeasurement=0。
QPointer<RawDataChartView> self(this);
cmdRepo_->exportMeasurementDat(
dsId, /*electrodePosition*/ 2, /*ipDataMark*/ 0, /*typeMeasurement*/ 0,
[self](bool ok, QString fileName, QString fileData, QString msg) {
if (!self) return;
if (!ok) {
QMessageBox::warning(self, QStringLiteral("导出"),
msg.isEmpty() ? QStringLiteral("导出失败!") : msg);
return;
}
const QString suggested = fileName.isEmpty() ? QStringLiteral("export.dat") : fileName;
const QString path = QFileDialog::getSaveFileName(
self, QStringLiteral("导出 DAT"), suggested, QStringLiteral("DAT 文件 (*.dat)"));
if (path.isEmpty()) return;
const QByteArray bytes = QByteArray::fromBase64(fileData.toUtf8());
QSaveFile f(path);
if (!f.open(QIODevice::WriteOnly) || f.write(bytes) != bytes.size() || !f.commit()) {
QMessageBox::warning(self, QStringLiteral("导出"), QStringLiteral("写入文件失败。"));
}
});
}
void RawDataChartView::toggleInfoMode(bool on) {
infoMode_ = on;
if (on && !infoPanel_) {
// 首次开启:建覆盖在图区右上角的属性面板(复刻原版 .scatterInfos 浮层)。
// A/B/M/N 按原版逐项配色item-label-a 红 / -b 蓝 / -m 绿 / -n 橙#F4B008
infoPanel_ = new QWidget(plot_->canvas());
infoPanel_->setObjectName(QStringLiteral("scatterInfoPanel"));
auto* il = new QVBoxLayout(infoPanel_);
il->setContentsMargins(8, 6, 8, 6);
il->setSpacing(2);
infoHint_ = new QLabel(QStringLiteral("点选散点查看属性"), infoPanel_);
il->addWidget(infoHint_);
// 各属性行标签上色label QSS 不染行值),初始隐藏,命中点后填值显示。
infoValA_ = new QLabel(infoPanel_);
infoValA_->setObjectName(QStringLiteral("infoA"));
infoValB_ = new QLabel(infoPanel_);
infoValB_->setObjectName(QStringLiteral("infoB"));
infoValM_ = new QLabel(infoPanel_);
infoValM_->setObjectName(QStringLiteral("infoM"));
infoValN_ = new QLabel(infoPanel_);
infoValN_->setObjectName(QStringLiteral("infoN"));
infoValRow_ = new QLabel(infoPanel_);
infoValPseu_ = new QLabel(infoPanel_);
for (QLabel* l : {infoValA_, infoValB_, infoValM_, infoValN_, infoValRow_, infoValPseu_}) {
l->setVisible(false);
il->addWidget(l);
}
applyTokenizedStyleSheet(
infoPanel_,
QStringLiteral("QWidget#scatterInfoPanel { background: {{bg/panel}};"
" border: 1px solid {{border/default}}; border-radius: 6px; }"
"QLabel { color: {{text/primary}}; }"
"QLabel#infoA { color: #FF0000; }" // A 红
"QLabel#infoB { color: #0000FF; }" // B 蓝
"QLabel#infoM { color: #008000; }" // M 绿
"QLabel#infoN { color: #F4B008; }")); // N 橙黄
// 画布事件过滤器:信息模式下点击找最近点显示属性。
plot_->canvas()->installEventFilter(this);
}
if (infoPanel_) {
infoPanel_->setVisible(on);
if (on) {
infoPanel_->adjustSize();
infoPanel_->move(plot_->canvas()->width() - infoPanel_->width() - 10, 10);
infoPanel_->raise();
}
}
}
void RawDataChartView::showPointInfoAt(const QPoint& canvasPos) {
if (!infoValA_ || data_.scatter.x.empty()) return;
const QwtScaleMap xMap = plot_->canvasMap(QwtPlot::xTop);
const QwtScaleMap yMap = plot_->canvasMap(QwtPlot::yLeft);
const auto& s = data_.scatter;
const std::size_t n = std::min(s.x.size(), s.y.size());
double bestD2 = std::numeric_limits<double>::max();
std::size_t bestI = 0;
for (std::size_t i = 0; i < n; ++i) {
const double dx = xMap.transform(s.x[i]) - canvasPos.x();
const double dy = yMap.transform(s.y[i]) - canvasPos.y();
const double d2 = dx * dx + dy * dy;
if (d2 < bestD2) { bestD2 = d2; bestI = i; }
}
constexpr double kHit = 8.0; // 命中半径(像素)
if (bestD2 > kHit * kHit) return; // 未命中任何点 → 保持当前显示
auto at = [](const std::vector<double>& v, std::size_t i) {
return i < v.size() ? v[i] : 0.0;
};
// 复刻原版 scatterInfosA / B / M / N / DataRow / Pseu_ResisA/B/M/N 标签逐项配色)。
if (infoHint_) infoHint_->setVisible(false);
infoValA_->setText(QStringLiteral("A= %1").arg(QString::number(at(s.a, bestI), 'g', 6)));
infoValB_->setText(QStringLiteral("B= %1").arg(QString::number(at(s.b, bestI), 'g', 6)));
infoValM_->setText(QStringLiteral("M= %1").arg(QString::number(at(s.m, bestI), 'g', 6)));
infoValN_->setText(QStringLiteral("N= %1").arg(QString::number(at(s.n, bestI), 'g', 6)));
infoValRow_->setText(QStringLiteral("DataRow= %1").arg(QString::number(at(s.row, bestI), 'g', 6)));
infoValPseu_->setText(
QStringLiteral("Pseu_Resis= %1").arg(QString::number(at(s.pseu, bestI), 'g', 6)));
for (QLabel* l : {infoValA_, infoValB_, infoValM_, infoValN_, infoValRow_, infoValPseu_})
l->setVisible(true);
infoPanel_->adjustSize();
infoPanel_->move(plot_->canvas()->width() - infoPanel_->width() - 10, 10);
infoPanel_->raise();
}
void RawDataChartView::toggleMarqueeMode(bool on) {
marqueeMode_ = on;
if (marquee_) marquee_->setActive(on);
if (!on && scatterItem_) {
// 退出框选:清选区高亮(与原版 exitSelectMode clearSelection 一致)。
scatterItem_->clearSelection();
plot_->replot();
}
}
void RawDataChartView::onMarqueeSelected(const std::vector<int>& indices) {
// 框选完成:高亮框内散点(红框)。空框 → 清选区。
if (!scatterItem_) return;
scatterItem_->setSelectedIndices(indices);
plot_->replot();
}
bool RawDataChartView::eventFilter(QObject* obj, QEvent* ev) {
// 仅信息模式 + 画布左键点击:找最近散点显示属性,不消费事件(保留平移链路)。
if (infoMode_ && plot_ && obj == plot_->canvas() && ev->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(ev);
if (me->button() == Qt::LeftButton) showPointInfoAt(me->position().toPoint());
}
return QWidget::eventFilter(obj, ev);
}
void RawDataChartView::replotForAxis() {
// 本地换 x/y无网络按下拉 fieldCode 从备选列取数据,重设 scatter.x/.y 并重绘。
if (!xCombo_ || !yCombo_ || !scatterItem_) return;
const QString xCode = xCombo_->currentData().toString();
const QString yCode = yCombo_->currentData().toString();
if (xCode == QStringLiteral("horizontalDistance") && !data_.altXHorizontal.empty())
data_.scatter.x = data_.altXHorizontal;
else if (!data_.altXSlope.empty())
data_.scatter.x = data_.altXSlope; // 默认/斜距
if (yCode == QStringLiteral("elevationPseudoDepth") && !data_.altYElevationPseudo.empty())
data_.scatter.y = data_.altYElevationPseudo;
else if (yCode == QStringLiteral("pseudoDepth") && !data_.altYPseudo.empty())
data_.scatter.y = data_.altYPseudo;
// 层数(Layer No):数据为 null → 不改轴(保持当前),选项可选但 no-op。
scatterItem_->setData(data_.scatter, colorSvc_);
if (hoverTip_) hoverTip_->setField(&data_.scatter);
QRectF bbox = scatterItem_->boundingRect();
if (!bbox.isEmpty()) {
plot_->setAxisScale(QwtPlot::xTop, bbox.left(), bbox.right());
plot_->setAxisScale(QwtPlot::yLeft, bbox.top(), bbox.bottom());
}
plot_->updateAxes();
if (rescaler_) rescaler_->rescale();
plot_->replot();
}
void RawDataChartView::buildMeasurementToolbar(const geopro::core::ScatterToolbarConf& conf) {
auto* toolbar = new QWidget(this);
auto* tbLay = new QHBoxLayout(toolbar);
tbLay->setContentsMargins(4, 4, 4, 4);
tbLay->setSpacing(4);
// [i] info + [▣] 框选:占位(暂未实现)。用 QPainter 画的线性图标HiDPI 清晰,随主题)。
auto* btnInfo = new QToolButton(toolbar);
btnInfo->setToolTip(QStringLiteral("查看散点属性")); // 对照原版 datasetTool.vue tooltip
styleToolIconButton(btnInfo, makeInfoIcon());
auto* btnMarquee = new QToolButton(toolbar);
btnMarquee->setToolTip(QStringLiteral("散点的点选")); // 对照原版 datasetTool.vue tooltip
styleToolIconButton(btnMarquee, makeMarqueeIcon());
// 主题热切重绘图标info 锚定品牌蓝marquee 描边随次要文本色)。
connect(&ThemeManager::instance(), &ThemeManager::changed, btnInfo,
[btnInfo]() { btnInfo->setIcon(makeInfoIcon()); });
connect(&ThemeManager::instance(), &ThemeManager::changed, btnMarquee,
[btnMarquee]() { btnMarquee->setIcon(makeMarqueeIcon()); });
// [i] 信息:切换信息模式(点选散点看 A/B/M/N/DataRow/Pseu_Resis
btnInfo->setCheckable(true);
connect(btnInfo, &QToolButton::toggled, this, [this](bool on) { toggleInfoMode(on); });
// [▣] 框选:可勾选 → 进入框选模式(橡皮筋选框内散点高亮;显示/隐藏改对选中点)。
btnMarquee->setCheckable(true);
connect(btnMarquee, &QToolButton::toggled, this, [this](bool on) { toggleMarqueeMode(on); });
// 显示 / 隐藏popconfirm 确认 → saveDisplayStatus 持久化 → 本地切换M1
auto* btnShow = new QPushButton(QStringLiteral("显示"), toolbar);
auto* btnHide = new QPushButton(QStringLiteral("隐藏"), toolbar);
connect(btnShow, &QPushButton::clicked, this, [this]() { onShowHide(/*hide*/ false); });
connect(btnHide, &QPushButton::clicked, this, [this]() { onShowHide(/*hide*/ true); });
// 数据过滤范围过滤弹窗M3
auto* btnFilter = new QPushButton(QStringLiteral("数据过滤"), toolbar);
connect(btnFilter, &QPushButton::clicked, this, [this, btnFilter]() { openFilterDialog(btnFilter); });
// 导出 DAT原版在页头「导出」客户端页头为占位且跨 dd 共用,故 measurement 专属导出
// 收纳进本工具条M12。点击 → exportMeasurementDat → 选路径写盘。
auto* btnExport = new QPushButton(QStringLiteral("导出"), toolbar);
connect(btnExport, &QPushButton::clicked, this, [this]() { exportDat(); });
// x / y 下拉本地换列重绘v 下拉:重新请求散点+色阶M6值类型下拉本地变换M7
// 各下拉固定宽度对照原版 datasetTool.vueX=120/Y=160/V=160/值类型=120
xCombo_ = new EmptyAwareComboBox(toolbar);
xCombo_->setFixedWidth(kComboW_X);
fillCombo(xCombo_, conf.x, conf.defaultX, QString());
yCombo_ = new EmptyAwareComboBox(toolbar);
yCombo_->setFixedWidth(kComboW_Y);
fillCombo(yCombo_, conf.y, conf.defaultY, QString());
vCombo_ = new EmptyAwareComboBox(toolbar);
vCombo_->setFixedWidth(kComboW_V);
fillCombo(vCombo_, conf.v, conf.defaultV, QString());
valueTypeCombo_ = new EmptyAwareComboBox(toolbar);
valueTypeCombo_->setFixedWidth(kComboW_ValueType);
// 值类型固定三项(原版 linearity/inverse/logarithm本地变换无后端。
valueTypeCombo_->addItem(QStringLiteral("线性"), QStringLiteral("linearity"));
valueTypeCombo_->addItem(QStringLiteral("倒数"), QStringLiteral("inverse"));
valueTypeCombo_->addItem(QStringLiteral("对数"), QStringLiteral("logarithm"));
// 无高程时禁用 X/Y 下拉(对照原版 :disabled="!currentHasElevation")。
// 判断依据:高程相关备选列 altYElevationPseudo 非空即视为「有高程数据」(与 y 下拉
// 「伪深度+高程」项的数据源一致无高程时该列为空X/Y 轴切换无意义故禁用)。
const bool hasElevation = !data_.altYElevationPseudo.empty();
xCombo_->setEnabled(hasElevation);
yCombo_->setEnabled(hasElevation);
connect(xCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this](int) { replotForAxis(); });
connect(yCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this](int) { replotForAxis(); });
// V 值切换:重新请求散点+色阶(用 activated 仅用户操作触发,填充期不误触)。
connect(vCombo_, QOverload<int>::of(&QComboBox::activated), this,
[this](int) { reloadForVValue(); });
connect(valueTypeCombo_, QOverload<int>::of(&QComboBox::activated), this,
[this](int) { applyValueType(); });
// 色阶配置:复用 ColorScaleConfigDialog散点上色路径保存 + 本地重绘M8
auto* btnColorScale = new QPushButton(QStringLiteral("色阶配置"), toolbar);
connect(btnColorScale, &QPushButton::clicked, this,
[this, btnColorScale]() { openScatterColorScale(btnColorScale); });
// 右侧主操作(蓝色):生成视电阻率数据 / 反演运算 / 另存为。
auto* btnGen = new QPushButton(QStringLiteral("生成视电阻率数据"), toolbar);
auto* btnInvert = new QPushButton(QStringLiteral("反演运算"), toolbar);
auto* btnSaveAs = new QPushButton(QStringLiteral("另存为"), toolbar);
for (auto* b : {btnGen, btnInvert, btnSaveAs}) {
b->setObjectName(QStringLiteral("primaryBtn")); // 蓝色主按钮(下方 QSS
}
// 反演运算 → submitInversionTask生成视电阻率 → createVisualResistivityData共享反演对话框
connect(btnInvert, &QPushButton::clicked, this,
[this, btnInvert]() { openInversionDialog(/*apparentResistivity*/ false, btnInvert); });
connect(btnGen, &QPushButton::clicked, this,
[this, btnGen]() { openInversionDialog(/*apparentResistivity*/ true, btnGen); });
// 另存为:新增/覆盖弹窗 → saveRawDataM11
connect(btnSaveAs, &QPushButton::clicked, this,
[this, btnSaveAs]() { openSaveAs(btnSaveAs); });
tbLay->addWidget(btnInfo);
tbLay->addWidget(btnMarquee);
tbLay->addWidget(btnShow);
tbLay->addWidget(btnHide);
tbLay->addWidget(btnFilter);
tbLay->addWidget(xCombo_);
tbLay->addWidget(yCombo_);
tbLay->addWidget(vCombo_);
tbLay->addWidget(valueTypeCombo_);
tbLay->addWidget(btnColorScale);
tbLay->addStretch(); // 把导出 + 主操作推到右侧
// 导出:原版在详情页头 Header非工具条。客户端页头「导出」HeaderAction 为跨 ddCode
// 共用的静态占位PanelHeader 不暴露按钮/不发信号IDetailView 亦无导出接口),按 ddCode
// 分派转发成本高且易误触其它视图;故 measurement 专属导出保留在工具条内,置于最右侧、
// 紧邻主操作组样式贴近原版outline 风格的普通按钮)。
tbLay->addWidget(btnExport);
tbLay->addWidget(btnGen);
tbLay->addWidget(btnInvert);
tbLay->addWidget(btnSaveAs);
// 蓝色主按钮样式(随主题;普通按钮/下拉走全局 QSS 已支持明暗)。
applyTokenizedStyleSheet(
toolbar,
QStringLiteral(
"QPushButton#primaryBtn { background: {{accent/primary}}; color: {{text/on-primary}};"
" border: 1px solid {{accent/primary}}; border-radius: 6px; padding: 6px 14px; }"
"QPushButton#primaryBtn:hover { background: {{accent/primary-hover}}; border-color: {{accent/primary-hover}}; }"
"QPushButton#primaryBtn:pressed { background: {{accent/primary-pressed}}; }"));
// 替换 ctor 建的 inversion 工具条(同一顶层布局首位)。
rootLay_->replaceWidget(toolbar_, toolbar);
toolbar_->deleteLater();
toolbar_ = toolbar;
chartTypeCombo_ = nullptr; // 已随旧工具条移除
measurementToolbar_ = true;
}
void RawDataChartView::setPayload(const QVariant& payload) {
if (!payload.canConvert<geopro::core::ScatterPayload>()) {
// 坏/空 variant保持空态不渲染、不崩。E2+ 可在此显式提示「渲染数据格式错误」。
return;
}
setData(payload.value<geopro::core::ScatterPayload>());
}
void RawDataChartView::setData(const geopro::core::ScatterPayload& p) {
data_ = p;
baseV_ = data_.scatter.v; // 缓存原始 v线性M7 值类型变换从原值算,不累积误差
if (hoverTip_) hoverTip_->setField(&data_.scatter); // 显式重绑(地址稳定,消除隐式依赖)
if (marquee_) marquee_->setField(&data_.scatter); // M14 框选拾取同源重绑
// measurement 载荷toolbar 非空):首次到来时建并替换工具条(视觉 1:1。反演留空 → 不动。
if (!p.toolbar.empty() && !measurementToolbar_) buildMeasurementToolbar(p.toolbar);
if (p.scale.empty()) return;
// 重建 ColorMapService旧的 scatterItem 已被 QwtPlot detach/delete
delete colorSvc_;
colorSvc_ = new ColorMapService(p.scale);
// 散点颜色归一化对齐原版 Plotlycmin/cmax 未设 → cauto=数据 min/max按 vlist 有限值
// 的 min/max 设数据范围使整段色阶铺满数据实际范围。measurement 与反演原数据同此路径:
// v 含负异常值(如 -1066故 cmin<0中段视电阻率归一化到色阶中部深品红/紫)。
{
double vMin = std::numeric_limits<double>::max();
double vMax = std::numeric_limits<double>::lowest();
for (double v : p.scatter.v) {
if (!std::isfinite(v)) continue; // 跳过 NaN/±Inf脏数据否则数据范围被污染→全图 NaN 取色
if (v < vMin) vMin = v;
if (v > vMax) vMax = v;
}
if (vMin <= vMax) colorSvc_->setDataRange(vMin, vMax);
}
// 卸载旧散点项QwtPlot 默认 autoDelete=true析构时 delete 仍在 dict 的 item
// 必须先 detach()(从 dict 移除)再 delete否则 QwtPlot 析构时会 double-free。
if (scatterItem_) {
scatterItem_->detach();
delete scatterItem_;
scatterItem_ = nullptr;
}
// 新建散点项并挂到 plot
scatterItem_ = new ScatterPlotItem();
scatterItem_->setData(p.scatter, colorSvc_);
scatterItem_->attach(plot_);
// 按数据包围盒设置轴范围
QRectF bbox = scatterItem_->boundingRect();
if (!bbox.isEmpty()) {
plot_->setAxisScale(QwtPlot::xTop, bbox.left(), bbox.right());
plot_->setAxisScale(QwtPlot::yLeft, bbox.top(), bbox.bottom());
}
plot_->updateAxes();
if (rescaler_) rescaler_->rescale(); // 应用真实比尺
plot_->replot();
// 更新色阶条measurement 用右侧竖条,反演原数据用底部横条(二选一显示)。
if (p.verticalLegend) {
colorBarV_->setColorScale(p.scale);
colorBarV_->setVisible(true);
colorBar_->setVisible(false);
} else {
colorBar_->setColorScale(p.scale);
colorBar_->setVisible(true);
colorBarV_->setVisible(false);
}
}
} // namespace geopro::app