geopro/src/app/panels/chart/ContourPlotItem.cpp

261 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "panels/chart/ContourPlotItem.hpp"
#include <algorithm>
#include <cmath>
#include <QFont>
#include <QPainter>
#include <QPen>
#include <QPolygonF>
#include <qwt_plot.h>
#include <qwt_scale_map.h>
#include "panels/chart/ColorMapService.hpp"
namespace geopro::app {
namespace {
constexpr int kFillUpsample = 4; // 填充图像每网格格细分 K双线性插值平滑带边界
constexpr int kMaxFillDim = 2400; // 填充图像单边像素上限(防极端网格爆内存)
constexpr int kLabelFontPx = 10; // 等值线标注字号
constexpr int kLabelMinSegPx = 60; // 太短的等值线不标注像素长度阈值draw 期判定)
constexpr double kRad2Deg = 57.29577951308232; // 180/π(避免依赖 M_PI
} // namespace
ContourPlotItem::ContourPlotItem() : QwtPlotItem() {
setRenderHint(QwtPlotItem::RenderAntialiased, false);
// 网格数据 x 轴在底部(与 RawDataChartView 的顶部 x 轴不同y 轴在左。
setXAxis(QwtPlot::xBottom);
setYAxis(QwtPlot::yLeft);
}
void ContourPlotItem::setData(const core::Grid& g, ColorMapService* svc,
const std::vector<core::Anomaly>& anoms, bool showLines,
bool showLabels) {
showLines_ = showLines;
showLabels_ = showLabels;
anoms_ = anoms;
const int nx = g.nx(), ny = g.ny();
if (nx < 2 || ny < 2 || static_cast<int>(g.x.size()) < nx ||
static_cast<int>(g.y.size()) < ny) {
fillImage_ = QImage();
dataBBox_ = QRectF();
lines_.clear();
return;
}
const double xmin = g.x.front(), xmax = g.x.back();
const double ymin = g.y.front(), ymax = g.y.back();
dataBBox_ = QRectF(xmin, ymin, xmax - xmin, ymax - ymin);
buildFillImage(g, svc);
// 等值线(矢量):复用 render 管线,仅取 lines填充走栅格
if (showLines_) {
render::ContourOptions opt;
opt.upsample = 2;
opt.makeLines = true;
auto res = render::buildContourBands(g, svc->scale(), opt);
lines_ = std::move(res.lines);
// buildContourBands 当前未回填 level恒 0在此按线上代表点采网格值并吸附到最近色阶级
// 使标注显示真实等值线值。
resolveLineLevels(g, svc->scale());
} else {
lines_.clear();
}
}
void ContourPlotItem::resolveLineLevels(const core::Grid& g, const core::ColorScale& cs) {
const auto stops = cs.stopValues();
if (stops.empty() || lines_.empty()) return;
const int nx = g.nx(), ny = g.ny();
const double xmin = g.x.front(), xmax = g.x.back();
const double ymin = g.y.front(), ymax = g.y.back();
const double xspan = (xmax - xmin), yspan = (ymax - ymin);
// 在数据坐标点做双线性采样NaN 安全)。
auto sampleAt = [&](const core::Vec2& p) -> double {
if (xspan <= 0 || yspan <= 0) return std::nan("");
double fi = (p.x - xmin) / xspan * (nx - 1);
double fj = (p.y - ymin) / yspan * (ny - 1);
fi = std::clamp(fi, 0.0, static_cast<double>(nx - 1));
fj = std::clamp(fj, 0.0, static_cast<double>(ny - 1));
int i0 = std::min(static_cast<int>(fi), nx - 2);
int j0 = std::min(static_cast<int>(fj), ny - 2);
double ti = fi - i0, tj = fj - j0;
double v00 = g.valueAt(i0, j0), v10 = g.valueAt(i0 + 1, j0);
double v01 = g.valueAt(i0, j0 + 1), v11 = g.valueAt(i0 + 1, j0 + 1);
if (std::isnan(v00) || std::isnan(v10) || std::isnan(v01) || std::isnan(v11))
return std::nan("");
return (v00 * (1 - ti) + v10 * ti) * (1 - tj) + (v01 * (1 - ti) + v11 * ti) * tj;
};
for (auto& ln : lines_) {
double sampled = std::nan("");
for (const auto& p : ln.pts) { // 取首个非 NaN 采样点
sampled = sampleAt(p);
if (!std::isnan(sampled)) break;
}
if (std::isnan(sampled)) {
ln.level = std::nan("");
continue;
}
// 吸附到最近色阶级(等值线恰落在某级上)。
double best = stops.front();
double bestD = std::fabs(sampled - best);
for (double s : stops) {
double d = std::fabs(sampled - s);
if (d < bestD) { bestD = d; best = s; }
}
ln.level = best;
}
}
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;
QImage img(W, H, QImage::Format_ARGB32);
img.fill(Qt::transparent);
// 每像素 → 归一化网格坐标 (fi,fj) → 四邻格双线性插值;任一邻格 NaN 则该像素透明。
for (int py = 0; py < H; ++py) {
// 图像顶行 py=0 对应 y 最大ymax地表底行对应 y 最小(最深)。
double fj = static_cast<double>(H - 1 - py) / (H - 1) * (ny - 1);
int j0 = std::min(static_cast<int>(fj), ny - 2);
double tj = fj - j0;
auto* scan = reinterpret_cast<QRgb*>(img.scanLine(py));
for (int px = 0; px < W; ++px) {
double fi = static_cast<double>(px) / (W - 1) * (nx - 1);
int i0 = std::min(static_cast<int>(fi), nx - 2);
double ti = fi - i0;
double v00 = g.valueAt(i0, j0), v10 = g.valueAt(i0 + 1, j0);
double v01 = g.valueAt(i0, j0 + 1), v11 = g.valueAt(i0 + 1, j0 + 1);
if (std::isnan(v00) || std::isnan(v10) || std::isnan(v01) || std::isnan(v11))
continue; // 含无数据格 → 像素透明(不规则白边)
double v = (v00 * (1 - ti) + v10 * ti) * (1 - tj) +
(v01 * (1 - ti) + v11 * ti) * tj;
auto c = svc->colorAtDiscrete(v); // 离散色带 → 平滑填充带边界
scan[px] = qRgba(c.r, c.g, c.b, c.a ? c.a : 255);
}
}
fillImage_ = std::move(img);
}
QRectF ContourPlotItem::boundingRect() const {
return dataBBox_;
}
void ContourPlotItem::draw(QPainter* painter, const QwtScaleMap& xMap, const QwtScaleMap& yMap,
const QRectF& /*canvasRect*/) const {
if (dataBBox_.isNull()) return;
const double xmin = dataBBox_.left(), xmax = dataBBox_.right();
const double ymin = dataBBox_.top(), ymax = dataBBox_.bottom();
// 1) 填充:数据 bbox → 像素矩形(注意 y 翻转ymax→画布上沿、ymin→下沿blit + 平滑缩放。
if (!fillImage_.isNull()) {
const double pxL = xMap.transform(xmin);
const double pxR = xMap.transform(xmax);
const double pyTop = yMap.transform(ymax);
const double pyBot = yMap.transform(ymin);
QRectF target(pxL, pyTop, pxR - pxL, pyBot - pyTop);
painter->save();
painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
painter->drawImage(target, fillImage_);
painter->restore();
}
auto mapPt = [&](const core::Vec2& p) {
return QPointF(xMap.transform(p.x), yMap.transform(p.y));
};
// 2) 等值线:黑色 0 宽cosmetic细线。
if (showLines_ && !lines_.empty()) {
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
QPen pen(QColor(0, 0, 0));
pen.setWidthF(1.0); // 1px 黑色等值线
painter->setPen(pen);
for (const auto& ln : lines_) {
if (ln.pts.size() < 2) continue;
QPolygonF poly;
poly.reserve(static_cast<int>(ln.pts.size()));
for (const auto& p : ln.pts) poly << mapPt(p);
painter->drawPolyline(poly);
}
painter->restore();
// 3) 标注:沿线中段画 level 数值(小字黑,随相邻两点方向旋转)。
if (showLabels_) {
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
QFont f = painter->font();
f.setPixelSize(kLabelFontPx);
painter->setFont(f);
painter->setPen(QColor(0, 0, 0));
for (const auto& ln : lines_) {
if (ln.pts.size() < 2 || std::isnan(ln.level)) continue;
// 取折线中段两点定位置/朝向。
const std::size_t mid = ln.pts.size() / 2;
const QPointF a = mapPt(ln.pts[mid - 1]);
const QPointF b = mapPt(ln.pts[mid]);
// 整条线像素长度太短不标注(避免密集杂乱)。
const QPointF s = mapPt(ln.pts.front());
const QPointF e = mapPt(ln.pts.back());
if (std::hypot(e.x() - s.x(), e.y() - s.y()) < kLabelMinSegPx) continue;
double ang = std::atan2(b.y() - a.y(), b.x() - a.x()) * kRad2Deg;
if (ang > 90.0) ang -= 180.0; // 保持文字大体正向(不上下颠倒)
if (ang < -90.0) ang += 180.0;
const QString txt = QString::number(ln.level, 'g', 4);
painter->save();
painter->translate((a.x() + b.x()) * 0.5, (a.y() + b.y()) * 0.5);
painter->rotate(ang);
painter->drawText(QPointF(-12, -2), txt);
painter->restore();
}
painter->restore();
}
}
// 4) 异常叠加:点=小方块、线=折线、面=闭合多边形;颜色用 lineColordashed→虚线。
if (showAnomalies_ && !anoms_.empty()) {
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setBrush(Qt::NoBrush);
for (const auto& a : anoms_) {
if (a.localPts.empty()) continue;
QColor col(QString::fromStdString(a.lineColor));
if (!col.isValid()) col = QColor(0, 0, 0);
QPen pen(col);
pen.setWidthF(a.lineWidth > 0 ? a.lineWidth : 1.0);
pen.setStyle(a.dashed ? Qt::DashLine : Qt::SolidLine);
painter->setPen(pen);
if (a.markType == core::AnomalyMarkType::Point) {
const QPointF c = mapPt(a.localPts.front());
painter->drawRect(QRectF(c.x() - 3, c.y() - 3, 6, 6));
} else {
QPolygonF poly;
poly.reserve(static_cast<int>(a.localPts.size()));
for (const auto& p : a.localPts) poly << mapPt(p);
if (a.markType == core::AnomalyMarkType::Polygon)
painter->drawPolygon(poly); // 闭合
else
painter->drawPolyline(poly);
}
}
painter->restore();
}
}
} // namespace geopro::app