feat(ui): DatasetChartView 散点/等值面/异常叠加(QGraphicsView)
This commit is contained in:
parent
3192cf24cf
commit
facb812bca
|
|
@ -26,6 +26,7 @@ add_executable(geopro_desktop WIN32
|
||||||
panels/ObjectTreePanel.cpp
|
panels/ObjectTreePanel.cpp
|
||||||
panels/DynamicFormView.cpp
|
panels/DynamicFormView.cpp
|
||||||
panels/ObjectExceptionPanel.cpp
|
panels/ObjectExceptionPanel.cpp
|
||||||
|
panels/chart/DatasetChartView.cpp
|
||||||
CentralScene.cpp
|
CentralScene.cpp
|
||||||
ProjectListDialog.cpp
|
ProjectListDialog.cpp
|
||||||
SettingsDialog.cpp)
|
SettingsDialog.cpp)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
#include "panels/chart/DatasetChartView.hpp"
|
||||||
|
#include <QGraphicsScene>
|
||||||
|
#include <QGraphicsPathItem>
|
||||||
|
#include <QGraphicsRectItem>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QPen>
|
||||||
|
#include <QBrush>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QWheelEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QFontMetrics>
|
||||||
|
namespace geopro::app {
|
||||||
|
using geopro::core::Rgba;
|
||||||
|
|
||||||
|
static QColor toQ(const Rgba& c) { return QColor(c.r, c.g, c.b, c.a); }
|
||||||
|
|
||||||
|
DatasetChartView::DatasetChartView(QWidget* parent)
|
||||||
|
: QGraphicsView(parent), scene_(new QGraphicsScene(this)) {
|
||||||
|
setScene(scene_);
|
||||||
|
setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
setDragMode(QGraphicsView::ScrollHandDrag);
|
||||||
|
// 场景 y 向上为正:用 y 翻转的变换(QGraphicsView 默认 y 向下)。
|
||||||
|
scale(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatasetChartView::clearChart() {
|
||||||
|
scene_->clear(); anomalyItems_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatasetChartView::showScatter(const geopro::core::ScatterField& f, const geopro::core::ColorScale& cs) {
|
||||||
|
clearChart();
|
||||||
|
const double sz = 0.6; // 方块边长(数据单位,近似 web 方点)
|
||||||
|
for (size_t i = 0; i < f.v.size(); ++i) {
|
||||||
|
auto* r = scene_->addRect(f.x[i] - sz / 2, f.y[i] - sz / 2, sz, sz,
|
||||||
|
QPen(Qt::white, 0), QBrush(toQ(cs.colorAt(f.v[i]))));
|
||||||
|
r->setPen(QPen(Qt::white, 0)); // 白描边
|
||||||
|
}
|
||||||
|
rebuildAnomalyItems();
|
||||||
|
setLegend(cs);
|
||||||
|
viewport()->update();
|
||||||
|
fitInView(scene_->itemsBoundingRect(), Qt::KeepAspectRatio); // x:y 等比
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatasetChartView::showContour(const geopro::core::Grid& g, const geopro::core::ColorScale& cs,
|
||||||
|
const geopro::render::ContourOptions& opt) {
|
||||||
|
clearChart();
|
||||||
|
const auto r = geopro::render::buildContourBands(g, cs, opt);
|
||||||
|
for (const auto& b : r.bands) {
|
||||||
|
QPainterPath path; if (b.ring.empty()) continue;
|
||||||
|
path.moveTo(b.ring[0].x, b.ring[0].y);
|
||||||
|
for (size_t k = 1; k < b.ring.size(); ++k) path.lineTo(b.ring[k].x, b.ring[k].y);
|
||||||
|
path.closeSubpath();
|
||||||
|
auto* it = scene_->addPath(path, QPen(Qt::NoPen), QBrush(toQ(b.color)));
|
||||||
|
it->setZValue(0);
|
||||||
|
}
|
||||||
|
if (showContourLines_)
|
||||||
|
for (const auto& l : r.lines) {
|
||||||
|
if (l.pts.empty()) continue;
|
||||||
|
QPainterPath path; path.moveTo(l.pts[0].x, l.pts[0].y);
|
||||||
|
for (size_t k = 1; k < l.pts.size(); ++k) path.lineTo(l.pts[k].x, l.pts[k].y);
|
||||||
|
auto* it = scene_->addPath(path, QPen(QColor(0, 0, 0), 0));
|
||||||
|
it->setZValue(1);
|
||||||
|
}
|
||||||
|
rebuildAnomalyItems();
|
||||||
|
setLegend(cs);
|
||||||
|
viewport()->update();
|
||||||
|
fitInView(scene_->itemsBoundingRect(), Qt::IgnoreAspectRatio); // 剖面 X/Y 各自铺满
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatasetChartView::setAnomalies(const std::vector<geopro::core::Anomaly>& list) {
|
||||||
|
anomalies_ = list; rebuildAnomalyItems();
|
||||||
|
}
|
||||||
|
void DatasetChartView::setHiddenAnomalies(const std::set<int>& hidden) { hidden_ = hidden; rebuildAnomalyItems(); }
|
||||||
|
void DatasetChartView::setShowAnomalies(bool on) { showAnomalies_ = on; rebuildAnomalyItems(); }
|
||||||
|
void DatasetChartView::setShowContourLines(bool on) { showContourLines_ = on; }
|
||||||
|
|
||||||
|
void DatasetChartView::rebuildAnomalyItems() {
|
||||||
|
for (auto* it : anomalyItems_) { scene_->removeItem(it); delete it; }
|
||||||
|
anomalyItems_.clear();
|
||||||
|
if (!showAnomalies_) return;
|
||||||
|
for (int i = 0; i < static_cast<int>(anomalies_.size()); ++i) {
|
||||||
|
if (hidden_.count(i)) continue;
|
||||||
|
const auto& a = anomalies_[i];
|
||||||
|
if (a.localPts.size() < 2) continue;
|
||||||
|
QPainterPath path; path.moveTo(a.localPts[0].x, a.localPts[0].y);
|
||||||
|
for (size_t k = 1; k < a.localPts.size(); ++k) path.lineTo(a.localPts[k].x, a.localPts[k].y);
|
||||||
|
if (static_cast<int>(a.markType) == 3) path.closeSubpath();
|
||||||
|
QPen pen(QColor(QString::fromStdString(a.lineColor)), 0);
|
||||||
|
if (a.dashed) pen.setStyle(Qt::DashLine);
|
||||||
|
auto* it = scene_->addPath(path, pen);
|
||||||
|
it->setZValue(2);
|
||||||
|
anomalyItems_.push_back(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatasetChartView::wheelEvent(QWheelEvent* e) {
|
||||||
|
const double f = e->angleDelta().y() > 0 ? 1.15 : 1 / 1.15;
|
||||||
|
scale(f, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task 3.2:坐标轴 overlay + 底部色阶图例。
|
||||||
|
// drawForeground 在每次 repaint 时调用(缩放/平移后自动重绘),无需手动更新。
|
||||||
|
// 轴线"钉"在 viewport 左/下边:以 viewport()->rect() 为参考,
|
||||||
|
// 用 mapToScene / mapFromScene 把场景范围映射到像素位置。
|
||||||
|
void DatasetChartView::drawForeground(QPainter* p, const QRectF& /*rect*/) {
|
||||||
|
if (legendScale_.empty()) return;
|
||||||
|
|
||||||
|
p->save();
|
||||||
|
// 重置变换,后续全用视口像素坐标绘制
|
||||||
|
p->resetTransform();
|
||||||
|
|
||||||
|
const QRect vr = viewport()->rect();
|
||||||
|
const int W = vr.width();
|
||||||
|
const int H = vr.height();
|
||||||
|
|
||||||
|
// ── 色阶图例(底部)────────────────────────────────
|
||||||
|
const int legendH = 16; // 色条高度(px)
|
||||||
|
const int legendMarginB = 20; // 底部文字留白
|
||||||
|
const int legendMarginL = 50; // 左侧轴标签留白
|
||||||
|
const int legendMarginR = 10;
|
||||||
|
const int legendY = H - legendMarginB - legendH;
|
||||||
|
const int legendW = W - legendMarginL - legendMarginR;
|
||||||
|
|
||||||
|
const std::vector<double> stops = legendScale_.stopValues();
|
||||||
|
if (stops.size() >= 2 && legendW > 0) {
|
||||||
|
const int nSeg = static_cast<int>(stops.size()) - 1;
|
||||||
|
const double segW = static_cast<double>(legendW) / nSeg;
|
||||||
|
for (int s = 0; s < nSeg; ++s) {
|
||||||
|
// 取该段中点值的颜色(与 web 阶梯色一致)
|
||||||
|
const double midVal = (stops[s] + stops[s + 1]) * 0.5;
|
||||||
|
const QColor c = toQ(legendScale_.colorAt(midVal));
|
||||||
|
const int x0 = legendMarginL + static_cast<int>(s * segW);
|
||||||
|
const int x1 = legendMarginL + static_cast<int>((s + 1) * segW);
|
||||||
|
p->fillRect(QRect(x0, legendY, x1 - x0, legendH), c);
|
||||||
|
}
|
||||||
|
// 边框
|
||||||
|
p->setPen(QPen(QColor(80, 80, 80), 1));
|
||||||
|
p->drawRect(QRect(legendMarginL, legendY, legendW, legendH));
|
||||||
|
|
||||||
|
// 分段值文字(在每个分割点下方)
|
||||||
|
p->setPen(Qt::black);
|
||||||
|
QFont font = p->font();
|
||||||
|
font.setPixelSize(10);
|
||||||
|
p->setFont(font);
|
||||||
|
for (int s = 0; s <= nSeg; ++s) {
|
||||||
|
const int xTick = legendMarginL + static_cast<int>(s * segW);
|
||||||
|
const QString label = QString::number(stops[s], 'g', 4);
|
||||||
|
QRect textR(xTick - 20, legendY + legendH + 2, 40, 14);
|
||||||
|
p->drawText(textR, Qt::AlignCenter, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 坐标轴(左/下边钉住视口)────────────────────────
|
||||||
|
// 把视口四个角映射到场景,得到场景可见范围
|
||||||
|
const QPointF sceneTL = mapToScene(vr.topLeft());
|
||||||
|
const QPointF sceneBR = mapToScene(vr.bottomRight());
|
||||||
|
|
||||||
|
// 因为 y 轴翻转(scale(1,-1)),场景坐标 y 向上为正
|
||||||
|
// sceneTL 的 scene-y 可能大于 sceneBR 的 scene-y,需取 min/max
|
||||||
|
const double sceneXMin = qMin(sceneTL.x(), sceneBR.x());
|
||||||
|
const double sceneXMax = qMax(sceneTL.x(), sceneBR.x());
|
||||||
|
const double sceneYMin = qMin(sceneTL.y(), sceneBR.y());
|
||||||
|
const double sceneYMax = qMax(sceneTL.y(), sceneBR.y());
|
||||||
|
|
||||||
|
const int axisLeft = legendMarginL; // 左轴 x 像素
|
||||||
|
const int axisBottom = legendY - 4; // 下轴 y 像素(色阶条上方留 4px)
|
||||||
|
|
||||||
|
p->setPen(QPen(QColor(100, 100, 100), 1));
|
||||||
|
|
||||||
|
// 左侧轴线(竖)
|
||||||
|
p->drawLine(axisLeft, 4, axisLeft, axisBottom);
|
||||||
|
// 下轴线(横)
|
||||||
|
p->drawLine(axisLeft, axisBottom, W - legendMarginR, axisBottom);
|
||||||
|
|
||||||
|
// 刻度数(6~8 个)
|
||||||
|
const int nTick = 7;
|
||||||
|
QFont tickFont = p->font();
|
||||||
|
tickFont.setPixelSize(10);
|
||||||
|
p->setFont(tickFont);
|
||||||
|
|
||||||
|
// X 轴刻度(下轴)
|
||||||
|
const double xRange = sceneXMax - sceneXMin;
|
||||||
|
if (xRange > 0) {
|
||||||
|
for (int t = 0; t <= nTick; ++t) {
|
||||||
|
const double sx = sceneXMin + xRange * t / nTick;
|
||||||
|
const QPoint vp = mapFromScene(QPointF(sx, sceneYMin)); // 场景 → 视口像素
|
||||||
|
const int px = vp.x();
|
||||||
|
if (px < axisLeft || px > W - legendMarginR) continue;
|
||||||
|
p->setPen(QPen(QColor(100, 100, 100), 1));
|
||||||
|
p->drawLine(px, axisBottom, px, axisBottom + 4);
|
||||||
|
p->setPen(Qt::black);
|
||||||
|
const QString label = QString::number(sx, 'g', 4);
|
||||||
|
p->drawText(QRect(px - 20, axisBottom + 5, 40, 12), Qt::AlignCenter, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y 轴刻度(左轴)
|
||||||
|
const double yRange = sceneYMax - sceneYMin;
|
||||||
|
if (yRange > 0) {
|
||||||
|
for (int t = 0; t <= nTick; ++t) {
|
||||||
|
const double sy = sceneYMin + yRange * t / nTick;
|
||||||
|
const QPoint vp = mapFromScene(QPointF(sceneXMin, sy));
|
||||||
|
const int py = vp.y();
|
||||||
|
if (py < 4 || py > axisBottom) continue;
|
||||||
|
p->setPen(QPen(QColor(100, 100, 100), 1));
|
||||||
|
p->drawLine(axisLeft - 4, py, axisLeft, py);
|
||||||
|
p->setPen(Qt::black);
|
||||||
|
const QString label = QString::number(sy, 'g', 4);
|
||||||
|
p->drawText(QRect(0, py - 7, axisLeft - 5, 14), Qt::AlignRight | Qt::AlignVCenter, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p->restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
#pragma once
|
||||||
|
#include <set>
|
||||||
|
#include <vector>
|
||||||
|
#include <QGraphicsView>
|
||||||
|
#include "model/Field.hpp"
|
||||||
|
#include "model/ColorScale.hpp"
|
||||||
|
#include "model/Anomaly.hpp"
|
||||||
|
#include "ContourBands.hpp"
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 平面图表视图:散点(原数据) / 等值面(网格) + 异常叠加 + 色阶图例。VTK 仅经 ContourBands 算几何。
|
||||||
|
class DatasetChartView : public QGraphicsView {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit DatasetChartView(QWidget* parent = nullptr);
|
||||||
|
void showScatter(const geopro::core::ScatterField& f, const geopro::core::ColorScale& cs);
|
||||||
|
void showContour(const geopro::core::Grid& g, const geopro::core::ColorScale& cs,
|
||||||
|
const geopro::render::ContourOptions& opt);
|
||||||
|
void setAnomalies(const std::vector<geopro::core::Anomaly>& list);
|
||||||
|
void setHiddenAnomalies(const std::set<int>& hidden); // 下标=list 序
|
||||||
|
void setShowAnomalies(bool on);
|
||||||
|
void setShowContourLines(bool on);
|
||||||
|
void clearChart();
|
||||||
|
protected:
|
||||||
|
void wheelEvent(QWheelEvent* e) override; // 滚轮缩放
|
||||||
|
void drawForeground(QPainter* p, const QRectF& rect) override;
|
||||||
|
private:
|
||||||
|
void rebuildAnomalyItems();
|
||||||
|
void setLegend(const geopro::core::ColorScale& cs) { legendScale_ = cs; }
|
||||||
|
QGraphicsScene* scene_;
|
||||||
|
std::vector<geopro::core::Anomaly> anomalies_;
|
||||||
|
std::set<int> hidden_;
|
||||||
|
bool showAnomalies_ = true;
|
||||||
|
bool showContourLines_ = true;
|
||||||
|
std::vector<class QGraphicsItem*> anomalyItems_;
|
||||||
|
geopro::core::ColorScale legendScale_;
|
||||||
|
};
|
||||||
|
} // namespace geopro::app
|
||||||
Loading…
Reference in New Issue