#include "panels/chart/ContourPlotItem.hpp" #include #include #include #include #include #include #include #include #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& 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(g.x.size()) < nx || static_cast(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(nx - 1)); fj = std::clamp(fj, 0.0, static_cast(ny - 1)); int i0 = std::min(static_cast(fi), nx - 2); int j0 = std::min(static_cast(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(H - 1 - py) / (H - 1) * (ny - 1); int j0 = std::min(static_cast(fj), ny - 2); double tj = fj - j0; auto* scan = reinterpret_cast(img.scanLine(py)); for (int px = 0; px < W; ++px) { double fi = static_cast(px) / (W - 1) * (nx - 1); int i0 = std::min(static_cast(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(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) 异常叠加:点=小方块、线=折线、面=闭合多边形;颜色用 lineColor,dashed→虚线。 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(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