feat/dataset-detail-chart #5

Merged
gaozheng merged 74 commits from feat/dataset-detail-chart into main 2026-06-13 17:30:37 +08:00
11 changed files with 44 additions and 33 deletions
Showing only changes of commit 78f96dbc08 - Show all commits

View File

@ -117,9 +117,13 @@ void ContourPlotItem::buildFillImage(const core::Grid& g, ColorMapService* svc)
const int nx = g.nx(), ny = g.ny(); const int nx = g.nx(), ny = g.ny();
int W = (nx - 1) * kFillUpsample + 1; int W = (nx - 1) * kFillUpsample + 1;
int H = (ny - 1) * kFillUpsample + 1; int H = (ny - 1) * kFillUpsample + 1;
// 限幅:极端网格下按比例降采样,保证内存/性能可控。 // 限幅:极端网格下按**统一比例**降采样W/H 独立截断会使采样非均匀→内容横/纵失真)。
if (W > kMaxFillDim) W = kMaxFillDim; if (W > kMaxFillDim || H > kMaxFillDim) {
if (H > kMaxFillDim) H = kMaxFillDim; const double s = std::min(static_cast<double>(kMaxFillDim) / W,
static_cast<double>(kMaxFillDim) / H);
W = std::max(2, static_cast<int>(W * s));
H = std::max(2, static_cast<int>(H * s));
}
QImage img(W, H, QImage::Format_ARGB32); QImage img(W, H, QImage::Format_ARGB32);
img.fill(Qt::transparent); img.fill(Qt::transparent);

View File

