feat/dataset-detail-chart #5
|
|
@ -26,7 +26,9 @@ 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
|
panels/DescriptionPanel.cpp
|
||||||
|
panels/chart/RawDataChartView.cpp
|
||||||
|
panels/chart/GridDataChartView.cpp
|
||||||
panels/AnomalyTablePanel.cpp
|
panels/AnomalyTablePanel.cpp
|
||||||
panels/DatasetDetailPage.cpp
|
panels/DatasetDetailPage.cpp
|
||||||
panels/DatasetDetailPanel.cpp
|
panels/DatasetDetailPanel.cpp
|
||||||
|
|
@ -51,6 +53,11 @@ target_link_libraries(geopro_desktop PRIVATE
|
||||||
geopro_controller # Phase 5:导航编排(WorkbenchNavController)
|
geopro_controller # Phase 5:导航编排(WorkbenchNavController)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Qwt(二维科学图表;源码存在时由根 CMake 定义 qwt 目标)。本步骤骨架先链好供后续渲染步骤使用。
|
||||||
|
if(TARGET qwt)
|
||||||
|
target_link_libraries(geopro_desktop PRIVATE qwt)
|
||||||
|
endif()
|
||||||
|
|
||||||
vtk_module_autoinit(TARGETS geopro_desktop MODULES ${VTK_LIBRARIES})
|
vtk_module_autoinit(TARGETS geopro_desktop MODULES ${VTK_LIBRARIES})
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,38 @@
|
||||||
#include "panels/DatasetDetailPage.hpp"
|
#include "panels/DatasetDetailPage.hpp"
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
#include "Glyphs.hpp"
|
||||||
#include <QToolButton>
|
#include "PanelHeader.hpp"
|
||||||
#include <QButtonGroup>
|
#include "panels/chart/GridDataChartView.hpp"
|
||||||
#include <QCheckBox>
|
#include "panels/chart/RawDataChartView.hpp"
|
||||||
#include "panels/chart/DatasetChartView.hpp"
|
|
||||||
#include "panels/AnomalyTablePanel.hpp"
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
DatasetDetailPage::DatasetDetailPage(QWidget* parent) : QWidget(parent) {
|
DatasetDetailPage::DatasetDetailPage(QWidget* parent) : QWidget(parent) {
|
||||||
auto* lay = new QVBoxLayout(this);
|
auto* lay = new QVBoxLayout(this);
|
||||||
auto* bar = new QHBoxLayout();
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
auto* origin = new QToolButton(this); origin->setText("原数据"); origin->setCheckable(true);
|
lay->setSpacing(0);
|
||||||
auto* grid = new QToolButton(this); grid->setText("网格数据"); grid->setCheckable(true);
|
|
||||||
grid->setChecked(true);
|
|
||||||
auto* grp = new QButtonGroup(this); grp->setExclusive(true); grp->addButton(origin); grp->addButton(grid);
|
|
||||||
auto* showAnom = new QCheckBox("显示异常", this); showAnom->setChecked(true);
|
|
||||||
auto* showLines = new QCheckBox("显示等值线", this); showLines->setChecked(true);
|
|
||||||
bar->addWidget(origin); bar->addWidget(grid); bar->addStretch();
|
|
||||||
bar->addWidget(showAnom); bar->addWidget(showLines);
|
|
||||||
lay->addLayout(bar);
|
|
||||||
|
|
||||||
chart_ = new DatasetChartView(this);
|
rawView_ = new RawDataChartView(this);
|
||||||
anomalyTable_ = new AnomalyTablePanel(this);
|
gridView_ = new GridDataChartView(this);
|
||||||
lay->addWidget(chart_, 3);
|
|
||||||
lay->addWidget(anomalyTable_, 1);
|
|
||||||
|
|
||||||
connect(grid, &QToolButton::clicked, this, [this] { gridMode_ = true; showGridMode(); });
|
const QVector<PanelTab> tabs = {
|
||||||
connect(origin, &QToolButton::clicked, this, [this] { gridMode_ = false; showScatterMode(); });
|
{Glyph::Detail, QStringLiteral("原数据"), rawView_, false},
|
||||||
connect(showAnom, &QCheckBox::toggled, chart_, [this](bool on) { chart_->setShowAnomalies(on); });
|
{Glyph::Dataset, QStringLiteral("网格数据"), gridView_, false},
|
||||||
connect(showLines, &QCheckBox::toggled, chart_, [this](bool on) {
|
};
|
||||||
chart_->setShowContourLines(on); if (gridMode_) showGridMode(); });
|
const QVector<HeaderAction> actions = {
|
||||||
connect(anomalyTable_, &AnomalyTablePanel::hiddenChanged, chart_,
|
{Glyph::Download, QStringLiteral("导出")},
|
||||||
[this](const std::set<int>& h) { chart_->setHiddenAnomalies(h); });
|
};
|
||||||
|
|
||||||
|
auto tabbedPanel = buildTabbedPanel(tabs, actions);
|
||||||
|
lay->addWidget(tabbedPanel.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatasetDetailPage::setData(const geopro::controller::DatasetDetailController::ChartData& d) {
|
void DatasetDetailPage::setData(const geopro::controller::DatasetDetailController::ChartData& d) {
|
||||||
dsId_ = d.dsId; data_ = d;
|
dsId_ = d.dsId;
|
||||||
chart_->setAnomalies(d.anomalies);
|
rawView_->setData(d);
|
||||||
anomalyTable_->setAnomalies(d.anomalies, {}, {}); // 创建时间/备注可后续从 VO 补
|
gridView_->setData(d);
|
||||||
if (gridMode_) showGridMode(); else showScatterMode();
|
|
||||||
}
|
|
||||||
void DatasetDetailPage::showGridMode() {
|
|
||||||
geopro::render::ContourOptions opt; // 默认 2x+smooth+simplify0.5
|
|
||||||
chart_->showContour(data_.grid, data_.gridScale, opt);
|
|
||||||
}
|
|
||||||
void DatasetDetailPage::showScatterMode() {
|
|
||||||
chart_->showScatter(data_.scatter, data_.scatterScale);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include "DatasetDetailController.hpp" // ChartData
|
#include "DatasetDetailController.hpp"
|
||||||
namespace geopro::app {
|
|
||||||
class DatasetChartView; class AnomalyTablePanel;
|
|
||||||
|
|
||||||
// 单个数据集详情页:标题 + 原数据/网格数据 切换 + 叠加开关 + 图表 + 异常表。
|
namespace geopro::app {
|
||||||
|
|
||||||
|
class RawDataChartView;
|
||||||
|
class GridDataChartView;
|
||||||
|
|
||||||
|
// 单个数据集详情页:下划线页签「原数据 / 网格数据」+ 右侧「导出」操作。
|
||||||
|
// 内部分别由 RawDataChartView / GridDataChartView 实现各自三层布局。
|
||||||
class DatasetDetailPage : public QWidget {
|
class DatasetDetailPage : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
|
|
@ -12,11 +16,9 @@ public:
|
||||||
void setData(const geopro::controller::DatasetDetailController::ChartData& d);
|
void setData(const geopro::controller::DatasetDetailController::ChartData& d);
|
||||||
QString dsId() const { return dsId_; }
|
QString dsId() const { return dsId_; }
|
||||||
private:
|
private:
|
||||||
void showScatterMode(); void showGridMode();
|
|
||||||
QString dsId_;
|
QString dsId_;
|
||||||
geopro::controller::DatasetDetailController::ChartData data_;
|
RawDataChartView* rawView_;
|
||||||
DatasetChartView* chart_;
|
GridDataChartView* gridView_;
|
||||||
AnomalyTablePanel* anomalyTable_;
|
|
||||||
bool gridMode_ = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
#include "panels/DescriptionPanel.hpp"
|
||||||
|
|
||||||
|
#include <QTextEdit>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
DescriptionPanel::DescriptionPanel(QWidget* parent) : QWidget(parent) {
|
||||||
|
auto* lay = new QVBoxLayout(this);
|
||||||
|
lay->setContentsMargins(8, 8, 8, 8);
|
||||||
|
|
||||||
|
edit_ = new QTextEdit(this);
|
||||||
|
edit_->setReadOnly(true);
|
||||||
|
edit_->setPlaceholderText(QStringLiteral("暂无描述"));
|
||||||
|
lay->addWidget(edit_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DescriptionPanel::setText(const QString& text) {
|
||||||
|
edit_->setPlainText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class QTextEdit;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 数据集描述面板:只读文本,供网格数据底部页签「描述」使用。
|
||||||
|
class DescriptionPanel : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit DescriptionPanel(QWidget* parent = nullptr);
|
||||||
|
void setText(const QString& text);
|
||||||
|
private:
|
||||||
|
QTextEdit* edit_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -1,232 +0,0 @@
|
||||||
#include "panels/chart/DatasetChartView.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#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() 删除场景中的所有 item(含 anomalyItems_ 所指向的对象);
|
|
||||||
// anomalyItems_.clear() 随后清空向量中的悬空指针。
|
|
||||||
// 二者必须成对、顺序不可颠倒:若先 clear() 后仍持有指针,rebuildAnomalyItems
|
|
||||||
// 中的 delete 会对已释放对象 double-free;若先清向量、后继续操作旧指针同样 UB。
|
|
||||||
scene_->clear(); anomalyItems_.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DatasetChartView::showScatter(const geopro::core::ScatterField& f, const geopro::core::ColorScale& cs) {
|
|
||||||
clearChart();
|
|
||||||
const double sz = 0.6; // 方块边长(数据单位,近似 web 方点)
|
|
||||||
const size_t n = std::min({f.x.size(), f.y.size(), f.v.size()});
|
|
||||||
for (size_t i = 0; i < n; ++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.empty()) continue;
|
|
||||||
if (static_cast<int>(a.markType) == 1) { // 点:小方块标记
|
|
||||||
const double s = 0.8;
|
|
||||||
auto* dot = scene_->addRect(a.localPts[0].x - s/2, a.localPts[0].y - s/2, s, s,
|
|
||||||
QPen(QColor(QString::fromStdString(a.lineColor)), 0),
|
|
||||||
QBrush(QColor(QString::fromStdString(a.lineColor))));
|
|
||||||
dot->setZValue(2);
|
|
||||||
anomalyItems_.push_back(dot);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
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
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
#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
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
#include "panels/chart/GridDataChartView.hpp"
|
||||||
|
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QSlider>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "PanelHeader.hpp"
|
||||||
|
#include "panels/AnomalyTablePanel.hpp"
|
||||||
|
#include "panels/DescriptionPanel.hpp"
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
GridDataChartView::GridDataChartView(QWidget* parent) : QWidget(parent) {
|
||||||
|
auto* lay = new QVBoxLayout(this);
|
||||||
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
lay->setSpacing(0);
|
||||||
|
|
||||||
|
// ---- 工具条 ----
|
||||||
|
auto* toolbar = new QWidget(this);
|
||||||
|
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* btnWhiten = new QToolButton(toolbar);
|
||||||
|
btnWhiten->setText(QStringLiteral("白化"));
|
||||||
|
|
||||||
|
auto* btnFilter = new QToolButton(toolbar);
|
||||||
|
btnFilter->setText(QStringLiteral("滤波处理"));
|
||||||
|
|
||||||
|
auto* chkShowAnom = new QCheckBox(QStringLiteral("显示异常"), toolbar);
|
||||||
|
chkShowAnom->setChecked(true);
|
||||||
|
|
||||||
|
auto* chkShowContourLabel = new QCheckBox(QStringLiteral("显示等值线标注"), toolbar);
|
||||||
|
chkShowContourLabel->setChecked(true);
|
||||||
|
|
||||||
|
auto* chkContourTip = new QCheckBox(QStringLiteral("显示等值线提示信息"), toolbar);
|
||||||
|
chkContourTip->setChecked(false);
|
||||||
|
|
||||||
|
auto* lblSimplify = new QLabel(QStringLiteral("简化容差:"), toolbar);
|
||||||
|
|
||||||
|
simplifySlider_ = new QSlider(Qt::Horizontal, toolbar);
|
||||||
|
simplifySlider_->setRange(0, 100);
|
||||||
|
simplifySlider_->setValue(50);
|
||||||
|
simplifySlider_->setFixedWidth(80);
|
||||||
|
|
||||||
|
simplifyValueLabel_ = new QLabel(QStringLiteral("0.5"), toolbar);
|
||||||
|
simplifyValueLabel_->setFixedWidth(28);
|
||||||
|
|
||||||
|
connect(simplifySlider_, &QSlider::valueChanged, this, [this](int v) {
|
||||||
|
simplifyValueLabel_->setText(QString::number(v / 100.0, 'f', 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
auto* btnAnomalyLabel = new QToolButton(toolbar);
|
||||||
|
btnAnomalyLabel->setText(QStringLiteral("异常标注"));
|
||||||
|
|
||||||
|
auto* btnAutoLabel = new QToolButton(toolbar);
|
||||||
|
btnAutoLabel->setText(QStringLiteral("自动标注"));
|
||||||
|
|
||||||
|
auto* btnSaveAs = new QToolButton(toolbar);
|
||||||
|
btnSaveAs->setText(QStringLiteral("另存为"));
|
||||||
|
|
||||||
|
tbLay->addWidget(btnGrid);
|
||||||
|
tbLay->addWidget(btnColorScale);
|
||||||
|
tbLay->addWidget(btnWhiten);
|
||||||
|
tbLay->addWidget(btnFilter);
|
||||||
|
tbLay->addWidget(chkShowAnom);
|
||||||
|
tbLay->addWidget(chkShowContourLabel);
|
||||||
|
tbLay->addWidget(chkContourTip);
|
||||||
|
tbLay->addWidget(lblSimplify);
|
||||||
|
tbLay->addWidget(simplifySlider_);
|
||||||
|
tbLay->addWidget(simplifyValueLabel_);
|
||||||
|
tbLay->addWidget(btnAnomalyLabel);
|
||||||
|
tbLay->addWidget(btnAutoLabel);
|
||||||
|
tbLay->addWidget(btnSaveAs);
|
||||||
|
tbLay->addStretch();
|
||||||
|
|
||||||
|
lay->addWidget(toolbar);
|
||||||
|
|
||||||
|
// ---- 图表占位区(stretch) ----
|
||||||
|
plotArea_ = new QWidget(this);
|
||||||
|
plotArea_->setObjectName(QStringLiteral("gridPlotArea"));
|
||||||
|
lay->addWidget(plotArea_, 1);
|
||||||
|
|
||||||
|
// ---- 色阶占位(固定高 36px) ----
|
||||||
|
auto* colorScaleBar = new QWidget(this);
|
||||||
|
colorScaleBar->setObjectName(QStringLiteral("gridColorScaleBar"));
|
||||||
|
colorScaleBar->setFixedHeight(36);
|
||||||
|
lay->addWidget(colorScaleBar);
|
||||||
|
|
||||||
|
// ---- 底部双页签(异常列表 / 描述),固定高约 200px ----
|
||||||
|
anomalyTable_ = new AnomalyTablePanel(this);
|
||||||
|
descriptionPanel_ = new DescriptionPanel(this);
|
||||||
|
|
||||||
|
const QVector<PanelTab> bottomTabs = {
|
||||||
|
{Glyph::Anomaly, QStringLiteral("异常列表"), anomalyTable_, true},
|
||||||
|
{Glyph::Property, QStringLiteral("描述"), descriptionPanel_, false},
|
||||||
|
};
|
||||||
|
auto tabbedBottom = buildTabbedPanel(bottomTabs, {});
|
||||||
|
|
||||||
|
auto* bottomContainer = tabbedBottom.container;
|
||||||
|
bottomContainer->setFixedHeight(200);
|
||||||
|
lay->addWidget(bottomContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridDataChartView::setData(const geopro::controller::DatasetDetailController::ChartData& d) {
|
||||||
|
data_ = d;
|
||||||
|
// 步骤1:只把 anomalies 喂给底部异常列表;图表区留空。
|
||||||
|
anomalyTable_->setAnomalies(d.anomalies, {}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QWidget>
|
||||||
|
#include "DatasetDetailController.hpp"
|
||||||
|
|
||||||
|
class QSlider;
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
class AnomalyTablePanel;
|
||||||
|
class DescriptionPanel;
|
||||||
|
|
||||||
|
// 网格数据图表视图(步骤1骨架):工具条 + 图表占位 + 色阶占位 + 底部双页签(异常列表/描述)。
|
||||||
|
// 图表渲染由后续步骤填入 plotArea()。
|
||||||
|
class GridDataChartView : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit GridDataChartView(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// 步骤1:把 anomalies 喂给底部异常列表;图表区留空。
|
||||||
|
void setData(const geopro::controller::DatasetDetailController::ChartData& d);
|
||||||
|
|
||||||
|
// 供后续步骤填充 Qwt 画布的占位区域。
|
||||||
|
QWidget* plotArea() const { return plotArea_; }
|
||||||
|
|
||||||
|
// 供后续步骤联动异常叠加。
|
||||||
|
AnomalyTablePanel* anomalyTable() const { return anomalyTable_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
geopro::controller::DatasetDetailController::ChartData data_;
|
||||||
|
QWidget* plotArea_;
|
||||||
|
AnomalyTablePanel* anomalyTable_;
|
||||||
|
DescriptionPanel* descriptionPanel_;
|
||||||
|
QSlider* simplifySlider_;
|
||||||
|
QLabel* simplifyValueLabel_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
#include "panels/chart/RawDataChartView.hpp"
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) {
|
||||||
|
auto* lay = new QVBoxLayout(this);
|
||||||
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
lay->setSpacing(0);
|
||||||
|
|
||||||
|
// ---- 工具条 ----
|
||||||
|
auto* toolbar = new QWidget(this);
|
||||||
|
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 QComboBox(toolbar);
|
||||||
|
chartTypeCombo_->addItem(QStringLiteral("散点图"));
|
||||||
|
|
||||||
|
auto* btnSaveAs = new QToolButton(toolbar);
|
||||||
|
btnSaveAs->setText(QStringLiteral("另存为"));
|
||||||
|
|
||||||
|
tbLay->addWidget(btnGrid);
|
||||||
|
tbLay->addWidget(btnColorScale);
|
||||||
|
tbLay->addWidget(lblCurrentChart);
|
||||||
|
tbLay->addWidget(chartTypeCombo_);
|
||||||
|
tbLay->addWidget(btnSaveAs);
|
||||||
|
tbLay->addStretch();
|
||||||
|
|
||||||
|
lay->addWidget(toolbar);
|
||||||
|
|
||||||
|
// ---- 图表占位区(stretch) ----
|
||||||
|
plotArea_ = new QWidget(this);
|
||||||
|
plotArea_->setObjectName(QStringLiteral("rawPlotArea"));
|
||||||
|
lay->addWidget(plotArea_, 1);
|
||||||
|
|
||||||
|
// ---- 色阶占位(固定高 36px) ----
|
||||||
|
auto* colorScaleBar = new QWidget(this);
|
||||||
|
colorScaleBar->setObjectName(QStringLiteral("rawColorScaleBar"));
|
||||||
|
colorScaleBar->setFixedHeight(36);
|
||||||
|
lay->addWidget(colorScaleBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RawDataChartView::setData(const geopro::controller::DatasetDetailController::ChartData& d) {
|
||||||
|
// 步骤1:保存数据,图表渲染留给后续步骤。
|
||||||
|
data_ = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QWidget>
|
||||||
|
#include "DatasetDetailController.hpp"
|
||||||
|
|
||||||
|
class QComboBox;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 原数据图表视图(步骤1骨架):工具条 + 图表占位 + 色阶占位。
|
||||||
|
// 图表渲染由后续步骤填入 plotArea()。
|
||||||
|
class RawDataChartView : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit RawDataChartView(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// 步骤1:保存数据备后续渲染步骤使用;图表区留空。
|
||||||
|
void setData(const geopro::controller::DatasetDetailController::ChartData& d);
|
||||||
|
|
||||||
|
// 供后续步骤填充 Qwt 画布的占位区域。
|
||||||
|
QWidget* plotArea() const { return plotArea_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
geopro::controller::DatasetDetailController::ChartData data_;
|
||||||
|
QWidget* plotArea_;
|
||||||
|
QComboBox* chartTypeCombo_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
Loading…
Reference in New Issue