#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 #include "repo/IDatasetCommandRepository.hpp" #include #include #include "EmptyAwareComboBox.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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); // ---- QwtPlot(stretch 填满剩余空间)---- 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_); // 过原点零线(对齐原版 zeroline:x=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& 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); // ---- 图表行:plot(stretch)+ 右侧竖向色阶条(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& opts, const QString& defaultCode, const QString& defaultName) { for (const auto& o : opts) { // 用户可见名为 name;userData 存 fieldCode(重绘/识别用)。 combo->addItem(o.name, o.code); } // 默认选中:优先匹配 fieldCode,否则匹配 name(method 的 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 icons(viewBox 0 0 48 48,stroke-width 4,stroke=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 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)); } // 组装色阶 properties(colorBar + 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 dsIdGetter, std::function 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:反演原数据散点色阶(type1,businessCode 空串,对照原版 originPage)。 if (data_.scale.empty()) { showNotImplemented(anchor); return; } double vMin = std::numeric_limits::max(); double vMax = std::numeric_limits::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 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 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/max(cauto,与初始上色一致)。 double vMin = std::numeric_limits::max(); double vMax = std::numeric_limits::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(); }; // 收集要持久化的点 id:selective → 选中点 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(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 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 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::max(); double vMax = std::numeric_limits::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 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) // 持久化到后端(saveColorGradation,businessCode=当前 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 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 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::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& v, std::size_t i) { return i < v.size() ? v[i] : 0.0; }; // 复刻原版 scatterInfos:A / B / M / N / DataRow / Pseu_Resis(A/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& 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(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.vue(X=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::of(&QComboBox::currentIndexChanged), this, [this](int) { replotForAxis(); }); connect(yCombo_, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int) { replotForAxis(); }); // V 值切换:重新请求散点+色阶(用 activated 仅用户操作触发,填充期不误触)。 connect(vCombo_, QOverload::of(&QComboBox::activated), this, [this](int) { reloadForVValue(); }); connect(valueTypeCombo_, QOverload::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); }); // 另存为:新增/覆盖弹窗 → saveRawData(M11)。 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()) { // 坏/空 variant:保持空态(不渲染、不崩)。E2+ 可在此显式提示「渲染数据格式错误」。 return; } setData(payload.value()); } 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); // 散点颜色归一化对齐原版 Plotly(cmin/cmax 未设 → cauto=数据 min/max),按 vlist 有限值 // 的 min/max 设数据范围,使整段色阶铺满数据实际范围。measurement 与反演原数据同此路径: // v 含负异常值(如 -1066),故 cmin<0,中段视电阻率归一化到色阶中部(深品红/紫)。 { double vMin = std::numeric_limits::max(); double vMax = std::numeric_limits::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