@ -173,6 +173,11 @@ GridDataChartView::GridDataChartView(QWidget* parent) : QWidget(parent) {
}); });
} }
GridDataChartView::~GridDataChartView() {
// colorSvc_ 非 QObject、无 parent需手动释放contourItem_ 由 QwtPlot autoDelete 处理)。
delete colorSvc_;
}
void GridDataChartView::setData(const geopro::controller::DatasetDetailController::ChartData& d) { void GridDataChartView::setData(const geopro::controller::DatasetDetailController::ChartData& d) {
data_ = d; data_ = d;
// 开页:仅把 anomalies 喂给底部异常列表;图表区待网格数据懒加载后填充。 // 开页:仅把 anomalies 喂给底部异常列表;图表区待网格数据懒加载后填充。
@ -201,7 +206,8 @@ void GridDataChartView::setGridData(const geopro::core::Grid& grid,
void GridDataChartView::rebuildContour() { void GridDataChartView::rebuildContour() {
if (!hasGrid_ || !colorSvc_) return; if (!hasGrid_ || !colorSvc_) return;
// 卸载旧项QwtPlot 不拥有 item // 卸载旧项QwtPlot 默认 autoDelete=true析构时 delete 仍在 dict 的 item
// 必须先 detach()(从 dict 移除)再 delete否则 QwtPlot 析构时会 double-free。
if (contourItem_) { if (contourItem_) {
contourItem_->detach(); contourItem_->detach();
delete contourItem_; delete contourItem_;

View File

@ -28,6 +28,7 @@ class GridDataChartView : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
explicit GridDataChartView(QWidget* parent = nullptr); explicit GridDataChartView(QWidget* parent = nullptr);
~GridDataChartView() override;
// 开页时调用:仅喂底部异常列表(网格数据随网格页激活懒加载)。 // 开页时调用:仅喂底部异常列表(网格数据随网格页激活懒加载)。
void setData(const geopro::controller::DatasetDetailController::ChartData& d); void setData(const geopro::controller::DatasetDetailController::ChartData& d);

View File

@ -115,6 +115,11 @@ RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) {
lay->addWidget(colorBar_); lay->addWidget(colorBar_);
} }
RawDataChartView::~RawDataChartView() {
// colorSvc_ 非 QObject、无 parent需手动释放plot_ 的 item 由 QwtPlot autoDelete 处理)。
delete colorSvc_;
}
QWidget* RawDataChartView::plotArea() const { QWidget* RawDataChartView::plotArea() const {
return plot_; return plot_;
} }
@ -129,10 +134,10 @@ void RawDataChartView::setData(
delete colorSvc_; delete colorSvc_;
colorSvc_ = new ColorMapService(d.scatterScale); colorSvc_ = new ColorMapService(d.scatterScale);
// 卸载旧散点项 // 卸载旧散点项QwtPlot 默认 autoDelete=true析构时 delete 仍在 dict 的 item
// 必须先 detach()(从 dict 移除)再 delete否则 QwtPlot 析构时会 double-free。
if (scatterItem_) { if (scatterItem_) {
scatterItem_->detach(); scatterItem_->detach();
// QwtPlot 不拥有 item需要我们释放
delete scatterItem_; delete scatterItem_;
scatterItem_ = nullptr; scatterItem_ = nullptr;
} }

View File

@ -17,6 +17,7 @@ class RawDataChartView : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
explicit RawDataChartView(QWidget* parent = nullptr); explicit RawDataChartView(QWidget* parent = nullptr);
~RawDataChartView() override;
void setData(const geopro::controller::DatasetDetailController::ChartData& d); void setData(const geopro::controller::DatasetDetailController::ChartData& d);

View File

@ -50,7 +50,8 @@ void ScatterPlotItem::draw(QPainter* painter,
const auto& xs = field_.x; const auto& xs = field_.x;
const auto& ys = field_.y; const auto& ys = field_.y;
const auto& vs = field_.v; const auto& vs = field_.v;
const std::size_t n = xs.size(); // x/y/v 来自服务端独立数组,长度可能不一致——取 min 防越界。
const std::size_t n = std::min(xs.size(), ys.size());
if (n == 0) return; if (n == 0) return;
painter->save(); painter->save();

View File

@ -28,6 +28,9 @@ void DatasetDetailController::openDataset(const QString& dsId, const QString& dd
} catch (const std::exception& e) { } catch (const std::exception& e) {
busy_ = false; busy_ = false;
emit loadFailed(dsId, QString::fromStdString(e.what())); emit loadFailed(dsId, QString::fromStdString(e.what()));
} catch (...) { // 非 std 异常(VTK/Qt)也必须复位 busy_否则永久拒绝后续加载
busy_ = false;
emit loadFailed(dsId, QStringLiteral("未知错误"));
} }
} }
@ -48,6 +51,9 @@ void DatasetDetailController::loadGridData(const QString& dsId, const QString& d
} catch (const std::exception& e) { } catch (const std::exception& e) {
busy_ = false; busy_ = false;
emit loadFailed(dsId, QString::fromStdString(e.what())); emit loadFailed(dsId, QString::fromStdString(e.what()));
} catch (...) { // 非 std 异常(VTK/Qt)也必须复位 busy_否则永久拒绝后续加载
busy_ = false;
emit loadFailed(dsId, QStringLiteral("未知错误"));
} }
} }

View File

@ -1,6 +1,7 @@
#include "dto/DatasetChartDto.hpp" #include "dto/DatasetChartDto.hpp"
#include <cmath> #include <cmath>
#include <QString> #include <QString>
#include <QtGlobal>
namespace geopro::data::dto { namespace geopro::data::dto {
using namespace geopro::core; using namespace geopro::core;
@ -18,6 +19,8 @@ Grid parseInversionGrid(const QJsonObject& data) {
Grid g(nx < 1 ? 1 : nx, ny < 1 ? 1 : ny); Grid g(nx < 1 ? 1 : nx, ny < 1 ? 1 : ny);
g.x.clear(); for (auto e : x) g.x.push_back(num(e)); g.x.clear(); for (auto e : x) g.x.push_back(num(e));
g.y.clear(); for (auto e : y) g.y.push_back(num(e)); g.y.clear(); for (auto e : y) g.y.push_back(num(e));
if (v.size() != ny) // 服务端 v 行数与 y 不符:下方越界处填 NaN记录便于排查非静默
qWarning("parseInversionGrid: v rows=%d != ny=%d (缺失行将填 NaN)", v.size(), ny);
for (int j = 0; j < ny; ++j) { for (int j = 0; j < ny; ++j) {
const QJsonArray row = v.at(j).toArray(); const QJsonArray row = v.at(j).toArray();
for (int i = 0; i < nx; ++i) { for (int i = 0; i < nx; ++i) {
@ -58,7 +61,9 @@ std::vector<Anomaly> parseDatasetAnomalies(const QJsonArray& arr) {
Anomaly a; Anomaly a;
a.name = o.value("exceptionName").toString().toStdString(); a.name = o.value("exceptionName").toString().toStdString();
a.typeName = o.value("exceptionTypeName").toString().toStdString(); a.typeName = o.value("exceptionTypeName").toString().toStdString();
a.markType = static_cast<AnomalyMarkType>(o.value("exceptionMarkType").toInt(2)); const int mt = o.value("exceptionMarkType").toInt(2); // 1=点 2=线 3=面
a.markType = (mt >= 1 && mt <= 3) ? static_cast<AnomalyMarkType>(mt)
: AnomalyMarkType::Polyline; // 越界值兜底为线
const QJsonObject lg = o.value("legend").toObject(); const QJsonObject lg = o.value("legend").toObject();
a.lineColor = lg.value("polylineColor").toString("#000000").toStdString(); a.lineColor = lg.value("polylineColor").toString("#000000").toStdString();
a.lineWidth = lg.value("polylineWidth").toDouble(1.0); a.lineWidth = lg.value("polylineWidth").toDouble(1.0);

View File

@ -98,24 +98,6 @@ vtkSmartPointer<vtkUnstructuredGrid> toCellGrid(const Grid& g) {
return ug; return ug;
} }
// Douglas-Peucker 等值线简化(就地)。
void simplifyInPlace(std::vector<Vec2>& pts, double tol) {
if (tol <= 0 || pts.size() < 3) return;
std::vector<char> keep(pts.size(), 0); keep.front() = keep.back() = 1;
std::vector<std::pair<int,int>> st{{0, int(pts.size()) - 1}};
auto dist = [](const Vec2& p, const Vec2& a, const Vec2& b) {
double dx = b.x - a.x, dy = b.y - a.y, L = std::hypot(dx, dy);
if (L < 1e-12) return std::hypot(p.x - a.x, p.y - a.y);
return std::fabs((p.x - a.x) * dy - (p.y - a.y) * dx) / L; };
while (!st.empty()) { auto [s, e] = st.back(); st.pop_back();
double dmax = 0; int idx = -1;
for (int k = s + 1; k < e; ++k) { double d = dist(pts[k], pts[s], pts[e]);
if (d > dmax) { dmax = d; idx = k; } }
if (idx >= 0 && dmax > tol) { keep[idx] = 1; st.push_back({s, idx}); st.push_back({idx, e}); } }
std::vector<Vec2> r; for (size_t k = 0; k < pts.size(); ++k) if (keep[k]) r.push_back(pts[k]);
pts.swap(r);
}
} // namespace } // namespace
ContourBandsResult buildContourBands(const Grid& g, const ColorScale& cs, const ContourOptions& opt) { ContourBandsResult buildContourBands(const Grid& g, const ColorScale& cs, const ContourOptions& opt) {

View File

@ -1,4 +1,5 @@
#pragma once #pragma once
#include <limits>
#include <vector> #include <vector>
#include "model/Field.hpp" #include "model/Field.hpp"
#include "model/ColorScale.hpp" #include "model/ColorScale.hpp"
@ -10,7 +11,7 @@ struct BandPolygon {
std::vector<geopro::core::Vec2> ring; // 单环多边形局部坐标y=高程向上为正) std::vector<geopro::core::Vec2> ring; // 单环多边形局部坐标y=高程向上为正)
}; };
struct ContourLine { struct ContourLine {
double level; double level = std::numeric_limits<double>::quiet_NaN(); // NaN=未解析(与真实 level 0 区分)
std::vector<geopro::core::Vec2> pts; std::vector<geopro::core::Vec2> pts;
}; };
struct ContourBandsResult { struct ContourBandsResult {
@ -20,7 +21,6 @@ struct ContourBandsResult {
struct ContourOptions { struct ContourOptions {
int upsample = 2; // 双线性上采样倍数1=不采样) int upsample = 2; // 双线性上采样倍数1=不采样)
double smooth = 0.3; // 平滑强度 0..10=不平滑) double smooth = 0.3; // 平滑强度 0..10=不平滑)
double simplifyTol = 0.5; // 等值线简化容差数据单位0=不简化)
bool makeLines = true; bool makeLines = true;
}; };

View File

@ -12,8 +12,8 @@ TEST(ContourBands, UpsampleIncreasesDetail) {
g.vmin = 0; g.vmax = 6; g.vmin = 0; g.vmax = 6;
ColorScale cs; ColorScale cs;
cs.addStop(0, Rgba{0,0,255,255}); cs.addStop(3, Rgba{0,255,0,255}); cs.addStop(6, Rgba{255,0,0,255}); cs.addStop(0, Rgba{0,0,255,255}); cs.addStop(3, Rgba{0,255,0,255}); cs.addStop(6, Rgba{255,0,0,255});
ContourOptions a; a.upsample = 1; a.smooth = 0; a.simplifyTol = 0; ContourOptions a; a.upsample = 1; a.smooth = 0;
ContourOptions b; b.upsample = 2; b.smooth = 0; b.simplifyTol = 0; ContourOptions b; b.upsample = 2; b.smooth = 0;
auto ra = buildContourBands(g, cs, a); auto ra = buildContourBands(g, cs, a);
auto rb = buildContourBands(g, cs, b); auto rb = buildContourBands(g, cs, b);
EXPECT_GT(rb.bands.size(), ra.bands.size()); EXPECT_GT(rb.bands.size(), ra.bands.size());
@ -27,7 +27,7 @@ TEST(ContourBands, ClipsNaNRegion) {
g.valueAt(2, 2) = std::nan(""); // 右上角无效 g.valueAt(2, 2) = std::nan(""); // 右上角无效
g.vmin = 0; g.vmax = 10; g.vmin = 0; g.vmax = 10;
ColorScale cs; cs.addStop(0, Rgba{0,0,255,255}); cs.addStop(10, Rgba{255,0,0,255}); ColorScale cs; cs.addStop(0, Rgba{0,0,255,255}); cs.addStop(10, Rgba{255,0,0,255});
ContourOptions opt; opt.upsample = 1; opt.smooth = 0; opt.simplifyTol = 0; ContourOptions opt; opt.upsample = 1; opt.smooth = 0;
auto r = buildContourBands(g, cs, opt); auto r = buildContourBands(g, cs, opt);
bool coversCorner = false; bool coversCorner = false;
for (const auto& b : r.bands) for (const auto& b : r.bands)
@ -48,7 +48,7 @@ TEST(ContourBands, ProducesNonEmptyBands) {
cs.addStop(2.0, Rgba{0, 255, 0, 255}); cs.addStop(2.0, Rgba{0, 255, 0, 255});
cs.addStop(4.0, Rgba{255, 0, 0, 255}); cs.addStop(4.0, Rgba{255, 0, 0, 255});
ContourOptions opt; opt.upsample = 1; opt.smooth = 0; opt.simplifyTol = 0; ContourOptions opt; opt.upsample = 1; opt.smooth = 0;
auto r = buildContourBands(g, cs, opt); auto r = buildContourBands(g, cs, opt);
ASSERT_FALSE(r.bands.empty()); ASSERT_FALSE(r.bands.empty());
for (const auto& b : r.bands) EXPECT_GE(b.ring.size(), 3u); for (const auto& b : r.bands) EXPECT_GE(b.ring.size(), 3u);
@ -65,7 +65,7 @@ TEST(ContourBands, ProducesContourLines) {
ColorScale cs; ColorScale cs;
cs.addStop(0, Rgba{0,0,255,255}); cs.addStop(6, Rgba{0,255,0,255}); cs.addStop(0, Rgba{0,0,255,255}); cs.addStop(6, Rgba{0,255,0,255});
cs.addStop(12, Rgba{255,255,0,255}); cs.addStop(18, Rgba{255,0,0,255}); cs.addStop(12, Rgba{255,255,0,255}); cs.addStop(18, Rgba{255,0,0,255});
ContourOptions opt; opt.upsample = 1; opt.smooth = 0; opt.simplifyTol = 0; opt.makeLines = true; ContourOptions opt; opt.upsample = 1; opt.smooth = 0; opt.makeLines = true;
auto r = buildContourBands(g, cs, opt); auto r = buildContourBands(g, cs, opt);
EXPECT_FALSE(r.lines.empty()); EXPECT_FALSE(r.lines.empty());
} }