504 lines
22 KiB
C++
504 lines
22 KiB
C++
#include "panels/chart/AutoAnnotationDialog.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <utility>
|
||
|
||
#include <QButtonGroup>
|
||
#include <QComboBox>
|
||
#include <QFormLayout>
|
||
#include <QFrame>
|
||
#include <QHBoxLayout>
|
||
#include <QHeaderView>
|
||
#include <QJsonObject>
|
||
#include <QLabel>
|
||
#include <QLineEdit>
|
||
#include <QMessageBox>
|
||
#include <QPointer>
|
||
#include <QPushButton>
|
||
#include <QRadioButton>
|
||
#include <QSpinBox>
|
||
#include <QTableWidget>
|
||
#include <QToolButton>
|
||
#include <QVBoxLayout>
|
||
|
||
#include <qwt_plot.h>
|
||
|
||
#include "FormKit.hpp"
|
||
#include "Theme.hpp" // scaledPx
|
||
#include "dto/DatasetChartDto.hpp" // parseDatasetAnomalies(JSON→Anomaly)
|
||
#include "panels/chart/ColorMapService.hpp"
|
||
#include "panels/chart/ContourPlotItem.hpp"
|
||
#include "repo/IDatasetCommandRepository.hpp"
|
||
|
||
namespace geopro::app {
|
||
|
||
namespace {
|
||
constexpr int kDefaultMinPoints = 4; // 原版 minPointCount 默认 4
|
||
// 面异常类型固定 remarkSourceType="3"(原版自动标注仅支持面/polygon)。
|
||
const QString kPolygonType = QStringLiteral("3");
|
||
|
||
// 从网格标量算 max/min/mean/median(过滤 NaN)。空 → 全 '-'。
|
||
struct Stats { bool valid = false; double mx = 0, mn = 0, mean = 0, median = 0; };
|
||
Stats computeStats(const std::vector<double>& raw) {
|
||
std::vector<double> v;
|
||
v.reserve(raw.size());
|
||
for (double d : raw)
|
||
if (!std::isnan(d)) v.push_back(d);
|
||
if (v.empty()) return {};
|
||
std::sort(v.begin(), v.end());
|
||
Stats s;
|
||
s.valid = true;
|
||
s.mn = v.front();
|
||
s.mx = v.back();
|
||
double sum = 0;
|
||
for (double d : v) sum += d;
|
||
s.mean = sum / static_cast<double>(v.size());
|
||
const size_t m = v.size() / 2;
|
||
s.median = (v.size() % 2 == 0) ? (v[m - 1] + v[m]) / 2.0 : v[m];
|
||
return s;
|
||
}
|
||
|
||
QString fmt2(bool valid, double x) {
|
||
return valid ? QString::number(x, 'f', 2) : QStringLiteral("-");
|
||
}
|
||
} // namespace
|
||
|
||
AutoAnnotationDialog::AutoAnnotationDialog(geopro::data::IDatasetCommandRepository* repo,
|
||
QString dsObjectId, QString projectId,
|
||
const geopro::core::Grid& grid,
|
||
const geopro::core::ColorScale& scale, QWidget* parent)
|
||
: QDialog(parent),
|
||
repo_(repo),
|
||
dsObjectId_(std::move(dsObjectId)),
|
||
projectId_(std::move(projectId)),
|
||
grid_(grid),
|
||
scale_(scale) {
|
||
setWindowTitle(QStringLiteral("自动标注"));
|
||
setModal(true);
|
||
resize(geopro::app::scaledPx(1400), geopro::app::scaledPx(600));
|
||
|
||
auto* root = formkit::dialogRoot(this);
|
||
auto* split = new QHBoxLayout();
|
||
|
||
// ── 左:规则卡片列表(35%,对照原版左栏)────────────────────────────────
|
||
auto* leftCol = new QVBoxLayout();
|
||
leftCol->addWidget(new QLabel(QStringLiteral("异常判定规则"), this));
|
||
auto* ruleContainer = new QWidget(this);
|
||
ruleHost_ = new QVBoxLayout(ruleContainer);
|
||
ruleHost_->setContentsMargins(0, 0, 0, 0);
|
||
leftCol->addWidget(ruleContainer);
|
||
auto* addBtn = new QPushButton(QStringLiteral("添加规则"), this);
|
||
connect(addBtn, &QPushButton::clicked, this, [this]() { addRule(); });
|
||
leftCol->addWidget(addBtn);
|
||
leftCol->addStretch();
|
||
auto* leftWrap = new QWidget(this);
|
||
leftWrap->setLayout(leftCol);
|
||
split->addWidget(leftWrap, 35);
|
||
|
||
// ── 右:上(统计条 + 预览图) + 下预览表 ──────────────────────────────────
|
||
// 对照原版右上 <ContourPreview>:等值面预览图为主、数据统计在上。
|
||
auto* rightCol = new QVBoxLayout();
|
||
buildStatsBar(rightCol);
|
||
buildPreviewPlot(rightCol);
|
||
|
||
detectedLabel_ = new QLabel(QStringLiteral("自动标注结果"), this);
|
||
rightCol->addWidget(detectedLabel_);
|
||
previewTable_ = new QTableWidget(0, 6, this);
|
||
previewTable_->setHorizontalHeaderLabels({QStringLiteral("序号"), QStringLiteral("异常名称"),
|
||
QStringLiteral("异常类型"), QStringLiteral("阈值范围"),
|
||
QStringLiteral("阈值模式"), QStringLiteral("操作")});
|
||
previewTable_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||
previewTable_->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||
rightCol->addWidget(previewTable_, 1);
|
||
auto* rightWrap = new QWidget(this);
|
||
rightWrap->setLayout(rightCol);
|
||
split->addWidget(rightWrap, 65);
|
||
|
||
root->addLayout(split, 1);
|
||
|
||
// ── 底部按钮:取消 / 执行自动标注 / 确认保存 ─────────────────────────────
|
||
auto* btnLay = new QHBoxLayout();
|
||
btnLay->addStretch();
|
||
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
|
||
auto* execBtn = new QPushButton(QStringLiteral("执行自动标注"), this);
|
||
saveBtn_ = new QPushButton(QStringLiteral("确认保存"), this);
|
||
saveBtn_->setDefault(true); // 区域唯一主操作(规范 §6.7 primary);执行/取消为次按钮
|
||
saveBtn_->setEnabled(false); // 必须先执行得到预览才能保存
|
||
btnLay->addWidget(cancelBtn);
|
||
btnLay->addWidget(execBtn);
|
||
btnLay->addWidget(saveBtn_);
|
||
root->addLayout(btnLay);
|
||
|
||
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
|
||
connect(execBtn, &QPushButton::clicked, this, &AutoAnnotationDialog::onExecute);
|
||
connect(saveBtn_, &QPushButton::clicked, this, &AutoAnnotationDialog::onSave);
|
||
|
||
addRule(); // 默认一条规则
|
||
loadExceptionTypes(); // 拉面异常类型
|
||
}
|
||
|
||
AutoAnnotationDialog::~AutoAnnotationDialog() {
|
||
// previewItem_ 由 QwtPlot autoDelete=true 处理,但析构顺序不确定:先 detach 再交还。
|
||
if (previewItem_) {
|
||
previewItem_->detach();
|
||
delete previewItem_;
|
||
previewItem_ = nullptr;
|
||
}
|
||
// colorSvc_ 为 unique_ptr,自动释放。
|
||
}
|
||
|
||
void AutoAnnotationDialog::buildStatsBar(QVBoxLayout* into) {
|
||
// 数据统计(max/min/mean/median,从网格标量算)。
|
||
const Stats s = computeStats(grid_.values());
|
||
auto* bar = new QFrame(this);
|
||
bar->setFrameShape(QFrame::StyledPanel);
|
||
auto* lay = new QHBoxLayout(bar);
|
||
auto addStat = [&](const QString& name, double v) {
|
||
lay->addWidget(new QLabel(QStringLiteral("%1:%2").arg(name, fmt2(s.valid, v)), bar));
|
||
};
|
||
addStat(QStringLiteral("最大值"), s.mx);
|
||
addStat(QStringLiteral("最小值"), s.mn);
|
||
addStat(QStringLiteral("均值"), s.mean);
|
||
addStat(QStringLiteral("中位数"), s.median);
|
||
lay->addStretch();
|
||
into->addWidget(bar);
|
||
}
|
||
|
||
void AutoAnnotationDialog::buildPreviewPlot(QVBoxLayout* into) {
|
||
// 复刻原版 <ContourPreview>:用 GridDataChartView 同款 ContourPlotItem 渲染当前网格等值面,
|
||
// 预览图为主区域;执行/删除时把预演异常实时叠加(refreshPreviewAnomalies)。
|
||
previewPlot_ = new QwtPlot(this);
|
||
previewPlot_->setObjectName(QStringLiteral("autoAnnotPreview"));
|
||
previewPlot_->enableAxis(QwtPlot::xBottom, true);
|
||
previewPlot_->enableAxis(QwtPlot::xTop, false);
|
||
previewPlot_->enableAxis(QwtPlot::yLeft, true);
|
||
previewPlot_->setMinimumHeight(geopro::app::scaledPx(220));
|
||
|
||
// 网格 < 2×2 视为无可渲染数据:仅占位,不建等值面项。
|
||
if (grid_.nx() < 2 || grid_.ny() < 2) {
|
||
into->addWidget(previewPlot_, 1);
|
||
return;
|
||
}
|
||
|
||
colorSvc_ = std::make_unique<ColorMapService>(scale_);
|
||
previewItem_ = new ContourPlotItem();
|
||
// 轻量预览:渲染等值面 + 等值线,关标注(小图标注过密);初始无异常。
|
||
previewItem_->setData(grid_, colorSvc_.get(), {}, /*showLines=*/true, /*showLabels=*/false);
|
||
previewItem_->setShowAnomalies(true);
|
||
previewItem_->attach(previewPlot_);
|
||
|
||
// 轴范围 = 数据范围(y 深度向下:上沿 ymax、下沿 ymin,与 GridDataChartView 一致)。
|
||
const QRectF bbox = previewItem_->boundingRect();
|
||
if (!bbox.isNull()) {
|
||
previewPlot_->setAxisScale(QwtPlot::xBottom, bbox.left(), bbox.right());
|
||
previewPlot_->setAxisScale(QwtPlot::yLeft, bbox.top(), bbox.bottom());
|
||
}
|
||
previewPlot_->replot();
|
||
into->addWidget(previewPlot_, 1);
|
||
}
|
||
|
||
void AutoAnnotationDialog::refreshPreviewAnomalies() {
|
||
// 把当前 previewExceptions_(execute 返回 / 删除后剩余)映射成 Anomaly 叠加到预览图。
|
||
// 复用 dto::parseDatasetAnomalies(与正式异常同一 JSON 形态:location.coordinate + legend)。
|
||
if (!previewItem_ || !colorSvc_) return;
|
||
const auto anoms = geopro::data::dto::parseDatasetAnomalies(previewExceptions_);
|
||
previewItem_->setData(grid_, colorSvc_.get(), anoms, /*showLines=*/true, /*showLabels=*/false);
|
||
previewItem_->setShowAnomalies(true);
|
||
if (previewPlot_) previewPlot_->replot();
|
||
}
|
||
|
||
void AutoAnnotationDialog::loadExceptionTypes() {
|
||
if (!repo_) return;
|
||
QPointer<AutoAnnotationDialog> self(this);
|
||
repo_->listExceptionTypes(projectId_, kPolygonType, [self](bool ok, QJsonArray list, QString) {
|
||
if (!self || !ok) return;
|
||
self->exceptionTypeOptions_ = {};
|
||
for (const QJsonValue& v : list) {
|
||
const QJsonObject o = v.toObject();
|
||
QString name = o.value(QStringLiteral("exceptionTypeName")).toString();
|
||
if (name.isEmpty()) name = o.value(QStringLiteral("label")).toString();
|
||
QString id = o.value(QStringLiteral("id")).toString();
|
||
if (id.isEmpty()) id = o.value(QStringLiteral("value")).toString();
|
||
if (!id.isEmpty())
|
||
self->exceptionTypeOptions_.append(
|
||
QJsonObject{{QStringLiteral("id"), id}, {QStringLiteral("name"), name}});
|
||
}
|
||
// 回填已存在规则卡片下拉。
|
||
for (auto& r : self->rules_) {
|
||
r.type->clear();
|
||
for (const QJsonValue& ov : self->exceptionTypeOptions_) {
|
||
const QJsonObject o = ov.toObject();
|
||
r.type->addItem(o.value(QStringLiteral("name")).toString(),
|
||
o.value(QStringLiteral("id")).toString());
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
void AutoAnnotationDialog::addRule() {
|
||
auto* card = new QFrame(this);
|
||
card->setFrameShape(QFrame::StyledPanel);
|
||
auto* cardLay = new QVBoxLayout(card);
|
||
cardLay->setContentsMargins(6, 6, 6, 6);
|
||
|
||
RuleCard rc;
|
||
rc.frame = card;
|
||
|
||
// 卡片头:折叠 + 「规则N」 + 删除。
|
||
auto* header = new QHBoxLayout();
|
||
auto* collapseBtn = new QToolButton(card);
|
||
collapseBtn->setText(QStringLiteral("▼"));
|
||
collapseBtn->setCheckable(true);
|
||
rc.title = new QLabel(card);
|
||
auto* delBtn = new QToolButton(card);
|
||
delBtn->setText(QStringLiteral("删除"));
|
||
header->addWidget(collapseBtn);
|
||
header->addWidget(rc.title, 1);
|
||
header->addWidget(delBtn);
|
||
cardLay->addLayout(header);
|
||
|
||
// 卡片主体(可折叠)。
|
||
rc.body = new QWidget(card);
|
||
auto* form = formkit::makeEditForm();
|
||
|
||
// 阈值模式:radio-button 组(数值/百分位)。
|
||
auto* modeRow = new QWidget(rc.body);
|
||
auto* modeLay = new QHBoxLayout(modeRow);
|
||
modeLay->setContentsMargins(0, 0, 0, 0);
|
||
auto* rbNum = new QRadioButton(QStringLiteral("数值"), modeRow);
|
||
auto* rbPct = new QRadioButton(QStringLiteral("百分位"), modeRow);
|
||
rbNum->setChecked(true);
|
||
rc.modeGroup = new QButtonGroup(rc.body);
|
||
rc.modeGroup->addButton(rbNum, 1);
|
||
rc.modeGroup->addButton(rbPct, 2);
|
||
modeLay->addWidget(rbNum);
|
||
modeLay->addWidget(rbPct);
|
||
modeLay->addStretch();
|
||
form->addRow(formkit::editLabel(QStringLiteral("阈值模式")), modeRow);
|
||
|
||
// 阈值范围:min - max。
|
||
auto* rangeRow = new QWidget(rc.body);
|
||
auto* rangeLay = new QHBoxLayout(rangeRow);
|
||
rangeLay->setContentsMargins(0, 0, 0, 0);
|
||
rc.min = new QLineEdit(rangeRow);
|
||
rc.min->setPlaceholderText(QStringLiteral("最小"));
|
||
rc.max = new QLineEdit(rangeRow);
|
||
rc.max->setPlaceholderText(QStringLiteral("最大"));
|
||
rangeLay->addWidget(rc.min);
|
||
rangeLay->addWidget(new QLabel(QStringLiteral("-"), rangeRow));
|
||
rangeLay->addWidget(rc.max);
|
||
form->addRow(formkit::editLabel(QStringLiteral("阈值范围")), rangeRow);
|
||
|
||
// 切模式清空 min/max(对照原版 handleThresholdModeChange)。
|
||
auto clearRange = [min = rc.min, max = rc.max]() { min->clear(); max->clear(); };
|
||
connect(rbNum, &QRadioButton::toggled, rc.body, [clearRange](bool on) { if (on) clearRange(); });
|
||
connect(rbPct, &QRadioButton::toggled, rc.body, [clearRange](bool on) { if (on) clearRange(); });
|
||
|
||
rc.minPoints = new QSpinBox(rc.body);
|
||
rc.minPoints->setRange(1, 100000);
|
||
rc.minPoints->setValue(kDefaultMinPoints);
|
||
form->addRow(formkit::editLabel(QStringLiteral("最小点数")), rc.minPoints);
|
||
|
||
// 空态感知下拉:异常类型异步加载(loadExceptionTypes),未选显占位、无数据弹「暂无数据」。
|
||
rc.type = formkit::comboBox(QStringLiteral("请选择异常类型"), rc.body);
|
||
for (const QJsonValue& ov : exceptionTypeOptions_) {
|
||
const QJsonObject o = ov.toObject();
|
||
rc.type->addItem(o.value(QStringLiteral("name")).toString(),
|
||
o.value(QStringLiteral("id")).toString());
|
||
}
|
||
form->addRow(formkit::editLabel(QStringLiteral("异常类型")), rc.type);
|
||
|
||
rc.body->setLayout(form);
|
||
cardLay->addWidget(rc.body);
|
||
|
||
// 折叠/展开。
|
||
connect(collapseBtn, &QToolButton::toggled, rc.body, [rc, collapseBtn](bool collapsed) {
|
||
rc.body->setVisible(!collapsed);
|
||
collapseBtn->setText(collapsed ? QStringLiteral("▶") : QStringLiteral("▼"));
|
||
});
|
||
// 删除(至少保留一条)。
|
||
connect(delBtn, &QToolButton::clicked, this, [this, card]() { removeRule(card); });
|
||
|
||
rules_.push_back(rc);
|
||
ruleHost_->addWidget(card);
|
||
renumberRules();
|
||
}
|
||
|
||
void AutoAnnotationDialog::removeRule(QWidget* frame) {
|
||
if (rules_.size() <= 1) { // 对照原版 atLeastOneRule。
|
||
QMessageBox::warning(this, windowTitle(), QStringLiteral("至少保留一条规则"));
|
||
return;
|
||
}
|
||
auto it = std::find_if(rules_.begin(), rules_.end(),
|
||
[frame](const RuleCard& c) { return c.frame == frame; });
|
||
if (it == rules_.end()) return;
|
||
ruleHost_->removeWidget(frame);
|
||
frame->deleteLater();
|
||
rules_.erase(it);
|
||
renumberRules();
|
||
}
|
||
|
||
void AutoAnnotationDialog::renumberRules() {
|
||
for (int i = 0; i < static_cast<int>(rules_.size()); ++i)
|
||
rules_[i].title->setText(QStringLiteral("规则 %1").arg(i + 1));
|
||
}
|
||
|
||
int AutoAnnotationDialog::currentMode(const RuleCard& c) const {
|
||
const int id = c.modeGroup->checkedId();
|
||
return id > 0 ? id : 1; // 默认数值
|
||
}
|
||
|
||
QJsonArray AutoAnnotationDialog::buildRuleList() const {
|
||
QJsonArray arr;
|
||
for (const auto& r : rules_) {
|
||
QJsonObject rule{
|
||
{QStringLiteral("exceptionTypeId"), r.type->currentData().toString()},
|
||
{QStringLiteral("thresholdMode"), currentMode(r)},
|
||
{QStringLiteral("minPointCount"), r.minPoints->value()},
|
||
};
|
||
// min/max:空 → null(对照原版 Number(...) 或 null)。
|
||
const QString mn = r.min->text().trimmed();
|
||
const QString mx = r.max->text().trimmed();
|
||
rule.insert(QStringLiteral("thresholdMin"),
|
||
mn.isEmpty() ? QJsonValue(QJsonValue::Null) : QJsonValue(mn.toDouble()));
|
||
rule.insert(QStringLiteral("thresholdMax"),
|
||
mx.isEmpty() ? QJsonValue(QJsonValue::Null) : QJsonValue(mx.toDouble()));
|
||
arr.append(rule);
|
||
}
|
||
return arr;
|
||
}
|
||
|
||
void AutoAnnotationDialog::onExecute() {
|
||
if (!repo_) return;
|
||
// 校验:每条规则 min/max 至少填一个、异常类型必选(对照原版 handleExecute)。
|
||
for (const auto& r : rules_) {
|
||
if (r.min->text().trimmed().isEmpty() && r.max->text().trimmed().isEmpty()) {
|
||
QMessageBox::warning(this, windowTitle(), QStringLiteral("阈值范围至少填写一项"));
|
||
return;
|
||
}
|
||
if (r.type->currentData().toString().isEmpty()) {
|
||
QMessageBox::warning(this, windowTitle(), QStringLiteral("请选择异常类型"));
|
||
return;
|
||
}
|
||
}
|
||
const QJsonArray rules = buildRuleList();
|
||
QJsonObject body{
|
||
{QStringLiteral("dsObjectId"), dsObjectId_},
|
||
{QStringLiteral("projectId"), projectId_},
|
||
{QStringLiteral("exceptionMarkRuleList"), rules},
|
||
};
|
||
QPointer<AutoAnnotationDialog> self(this);
|
||
repo_->executeExceptionMark(body, [self](bool ok, QJsonObject data, QString msg) {
|
||
if (!self) return;
|
||
if (!ok) {
|
||
QMessageBox::warning(self, self->windowTitle(),
|
||
msg.isEmpty() ? QStringLiteral("自动标注执行失败") : msg);
|
||
return;
|
||
}
|
||
// 预览异常:兼容 data 直接为数组(wireObject 包成 value) 或 data.list。
|
||
QJsonArray list = data.value(QStringLiteral("value")).toArray();
|
||
if (list.isEmpty()) list = data.value(QStringLiteral("list")).toArray();
|
||
self->previewExceptions_ = list;
|
||
self->detectedLabel_->setText(
|
||
QStringLiteral("自动标注结果(共识别到 %1 个异常)").arg(list.size()));
|
||
self->previewTable_->setRowCount(list.size());
|
||
for (int i = 0; i < list.size(); ++i) {
|
||
const QJsonObject o = list[i].toObject();
|
||
self->previewTable_->setItem(i, 0, new QTableWidgetItem(QString::number(i + 1)));
|
||
self->previewTable_->setItem(
|
||
i, 1, new QTableWidgetItem(o.value(QStringLiteral("exceptionName")).toString()));
|
||
self->previewTable_->setItem(
|
||
i, 2,
|
||
new QTableWidgetItem(o.value(QStringLiteral("exceptionTypeName")).toString()));
|
||
self->previewTable_->setItem(
|
||
i, 3, new QTableWidgetItem(o.value(QStringLiteral("remark")).toString()));
|
||
self->previewTable_->setItem(
|
||
i, 4,
|
||
new QTableWidgetItem(o.value(QStringLiteral("thresholdModeName")).toString()));
|
||
// 操作列:逐条删除(对照原版预览表 删除)。
|
||
auto* delBtn = new QPushButton(QStringLiteral("删除"), self->previewTable_);
|
||
QPointer<AutoAnnotationDialog> weak(self);
|
||
QObject::connect(delBtn, &QPushButton::clicked, self, [weak, i]() {
|
||
if (weak) weak->deletePreviewRow(i);
|
||
});
|
||
self->previewTable_->setCellWidget(i, 5, delBtn);
|
||
}
|
||
self->saveBtn_->setEnabled(!list.isEmpty());
|
||
self->refreshPreviewAnomalies(); // 实时叠加预演异常到预览图
|
||
if (list.isEmpty())
|
||
QMessageBox::information(self, self->windowTitle(), QStringLiteral("暂未识别到异常"));
|
||
});
|
||
}
|
||
|
||
void AutoAnnotationDialog::deletePreviewRow(int row) {
|
||
if (row < 0 || row >= previewExceptions_.size()) return;
|
||
const QString name = previewExceptions_[row].toObject().value(QStringLiteral("exceptionName")).toString();
|
||
if (QMessageBox::question(this, QStringLiteral("确认删除"),
|
||
QStringLiteral("%1,确认删除?").arg(name)) != QMessageBox::Yes)
|
||
return;
|
||
previewExceptions_.removeAt(row);
|
||
// 重建预览表(重排序号 + 重绑删除)。复用 onExecute 的填表分支会重发请求,故就地重建。
|
||
previewTable_->setRowCount(previewExceptions_.size());
|
||
for (int i = 0; i < previewExceptions_.size(); ++i) {
|
||
const QJsonObject o = previewExceptions_[i].toObject();
|
||
previewTable_->setItem(i, 0, new QTableWidgetItem(QString::number(i + 1)));
|
||
previewTable_->setItem(
|
||
i, 1, new QTableWidgetItem(o.value(QStringLiteral("exceptionName")).toString()));
|
||
previewTable_->setItem(
|
||
i, 2, new QTableWidgetItem(o.value(QStringLiteral("exceptionTypeName")).toString()));
|
||
previewTable_->setItem(i, 3,
|
||
new QTableWidgetItem(o.value(QStringLiteral("remark")).toString()));
|
||
previewTable_->setItem(
|
||
i, 4, new QTableWidgetItem(o.value(QStringLiteral("thresholdModeName")).toString()));
|
||
auto* delBtn = new QPushButton(QStringLiteral("删除"), previewTable_);
|
||
QPointer<AutoAnnotationDialog> weak(this);
|
||
connect(delBtn, &QPushButton::clicked, this, [weak, i]() {
|
||
if (weak) weak->deletePreviewRow(i);
|
||
});
|
||
previewTable_->setCellWidget(i, 5, delBtn);
|
||
}
|
||
detectedLabel_->setText(
|
||
QStringLiteral("自动标注结果(共识别到 %1 个异常)").arg(previewExceptions_.size()));
|
||
saveBtn_->setEnabled(!previewExceptions_.isEmpty());
|
||
refreshPreviewAnomalies(); // 同步从预览图移除该异常
|
||
}
|
||
|
||
void AutoAnnotationDialog::onSave() {
|
||
if (!repo_ || previewExceptions_.isEmpty()) {
|
||
QMessageBox::warning(this, windowTitle(), QStringLiteral("暂无可保存的异常,请先执行自动标注"));
|
||
return;
|
||
}
|
||
// 组装 exceptionList:保留 execute 返回项的关键字段(对照原版 batchCreateException)。
|
||
QJsonArray exceptionList;
|
||
for (const QJsonValue& v : previewExceptions_) {
|
||
const QJsonObject o = v.toObject();
|
||
exceptionList.append(QJsonObject{
|
||
{QStringLiteral("remarkSourceType"), o.value(QStringLiteral("exceptionMarkType"))},
|
||
{QStringLiteral("exceptionName"), o.value(QStringLiteral("exceptionName"))},
|
||
{QStringLiteral("exceptionTypeId"), o.value(QStringLiteral("exceptionTypeId"))},
|
||
{QStringLiteral("location"), o.value(QStringLiteral("location"))},
|
||
{QStringLiteral("remark"), o.value(QStringLiteral("remark"))},
|
||
});
|
||
}
|
||
QJsonObject body{
|
||
{QStringLiteral("projectId"), projectId_},
|
||
{QStringLiteral("remarkSourceId"), dsObjectId_},
|
||
{QStringLiteral("exceptionList"), exceptionList},
|
||
};
|
||
saveBtn_->setEnabled(false);
|
||
QPointer<AutoAnnotationDialog> self(this);
|
||
repo_->batchCreateException(body, [self](bool ok, QString msg) {
|
||
if (!self) return;
|
||
self->saveBtn_->setEnabled(true);
|
||
if (ok) {
|
||
self->accept();
|
||
} else {
|
||
QMessageBox::warning(self, self->windowTitle(),
|
||
msg.isEmpty() ? QStringLiteral("保存失败") : msg);
|
||
}
|
||
});
|
||
}
|
||
|
||
} // namespace geopro::app
|