feat/dataset-detail-chart #5
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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("未知错误"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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..1(0=不平滑)
|
||||
double simplifyTol = 0.5; // 等值线简化容差(数据单位,0=不简化)
|
||||
bool makeLines = true;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue