346 lines
15 KiB
C++
346 lines
15 KiB
C++
#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) 异常叠加:点=小方块、线=折线、面=闭合多边形;颜色用 lineColor,dashed→虚线。
|
||
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
|