From 78f96dbc081aa26d9e1e8179ee1797b980ad969b Mon Sep 17 00:00:00 2001 From: gaozheng Date: Thu, 11 Jun 2026 18:59:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(review):=20=E4=BF=AE=20cpp-review=20HIGH/ME?= =?UTF-8?q?DIUM=20=E2=80=94=E6=95=A3=E7=82=B9ys=E8=B6=8A=E7=95=8C/colorSvc?= =?UTF-8?q?=E6=9E=90=E6=9E=84=E6=B3=84=E6=BC=8F/QwtPlot=20autoDelete?= =?UTF-8?q?=E6=B3=A8=E9=87=8A/=E6=8E=A7=E5=88=B6=E5=99=A8catch(...)?= =?UTF-8?q?=E9=98=B2busy=E6=AD=BB=E9=94=81=20+=20=E6=B8=85=E6=AD=BB?= =?UTF-8?q?=E4=BB=A3=E7=A0=81simplifyInPlace/simplifyTol=20+=20=E5=A1=AB?= =?UTF-8?q?=E5=85=85=E7=AD=89=E6=AF=94=E9=99=90=E5=B9=85=20+=20DTO?= =?UTF-8?q?=E8=A1=8C=E6=95=B0=E6=A0=A1=E9=AA=8C+=E6=9E=9A=E4=B8=BE?= =?UTF-8?q?=E9=92=B3=E5=88=B6=20+=20ContourLine.level=E9=BB=98=E8=AE=A4NaN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/panels/chart/ContourPlotItem.cpp | 10 +++++++--- src/app/panels/chart/GridDataChartView.cpp | 8 +++++++- src/app/panels/chart/GridDataChartView.hpp | 1 + src/app/panels/chart/RawDataChartView.cpp | 9 +++++++-- src/app/panels/chart/RawDataChartView.hpp | 1 + src/app/panels/chart/ScatterPlotItem.cpp | 3 ++- src/controller/DatasetDetailController.cpp | 6 ++++++ src/data/dto/DatasetChartDto.cpp | 7 ++++++- src/render/ContourBands.cpp | 18 ------------------ src/render/ContourBands.hpp | 4 ++-- tests/render/test_contour_bands.cpp | 10 +++++----- 11 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/app/panels/chart/ContourPlotItem.cpp b/src/app/panels/chart/ContourPlotItem.cpp index dcc4091..663bd61 100644 --- a/src/app/panels/chart/ContourPlotItem.cpp +++ b/src/app/panels/chart/ContourPlotItem.cpp @@ -117,9 +117,13 @@ void ContourPlotItem::buildFillImage(const core::Grid& g, ColorMapService* svc) const int nx = g.nx(), ny = g.ny(); int W = (nx - 1) * kFillUpsample + 1; int H = (ny - 1) * kFillUpsample + 1; - // 限幅:极端网格下按比例降采样,保证内存/性能可控。 - if (W > kMaxFillDim) W = kMaxFillDim; - if (H > kMaxFillDim) H = kMaxFillDim; + // 限幅:极端网格下按**统一比例**降采样(W/H 独立截断会使采样非均匀→内容横/纵失真)。 + if (W > kMaxFillDim || H > kMaxFillDim) { + const double s = std::min(static_cast(kMaxFillDim) / W, + static_cast(kMaxFillDim) / H); + W = std::max(2, static_cast(W * s)); + H = std::max(2, static_cast(H * s)); + } QImage img(W, H, QImage::Format_ARGB32); img.fill(Qt::transparent); diff --git a/src/app/panels/chart/GridDataChartView.cpp b/src/app/panels/chart/GridDataChartView.cpp index 31c5046..c41eaa4 100644 --- a/src/app/panels/chart/GridDataChartView.cpp +++ b/src/app/panels/chart/GridDataChartView.cpp @@ -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) { data_ = d; // 开页:仅把 anomalies 喂给底部异常列表;图表区待网格数据懒加载后填充。 @@ -201,7 +206,8 @@ void GridDataChartView::setGridData(const geopro::core::Grid& grid, void GridDataChartView::rebuildContour() { if (!hasGrid_ || !colorSvc_) return; - // 卸载旧项(QwtPlot 不拥有 item)。 + // 卸载旧项:QwtPlot 默认 autoDelete=true(析构时 delete 仍在 dict 的 item)。 + // 必须先 detach()(从 dict 移除)再 delete,否则 QwtPlot 析构时会 double-free。 if (contourItem_) { contourItem_->detach(); delete contourItem_; diff --git a/src/app/panels/chart/GridDataChartView.hpp b/src/app/panels/chart/GridDataChartView.hpp index 75d9898..cf0acf7 100644 --- a/src/app/panels/chart/GridDataChartView.hpp +++ b/src/app/panels/chart/GridDataChartView.hpp @@ -28,6 +28,7 @@ class GridDataChartView : public QWidget { Q_OBJECT public: explicit GridDataChartView(QWidget* parent = nullptr); + ~GridDataChartView() override; // 开页时调用:仅喂底部异常列表(网格数据随网格页激活懒加载)。 void setData(const geopro::controller::DatasetDetailController::ChartData& d); diff --git a/src/app/panels/chart/RawDataChartView.cpp b/src/app/panels/chart/RawDataChartView.cpp index 20aae00..5840f61 100644 --- a/src/app/panels/chart/RawDataChartView.cpp +++ b/src/app/panels/chart/RawDataChartView.cpp @@ -115,6 +115,11 @@ RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) { lay->addWidget(colorBar_); } +RawDataChartView::~RawDataChartView() { + // colorSvc_ 非 QObject、无 parent,需手动释放(plot_ 的 item 由 QwtPlot autoDelete 处理)。 + delete colorSvc_; +} + QWidget* RawDataChartView::plotArea() const { return plot_; } @@ -129,10 +134,10 @@ void RawDataChartView::setData( delete colorSvc_; colorSvc_ = new ColorMapService(d.scatterScale); - // 卸载旧散点项 + // 卸载旧散点项:QwtPlot 默认 autoDelete=true(析构时 delete 仍在 dict 的 item)。 + // 必须先 detach()(从 dict 移除)再 delete,否则 QwtPlot 析构时会 double-free。 if (scatterItem_) { scatterItem_->detach(); - // QwtPlot 不拥有 item,需要我们释放 delete scatterItem_; scatterItem_ = nullptr; } diff --git a/src/app/panels/chart/RawDataChartView.hpp b/src/app/panels/chart/RawDataChartView.hpp index 09db06c..0344849 100644 --- a/src/app/panels/chart/RawDataChartView.hpp +++ b/src/app/panels/chart/RawDataChartView.hpp @@ -17,6 +17,7 @@ class RawDataChartView : public QWidget { Q_OBJECT public: explicit RawDataChartView(QWidget* parent = nullptr); + ~RawDataChartView() override; void setData(const geopro::controller::DatasetDetailController::ChartData& d); diff --git a/src/app/panels/chart/ScatterPlotItem.cpp b/src/app/panels/chart/ScatterPlotItem.cpp index bda4d2d..bfd8ed1 100644 --- a/src/app/panels/chart/ScatterPlotItem.cpp +++ b/src/app/panels/chart/ScatterPlotItem.cpp @@ -50,7 +50,8 @@ void ScatterPlotItem::draw(QPainter* painter, const auto& xs = field_.x; const auto& ys = field_.y; 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; painter->save(); diff --git a/src/controller/DatasetDetailController.cpp b/src/controller/DatasetDetailController.cpp index ba07695..5a923b8 100644 --- a/src/controller/DatasetDetailController.cpp +++ b/src/controller/DatasetDetailController.cpp @@ -28,6 +28,9 @@ void DatasetDetailController::openDataset(const QString& dsId, const QString& dd } catch (const std::exception& e) { busy_ = false; 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) { busy_ = false; emit loadFailed(dsId, QString::fromStdString(e.what())); + } catch (...) { // 非 std 异常(VTK/Qt)也必须复位 busy_,否则永久拒绝后续加载 + busy_ = false; + emit loadFailed(dsId, QStringLiteral("未知错误")); } } diff --git a/src/data/dto/DatasetChartDto.cpp b/src/data/dto/DatasetChartDto.cpp index 6d20cdf..5cdf49c 100644 --- a/src/data/dto/DatasetChartDto.cpp +++ b/src/data/dto/DatasetChartDto.cpp @@ -1,6 +1,7 @@ #include "dto/DatasetChartDto.hpp" #include #include +#include namespace geopro::data::dto { using namespace geopro::core; @@ -18,6 +19,8 @@ Grid parseInversionGrid(const QJsonObject& data) { Grid g(nx < 1 ? 1 : nx, ny < 1 ? 1 : ny); 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)); + 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) { const QJsonArray row = v.at(j).toArray(); for (int i = 0; i < nx; ++i) { @@ -58,7 +61,9 @@ std::vector parseDatasetAnomalies(const QJsonArray& arr) { Anomaly a; a.name = o.value("exceptionName").toString().toStdString(); a.typeName = o.value("exceptionTypeName").toString().toStdString(); - a.markType = static_cast(o.value("exceptionMarkType").toInt(2)); + const int mt = o.value("exceptionMarkType").toInt(2); // 1=点 2=线 3=面 + a.markType = (mt >= 1 && mt <= 3) ? static_cast(mt) + : AnomalyMarkType::Polyline; // 越界值兜底为线 const QJsonObject lg = o.value("legend").toObject(); a.lineColor = lg.value("polylineColor").toString("#000000").toStdString(); a.lineWidth = lg.value("polylineWidth").toDouble(1.0); diff --git a/src/render/ContourBands.cpp b/src/render/ContourBands.cpp index 7a0d6e2..872d3a5 100644 --- a/src/render/ContourBands.cpp +++ b/src/render/ContourBands.cpp @@ -98,24 +98,6 @@ vtkSmartPointer toCellGrid(const Grid& g) { return ug; } -// Douglas-Peucker 等值线简化(就地)。 -void simplifyInPlace(std::vector& pts, double tol) { - if (tol <= 0 || pts.size() < 3) return; - std::vector keep(pts.size(), 0); keep.front() = keep.back() = 1; - std::vector> 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 r; for (size_t k = 0; k < pts.size(); ++k) if (keep[k]) r.push_back(pts[k]); - pts.swap(r); -} - } // namespace ContourBandsResult buildContourBands(const Grid& g, const ColorScale& cs, const ContourOptions& opt) { diff --git a/src/render/ContourBands.hpp b/src/render/ContourBands.hpp index 6b6a642..188d4e0 100644 --- a/src/render/ContourBands.hpp +++ b/src/render/ContourBands.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include "model/Field.hpp" #include "model/ColorScale.hpp" @@ -10,7 +11,7 @@ struct BandPolygon { std::vector ring; // 单环多边形(局部坐标,y=高程向上为正) }; struct ContourLine { - double level; + double level = std::numeric_limits::quiet_NaN(); // NaN=未解析(与真实 level 0 区分) std::vector pts; }; struct ContourBandsResult { @@ -20,7 +21,6 @@ struct ContourBandsResult { struct ContourOptions { int upsample = 2; // 双线性上采样倍数(1=不采样) double smooth = 0.3; // 平滑强度 0..1(0=不平滑) - double simplifyTol = 0.5; // 等值线简化容差(数据单位,0=不简化) bool makeLines = true; }; diff --git a/tests/render/test_contour_bands.cpp b/tests/render/test_contour_bands.cpp index b87998b..37996c4 100644 --- a/tests/render/test_contour_bands.cpp +++ b/tests/render/test_contour_bands.cpp @@ -12,8 +12,8 @@ TEST(ContourBands, UpsampleIncreasesDetail) { g.vmin = 0; g.vmax = 6; 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}); - ContourOptions a; a.upsample = 1; a.smooth = 0; a.simplifyTol = 0; - ContourOptions b; b.upsample = 2; b.smooth = 0; b.simplifyTol = 0; + ContourOptions a; a.upsample = 1; a.smooth = 0; + ContourOptions b; b.upsample = 2; b.smooth = 0; auto ra = buildContourBands(g, cs, a); auto rb = buildContourBands(g, cs, b); EXPECT_GT(rb.bands.size(), ra.bands.size()); @@ -27,7 +27,7 @@ TEST(ContourBands, ClipsNaNRegion) { g.valueAt(2, 2) = std::nan(""); // 右上角无效 g.vmin = 0; g.vmax = 10; 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); bool coversCorner = false; for (const auto& b : r.bands) @@ -48,7 +48,7 @@ TEST(ContourBands, ProducesNonEmptyBands) { cs.addStop(2.0, Rgba{0, 255, 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); ASSERT_FALSE(r.bands.empty()); for (const auto& b : r.bands) EXPECT_GE(b.ring.size(), 3u); @@ -65,7 +65,7 @@ TEST(ContourBands, ProducesContourLines) { ColorScale cs; 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}); - 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); EXPECT_FALSE(r.lines.empty()); }