geopro/src/app/panels/AnomalyListPanel.cpp

266 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/AnomalyListPanel.hpp"
#include <cmath>
#include <cstddef>
#include <QAbstractItemModel>
#include <QColor>
#include <QEvent>
#include <QListWidget>
#include <QListWidgetItem>
#include <QMouseEvent>
#include <QObject>
#include <QPainter>
#include <QPainterPath>
#include <QPen>
#include <QString>
#include <QStyledItemDelegate>
#include "Theme.hpp"
#include "model/ColorScale.hpp"
namespace geopro::app {
namespace {
// 由 localPts 算「位置(质心x)·深(质心y)·尺寸(包络对角)」摘要文本。
// 异常坐标在剖面距离/深度空间(x=距离米, y=深度米)。
QString summarize(const geopro::core::Anomaly& a)
{
if (a.localPts.empty()) return QStringLiteral("(无几何)");
double cx = 0.0, cy = 0.0;
double minx = a.localPts[0].x, maxx = a.localPts[0].x;
double miny = a.localPts[0].y, maxy = a.localPts[0].y;
for (const auto& p : a.localPts) {
cx += p.x; cy += p.y;
if (p.x < minx) minx = p.x;
if (p.x > maxx) maxx = p.x;
if (p.y < miny) miny = p.y;
if (p.y > maxy) maxy = p.y;
}
const auto n = static_cast<double>(a.localPts.size());
cx /= n; cy /= n;
const double span = std::hypot(maxx - minx, maxy - miny);
return QStringLiteral("位置 %1m · 深 %2m · 尺寸 %3m")
.arg(cx, 0, 'f', 0)
.arg(cy, 0, 'f', 0)
.arg(span, 0, 'f', 0);
}
// lineColor 字符串 → QColor兼容 "#RRGGBB" 与 "rgba(...)")。
QColor barColor(const QString& s)
{
const auto c = geopro::core::parseColor(s.toStdString(), geopro::core::AlphaScale::Bit255);
return QColor(c.r, c.g, c.b);
}
// 异常分级 → 状态语义键(规范 §1.4/§6.3/§8.3:高=Danger 中=Warning 低=Info 未知=Neutral
// Anomaly 数据模型不带显式 level 字段,故按可得信号稳健推断:
// 1) typeName/name 含「高/中/低」字样 → 直接定级;
// 2) 否则按 lineColor 色相归类(红→高 橙/黄→中 蓝→低,与右栏列表/三维标注牌同色);
// 3) 仍无法判定 → Neutral停用/未知),避免乱给状态色。
// 返回值为状态 token 前缀("danger"/"warning"/"info"/"neutral"),调用方据此拼 token 名。
QString anomalyStatus(const geopro::core::Anomaly& a)
{
const QString tag = QString::fromStdString(a.typeName + a.name);
if (tag.contains(QStringLiteral(""))) return QStringLiteral("danger");
if (tag.contains(QStringLiteral(""))) return QStringLiteral("warning");
if (tag.contains(QStringLiteral(""))) return QStringLiteral("info");
// 按 lineColor 色相归类HSV 色相环红≈0/360 橙黄≈2070 蓝≈190260
const QColor c = barColor(QString::fromStdString(a.lineColor));
if (c.isValid() && c.saturationF() > 0.25) {
const int h = c.hue(); // -1=无色相(灰)
if (h >= 0) {
if (h < 20 || h >= 330) return QStringLiteral("danger");
if (h < 75) return QStringLiteral("warning");
if (h >= 185 && h < 265) return QStringLiteral("info");
}
}
return QStringLiteral("neutral");
}
// 状态键 + 后缀 → 主题 token 名(如 status + "danger" + "-bg"。neutral 无 -bg回落中性面。
QColor statusColor(const QString& status, bool bg)
{
if (status == QStringLiteral("neutral"))
return geopro::app::tokenColor(bg ? "bg/panel-subtle" : "status/neutral");
const QString name = QStringLiteral("status/%1%2").arg(status, bg ? QStringLiteral("-bg") : QString());
return geopro::app::tokenColor(name.toUtf8().constData());
}
// 右侧眼睛命中区(卡片右端,竖直居中)。
QRect anomalyEyeRect(const QRect& itemRect)
{
const QRect r = itemRect.adjusted(4, 2, -4, -2);
const int sz = 22;
return QRect(r.right() - sz - 8, r.center().y() - sz / 2, sz, sz);
}
class AnomalyCardDelegate : public QStyledItemDelegate {
public:
using QStyledItemDelegate::QStyledItemDelegate;
QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const override
{
return QSize(0, 58);
}
bool editorEvent(QEvent* e, QAbstractItemModel* model, const QStyleOptionViewItem& opt,
const QModelIndex& idx) override
{
if (e->type() == QEvent::MouseButtonRelease) {
auto* me = static_cast<QMouseEvent*>(e);
if (anomalyEyeRect(opt.rect).contains(me->position().toPoint())) {
const auto cur = static_cast<Qt::CheckState>(idx.data(Qt::CheckStateRole).toInt());
model->setData(idx, cur == Qt::Checked ? Qt::Unchecked : Qt::Checked,
Qt::CheckStateRole);
return true; // 吃掉点击:只切显隐,不改选中
}
}
return QStyledItemDelegate::editorEvent(e, model, opt, idx);
}
void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override
{
p->save();
p->setRenderHint(QPainter::Antialiasing, true);
const QRect r = opt.rect.adjusted(4, 3, -4, -3);
const bool selected = opt.state & QStyle::State_Selected;
const bool hover = opt.state & QStyle::State_MouseOver;
// 分级状态键§6.3):高=Danger 中=Warning 低=Info 未知=Neutral。
const QString status = idx.data(kAnomalyStatusRole).toString();
// 卡底§6.3 规范§3.2 radius/md=6静止态 = 该分级状态浅底;选中/hover 叠一档
// 交互态(选中=bg/selected 强调底hover=bg/hover让交互可辨又不丢分级语义。
QPainterPath path; path.addRoundedRect(r, geopro::app::radius::kMd, geopro::app::radius::kMd);
const QColor cardBg = selected ? geopro::app::tokenColor("bg/selected")
: hover ? geopro::app::tokenColor("bg/hover")
: statusColor(status, /*bg=*/true);
p->fillPath(path, cardBg);
// 左 3px 状态色竖条(取分级状态主色,与卡底/标签同源§6.3
p->fillRect(QRect(r.left(), r.top() + 4, 3, r.height() - 8),
statusColor(status, /*bg=*/false));
const QString name = idx.data(Qt::DisplayRole).toString();
const QString type = idx.data(kAnomalyTypeRole).toString();
const QString summary = idx.data(kAnomalySummaryRole).toString();
const int left = r.left() + 14;
const int right = anomalyEyeRect(opt.rect).left() - 8; // 给眼睛留位
const int rowW = right - left;
// 第一行:状态圆点 + 名称text/body-strong 600。圆点 8px色 = 分级状态主色§6.3)。
const int dot = 8;
const QRect nameR(left + dot + 6, r.top() + 8, rowW - dot - 6, 20);
p->setBrush(statusColor(status, /*bg=*/false));
p->setPen(Qt::NoPen);
p->drawEllipse(QPointF(left + dot / 2.0, nameR.center().y()), dot / 2.0, dot / 2.0);
QFont nf = opt.font; nf.setPixelSize(geopro::app::scaledPx(13)); nf.setWeight(QFont::DemiBold);
p->setFont(nf);
p->setPen(geopro::app::tokenColor("text/primary"));
p->setBrush(Qt::NoBrush);
p->drawText(nameR, Qt::AlignLeft | Qt::AlignVCenter,
p->fontMetrics().elidedText(name, Qt::ElideRight, nameR.width()));
// 第二行:分级胶囊标签 + 摘要
int x = left;
const int cy = r.top() + 38;
if (!type.isEmpty()) {
// 等级标签§6.8):胶囊 radius/pill按高度算半径底 = 状态浅底,文字 = 状态主色。
QFont pf = opt.font; pf.setPixelSize(geopro::app::scaledPx(11));
p->setFont(pf);
const QFontMetrics fm(pf);
const int tw = fm.horizontalAdvance(type);
const int ph = fm.height() + 2;
const QRect pill(x, cy - ph / 2, tw + 12, ph);
QPainterPath pp; pp.addRoundedRect(pill, ph / 2.0, ph / 2.0);
p->fillPath(pp, statusColor(status, /*bg=*/true));
p->setPen(statusColor(status, /*bg=*/false));
p->drawText(pill, Qt::AlignCenter, type);
x = pill.right() + 8;
}
if (!summary.isEmpty()) {
// 属性行数值§2.1/§6.3用等宽字族保证「140m · 18m / 32 Ω·m」逐列对齐。
QFont sf = opt.font;
sf.setFamilies(QString::fromLatin1(geopro::app::type::kMonoFamily).split(QStringLiteral(", ")));
sf.setPixelSize(geopro::app::scaledPx(11));
p->setFont(sf);
p->setPen(geopro::app::tokenColor("text/secondary"));
const QRect sumR(x, cy - 10, right - x, 20);
p->drawText(sumR, Qt::AlignLeft | Qt::AlignVCenter,
p->fontMetrics().elidedText(summary, Qt::ElideRight, sumR.width()));
}
// 右侧眼睛(显隐):可见=次要色睁眼;隐藏=禁用色 + 斜杠
const bool visible =
static_cast<Qt::CheckState>(idx.data(Qt::CheckStateRole).toInt()) == Qt::Checked;
const QColor eyeCol = geopro::app::tokenColor(visible ? "text/secondary" : "text/disabled");
const QRectF eb = anomalyEyeRect(opt.rect);
const QPointF c = eb.center();
const double w = eb.width() * 0.42, h = eb.height() * 0.24;
p->setPen(QPen(eyeCol, 1.4));
p->setBrush(Qt::NoBrush);
QPainterPath eye;
eye.moveTo(c.x() - w, c.y());
eye.quadTo(c.x(), c.y() - h * 2.0, c.x() + w, c.y());
eye.quadTo(c.x(), c.y() + h * 2.0, c.x() - w, c.y());
p->drawPath(eye);
p->setBrush(eyeCol);
p->drawEllipse(c, h * 0.95, h * 0.95);
p->setBrush(Qt::NoBrush);
if (!visible)
p->drawLine(QPointF(c.x() - w, c.y() + h * 1.6), QPointF(c.x() + w, c.y() - h * 1.6));
p->restore();
}
};
} // namespace
void populateAnomalyList(QListWidget* list, const std::vector<geopro::core::Anomaly>& anomalies)
{
if (!list) return;
list->clear();
for (std::size_t i = 0; i < anomalies.size(); ++i) {
const auto& a = anomalies[i];
const QString name = QString::fromStdString(a.name.empty() ? "异常" : a.name);
auto* item = new QListWidgetItem(name, list);
item->setData(kAnomalyIndexRole, static_cast<int>(i));
item->setData(kAnomalyColorRole, QString::fromStdString(a.lineColor));
item->setData(kAnomalyTypeRole, QString::fromStdString(a.typeName));
item->setData(kAnomalySummaryRole, summarize(a));
item->setData(kAnomalyStatusRole, anomalyStatus(a)); // 分级状态键(驱动卡底/标签/竖条同色)
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(Qt::Checked); // 默认显示
}
}
QString anomalyVisibleCountText(QListWidget* list)
{
if (!list) return QStringLiteral("0/0");
int total = 0, visible = 0;
for (int i = 0; i < list->count(); ++i) {
const QListWidgetItem* it = list->item(i);
if (!it) continue;
++total;
if (it->checkState() == Qt::Checked) ++visible;
}
return QStringLiteral("%1/%2").arg(visible).arg(total);
}
void applyAnomalyCardDelegate(QListWidget* list)
{
if (!list) return;
list->setItemDelegate(new AnomalyCardDelegate(list));
list->setMouseTracking(true);
list->setSpacing(0);
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, list,
[list]() { list->viewport()->update(); });
}
} // namespace geopro::app