fix(review): 修 cpp-review HIGH/MEDIUM —散点ys越界/colorSvc析构泄漏/QwtPlot autoDelete注释/控制器catch(...)防busy死锁 + 清死代码simplifyInPlace/simplifyTol + 填充等比限幅 + DTO行数校验+枚举钳制 + ContourLine.level默认NaN

This commit is contained in:
gaozheng 2026-06-11 18:59:19 +08:00
parent 57c14ae8b4
commit 78f96dbc08
11 changed files with 44 additions and 33 deletions

View File

@ -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<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);
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) {
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_;

View File

@ -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);

View File

@ -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;
}

View File

@ -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);

View File

@ -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();

View File

@ -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("未知错误"));
}
}

View File

@ -1,6 +1,7 @@
#include "dto/DatasetChartDto.hpp"
#include <cmath>
#include <QString>
#include <QtGlobal>
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<Anomaly> parseDatasetAnomalies(const QJsonArray& arr) {
Anomaly a;
a.name = o.value("exceptionName").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();
a.lineColor = lg.value("polylineColor").toString("#000000").toStdString();
a.lineWidth = lg.value("polylineWidth").toDouble(1.0);

View File

@ -98,24 +98,6 @@ vtkSmartPointer<vtkUnstructuredGrid> toCellGrid(const Grid& g) {
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
ContourBandsResult buildContourBands(const Grid& g, const ColorScale& cs, const ContourOptions& opt) {

View File

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

View File

@ -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());
}