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

346 lines
15 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"
#include "panels/chart/ContourSimplify.hpp"
namespace geopro::app {
namespace {
constexpr int kFillUpsample = 4; // 填充图像每网格格细分 K双线性插值平滑带边界
constexpr int kMaxFillDim = 2400; // 填充图像单边像素上限(防极端网格爆内存)
constexpr int kLabelFontPx = 10; // 等值线标注字号
constexpr double kLabelMinLenPx = 24.0; // 整条线像素长度小于此不标注(极短碎线)
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();
linesRaw_.clear();
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);
linesRaw_ = std::move(res.lines);
lines_ = linesRaw_;
// buildContourBands 当前未回填 level恒 0在此按线上代表点采网格值并吸附到最近色阶级
// 使标注显示真实等值线值。
resolveLineLevels(g, svc->scale());
linesRaw_ = lines_; // level 回填后同步到原始集(简化保留 level
applySimplify(); // 按当前容差抽稀(首次 tol=0 即原样)。
} else {
linesRaw_.clear();
lines_.clear();
}
}
void ContourPlotItem::setSimplifyTolerance(double tol) {
simplifyTol_ = tol < 0.0 ? 0.0 : tol;
applySimplify();
}
void ContourPlotItem::applySimplify() {
if (simplifyTol_ <= 0.0) {
lines_ = linesRaw_;
return;
}
lines_.clear();
lines_.reserve(linesRaw_.size());
for (const auto& ln : linesRaw_) {
render::ContourLine s;
s.level = ln.level;
s.pts = douglasPeucker(ln.pts, simplifyTol_);
lines_.push_back(std::move(s));
}
}
double ContourPlotItem::contourLevelNear(double dataX, double dataY, double hitDataRadius) const {
// 在已绘制等值线(简化后)找最近线段,命中半径内返回该线 level。
double bestD2 = hitDataRadius * hitDataRadius;
double bestLevel = std::nan("");
for (const auto& ln : lines_) {
if (std::isnan(ln.level)) continue;
for (std::size_t i = 1; i < ln.pts.size(); ++i) {
const auto& a = ln.pts[i - 1];
const auto& b = ln.pts[i];
const double dx = b.x - a.x, dy = b.y - a.y;
const double len2 = dx * dx + dy * dy;
double t = len2 > 0 ? ((dataX - a.x) * dx + (dataY - a.y) * dy) / len2 : 0.0;
t = std::clamp(t, 0.0, 1.0);
const double px = a.x + t * dx, py = a.y + t * dy;
const double d2 = (dataX - px) * (dataX - px) + (dataY - py) * (dataY - py);
if (d2 < bestD2) { bestD2 = d2; bestLevel = ln.level; }
}
}
return bestLevel;
}
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;
// 限幅:极端网格下按**统一比例**降采样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);
// 每像素 → 归一化网格坐标 (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_;
}
QRectF ContourPlotItem::anomalyBoundingRect(int index) const {
if (index < 0 || index >= static_cast<int>(anoms_.size())) return {};
const auto& pts = anoms_[index].localPts;
if (pts.empty()) return {};
double minX = pts.front().x, maxX = pts.front().x;
double minY = pts.front().y, maxY = pts.front().y;
for (const auto& p : pts) {
minX = std::min(minX, p.x); maxX = std::max(maxX, p.x);
minY = std::min(minY, p.y); maxY = std::max(maxY, p.y);
}
return QRectF(minX, minY, maxX - minX, maxY - minY);
}
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) 等值线:按线形⚙ 配置取色/虚实(默认黑实线)。
if (showLines_ && !lines_.empty()) {
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
QPen pen(QColor(lineColor_.r, lineColor_.g, lineColor_.b, lineColor_.a));
pen.setWidthF(1.0); // 1px 等值线
pen.setStyle(lineDashed_ ? Qt::DashLine : Qt::SolidLine);
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) 标注:每条等值线只标一个(对齐原版),放弧长中点,随该处方向旋转。
if (showLabels_) {
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
QFont f = painter->font();
f.setPixelSize(kLabelFontPx);
painter->setFont(f);
painter->setPen(QColor(labelColor_.r, labelColor_.g, labelColor_.b, labelColor_.a));
const QFontMetricsF fm(f);
for (const auto& ln : lines_) {
if (ln.pts.size() < 2 || std::isnan(ln.level)) continue;
// 映射到像素 + 累计像素弧长。
std::vector<QPointF> px;
px.reserve(ln.pts.size());
for (const auto& p : ln.pts) px.push_back(mapPt(p));
double total = 0.0;
for (std::size_t i = 1; i < px.size(); ++i)
total += std::hypot(px[i].x() - px[i - 1].x(), px[i].y() - px[i - 1].y());
if (total < kLabelMinLenPx) continue; // 极短碎线不标注
const QString txt = QString::number(ln.level, 'g', 4);
const double halfW = fm.horizontalAdvance(txt) * 0.5;
const double targetAt = total * 0.5; // 弧长中点
double acc = 0.0;
for (std::size_t i = 1; i < px.size(); ++i) {
const double seg =
std::hypot(px[i].x() - px[i - 1].x(), px[i].y() - px[i - 1].y());
if (acc + seg >= targetAt || i == px.size() - 1) {
const double t = seg > 1e-6 ? (targetAt - acc) / seg : 0.0;
const QPointF pos(px[i - 1].x() + (px[i].x() - px[i - 1].x()) * t,
px[i - 1].y() + (px[i].y() - px[i - 1].y()) * t);
double ang =
std::atan2(px[i].y() - px[i - 1].y(), px[i].x() - px[i - 1].x()) *
kRad2Deg;
if (ang > 90.0) ang -= 180.0; // 文字大体正向
if (ang < -90.0) ang += 180.0;
painter->save();
painter->translate(pos);
painter->rotate(ang);
painter->drawText(QPointF(-halfW, -2), txt);
painter->restore();
break; // 只标一个
}
acc += seg;
}
}
painter->restore();
}
}
// 4) 异常叠加:点=小方块、线=折线、面=闭合多边形;颜色用 lineColordashed→虚线。
if (showAnomalies_ && !anoms_.empty()) {
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setBrush(Qt::NoBrush);
for (int ai = 0; ai < static_cast<int>(anoms_.size()); ++ai) {
const auto& a = anoms_[ai];
if (a.localPts.empty()) continue;
const bool hl = (ai == highlightIdx_); // I12 当前定位高亮
QColor col(QString::fromStdString(a.lineColor));
if (!col.isValid()) col = QColor(0, 0, 0);
QPen pen(hl ? QColor(255, 255, 0) : col); // 高亮黄(对照原版 #ffff00
pen.setWidthF(hl ? std::max(3.0, a.lineWidth) : (a.lineWidth > 0 ? a.lineWidth : 1.0));
pen.setStyle(hl ? Qt::SolidLine : (a.dashed ? Qt::DashLine : Qt::SolidLine));
painter->setPen(pen);
if (a.markType == core::AnomalyMarkType::Point) {
const QPointF c = mapPt(a.localPts.front());
const double r = hl ? 5.0 : 3.0;
painter->drawRect(QRectF(c.x() - r, c.y() - r, 2 * r, 2 * r));
} 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