991 lines
47 KiB
C++
991 lines
47 KiB
C++
#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);
|
||
|
||
// ---- 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<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);
|
||
|
||
// ---- 图表行: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<geopro::core::FieldOption>& 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<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));
|
||
}
|
||
|
||
// 组装色阶 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<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:反演原数据散点色阶(type1,businessCode 空串,对照原版 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/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) 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<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)
|
||
|
||
// 持久化到后端(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<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;
|
||
};
|
||
// 复刻原版 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<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.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<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); });
|
||
// 另存为:新增/覆盖弹窗 → 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<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);
|
||
|
||
// 散点颜色归一化对齐原版 Plotly(cmin/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
|