feat/vtk-3d-view #7
|
|
@ -38,12 +38,14 @@ add_executable(geopro_desktop WIN32
|
||||||
panels/DatasetAttrPanel.cpp
|
panels/DatasetAttrPanel.cpp
|
||||||
panels/ObjectExceptionPanel.cpp
|
panels/ObjectExceptionPanel.cpp
|
||||||
panels/DescriptionPanel.cpp
|
panels/DescriptionPanel.cpp
|
||||||
|
panels/QuillDelta.cpp
|
||||||
panels/chart/RawDataChartView.cpp
|
panels/chart/RawDataChartView.cpp
|
||||||
panels/chart/InversionFormDialog.cpp
|
panels/chart/InversionFormDialog.cpp
|
||||||
panels/chart/InversionFormParse.cpp
|
panels/chart/InversionFormParse.cpp
|
||||||
panels/chart/ScatterDataOps.cpp
|
panels/chart/ScatterDataOps.cpp
|
||||||
panels/chart/SaveAsDialog.cpp
|
panels/chart/SaveAsDialog.cpp
|
||||||
panels/chart/ScatterFilterDialog.cpp
|
panels/chart/ScatterFilterDialog.cpp
|
||||||
|
panels/chart/ScatterHistogram.cpp
|
||||||
panels/chart/InversionProcessOps.cpp
|
panels/chart/InversionProcessOps.cpp
|
||||||
panels/chart/GridWizardDialog.cpp
|
panels/chart/GridWizardDialog.cpp
|
||||||
panels/chart/WhiteningDialog.cpp
|
panels/chart/WhiteningDialog.cpp
|
||||||
|
|
@ -70,6 +72,9 @@ add_executable(geopro_desktop WIN32
|
||||||
panels/chart/ContourPlotItem.cpp
|
panels/chart/ContourPlotItem.cpp
|
||||||
panels/chart/LivePanner.cpp
|
panels/chart/LivePanner.cpp
|
||||||
panels/chart/ScatterHoverTip.cpp
|
panels/chart/ScatterHoverTip.cpp
|
||||||
|
panels/chart/ChartPickGeometry.cpp
|
||||||
|
panels/chart/ScatterMarqueePicker.cpp
|
||||||
|
panels/chart/ContourDrawTool.cpp
|
||||||
panels/columns/Column2DDataset.cpp
|
panels/columns/Column2DDataset.cpp
|
||||||
panels/columns/Column3DDataset.cpp
|
panels/columns/Column3DDataset.cpp
|
||||||
panels/columns/Column3DAnalysis.cpp
|
panels/columns/Column3DAnalysis.cpp
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,39 @@
|
||||||
#include "panels/DescriptionPanel.hpp"
|
#include "panels/DescriptionPanel.hpp"
|
||||||
|
|
||||||
|
#include <QColorDialog>
|
||||||
|
#include <QComboBox>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QTextCharFormat>
|
||||||
#include <QTextEdit>
|
#include <QTextEdit>
|
||||||
|
#include <QTextListFormat>
|
||||||
|
#include <QToolBar>
|
||||||
|
#include <QToolButton>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
|
#include "panels/QuillDelta.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// 字号下拉选项(px)——对照原版 ql-size 的 12~32px。
|
||||||
|
const int kFontSizesPx[] = {12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
DescriptionPanel::DescriptionPanel(QWidget* parent) : QWidget(parent) {
|
DescriptionPanel::DescriptionPanel(QWidget* parent) : QWidget(parent) {
|
||||||
auto* lay = new QVBoxLayout(this);
|
auto* lay = new QVBoxLayout(this);
|
||||||
lay->setContentsMargins(geopro::app::space::kMd, geopro::app::space::kMd,
|
lay->setContentsMargins(geopro::app::space::kMd, geopro::app::space::kMd,
|
||||||
geopro::app::space::kMd, geopro::app::space::kMd);
|
geopro::app::space::kMd, geopro::app::space::kMd);
|
||||||
|
lay->setSpacing(geopro::app::space::kSm);
|
||||||
|
|
||||||
|
auto* tb = new QToolBar(this);
|
||||||
|
buildToolbar(tb);
|
||||||
|
lay->addWidget(tb);
|
||||||
|
|
||||||
edit_ = new QTextEdit(this);
|
edit_ = new QTextEdit(this);
|
||||||
|
edit_->setAcceptRichText(true);
|
||||||
edit_->setPlaceholderText(QStringLiteral("暂无描述"));
|
edit_->setPlaceholderText(QStringLiteral("暂无描述"));
|
||||||
lay->addWidget(edit_, 1);
|
lay->addWidget(edit_, 1);
|
||||||
|
|
||||||
|
|
@ -25,13 +44,96 @@ DescriptionPanel::DescriptionPanel(QWidget* parent) : QWidget(parent) {
|
||||||
btnLay->addWidget(saveBtn_);
|
btnLay->addWidget(saveBtn_);
|
||||||
lay->addLayout(btnLay);
|
lay->addLayout(btnLay);
|
||||||
|
|
||||||
connect(saveBtn_, &QPushButton::clicked, this,
|
connect(saveBtn_, &QPushButton::clicked, this, [this]() { emit saveRequested(); });
|
||||||
[this]() { emit saveRequested(edit_->toPlainText()); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DescriptionPanel::setText(const QString& text) { edit_->setPlainText(text); }
|
void DescriptionPanel::buildToolbar(QToolBar* tb) {
|
||||||
|
// 粗体 / 斜体 / 下划线:可勾选按钮,作用于选区当前字符格式。
|
||||||
|
auto addToggle = [this, tb](const QString& label, auto applier) {
|
||||||
|
auto* btn = new QToolButton(tb);
|
||||||
|
btn->setText(label);
|
||||||
|
btn->setCheckable(true);
|
||||||
|
tb->addWidget(btn);
|
||||||
|
connect(btn, &QToolButton::toggled, this, applier);
|
||||||
|
return btn;
|
||||||
|
};
|
||||||
|
addToggle(QStringLiteral("B"), [this](bool on) {
|
||||||
|
QTextCharFormat f;
|
||||||
|
f.setFontWeight(on ? QFont::Bold : QFont::Normal);
|
||||||
|
edit_->mergeCurrentCharFormat(f);
|
||||||
|
});
|
||||||
|
addToggle(QStringLiteral("I"), [this](bool on) {
|
||||||
|
QTextCharFormat f;
|
||||||
|
f.setFontItalic(on);
|
||||||
|
edit_->mergeCurrentCharFormat(f);
|
||||||
|
});
|
||||||
|
addToggle(QStringLiteral("U"), [this](bool on) {
|
||||||
|
QTextCharFormat f;
|
||||||
|
f.setFontUnderline(on);
|
||||||
|
edit_->mergeCurrentCharFormat(f);
|
||||||
|
});
|
||||||
|
|
||||||
QString DescriptionPanel::text() const { return edit_->toPlainText(); }
|
// 字色:弹色板,作用于选区前景色。
|
||||||
|
auto* colorBtn = new QToolButton(tb);
|
||||||
|
colorBtn->setText(QStringLiteral("A"));
|
||||||
|
tb->addWidget(colorBtn);
|
||||||
|
connect(colorBtn, &QToolButton::clicked, this, [this]() {
|
||||||
|
const QColor c = QColorDialog::getColor(Qt::black, this, QStringLiteral("字体颜色"));
|
||||||
|
if (!c.isValid()) return;
|
||||||
|
QTextCharFormat f;
|
||||||
|
f.setForeground(c);
|
||||||
|
edit_->mergeCurrentCharFormat(f);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 字号下拉(px)。
|
||||||
|
auto* sizeBox = new QComboBox(tb);
|
||||||
|
for (int px : kFontSizesPx) sizeBox->addItem(QStringLiteral("%1px").arg(px), px);
|
||||||
|
sizeBox->setCurrentIndex(2); // 默认 16px(与原版一致)。
|
||||||
|
tb->addWidget(sizeBox);
|
||||||
|
connect(sizeBox, &QComboBox::currentIndexChanged, this, [this, sizeBox](int) {
|
||||||
|
const int px = sizeBox->currentData().toInt();
|
||||||
|
QTextCharFormat f;
|
||||||
|
f.setFontPointSize(px * 3.0 / 4.0); // px→pt。
|
||||||
|
edit_->mergeCurrentCharFormat(f);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 标题下拉(正文 / H1~H4)——块级,作用于当前段。
|
||||||
|
auto* headerBox = new QComboBox(tb);
|
||||||
|
headerBox->addItem(QStringLiteral("正文"), 0);
|
||||||
|
for (int h = 1; h <= 4; ++h) headerBox->addItem(QStringLiteral("标题%1").arg(h), h);
|
||||||
|
tb->addWidget(headerBox);
|
||||||
|
connect(headerBox, &QComboBox::currentIndexChanged, this, [this, headerBox](int) {
|
||||||
|
const int h = headerBox->currentData().toInt();
|
||||||
|
QTextCursor cur = edit_->textCursor();
|
||||||
|
QTextBlockFormat bf = cur.blockFormat();
|
||||||
|
bf.setHeadingLevel(h); // 0 表示正文。
|
||||||
|
cur.mergeBlockFormat(bf);
|
||||||
|
edit_->setTextCursor(cur);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 有序 / 无序列表——块级。
|
||||||
|
auto addListBtn = [this, tb](const QString& label, QTextListFormat::Style style) {
|
||||||
|
auto* btn = new QToolButton(tb);
|
||||||
|
btn->setText(label);
|
||||||
|
tb->addWidget(btn);
|
||||||
|
connect(btn, &QToolButton::clicked, this, [this, style]() {
|
||||||
|
edit_->textCursor().createList(style);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
addListBtn(QStringLiteral("1."), QTextListFormat::ListDecimal);
|
||||||
|
addListBtn(QStringLiteral("•"), QTextListFormat::ListDisc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DescriptionPanel::setDelta(const QJsonArray& ops) {
|
||||||
|
if (ops.isEmpty()) return;
|
||||||
|
deltaToDocument(ops, *edit_->document());
|
||||||
|
}
|
||||||
|
|
||||||
|
void DescriptionPanel::setPlainText(const QString& text) { edit_->setPlainText(text); }
|
||||||
|
|
||||||
|
QJsonArray DescriptionPanel::delta() const { return documentToDelta(*edit_->document()); }
|
||||||
|
|
||||||
|
QString DescriptionPanel::plainText() const { return edit_->toPlainText(); }
|
||||||
|
|
||||||
void DescriptionPanel::setSaveEnabled(bool on) { saveBtn_->setEnabled(on); }
|
void DescriptionPanel::setSaveEnabled(bool on) { saveBtn_->setEnabled(on); }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,38 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <QJsonArray>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
class QTextEdit;
|
class QTextEdit;
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
|
class QToolBar;
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
// 数据集描述面板:可编辑文本 + 保存按钮(I14)。
|
// 数据集描述面板:富文本编辑器 + 格式工具栏 + 保存按钮(I14)。
|
||||||
// 原版用 Quill 富文本(Delta),Qt 无对应控件 → 退化为纯文本编辑 + 保存;
|
// 对照原版 web(contourPage.vue)的 Quill 编辑器:粗体/斜体/下划线/字色/字号 +
|
||||||
// 保存时由调用方组装 {description, attachedParameters:{deltaContent}}(见 GridDataChartView)。
|
// 有序/无序列表 + 标题。保存时把富文本转 Quill Delta(attachedParameters.deltaContent)
|
||||||
|
// 与纯文本(description)一并提交(组装/请求见 GridDataChartView)。
|
||||||
|
// Delta↔QTextDocument 互转见 QuillDelta.{hpp,cpp}(纯函数,可单测)。
|
||||||
class DescriptionPanel : public QWidget {
|
class DescriptionPanel : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit DescriptionPanel(QWidget* parent = nullptr);
|
explicit DescriptionPanel(QWidget* parent = nullptr);
|
||||||
void setText(const QString& text);
|
|
||||||
QString text() const;
|
// 用 Quill Delta ops 回填编辑器(无 ops 时回退 setPlainText 兜底)。
|
||||||
|
void setDelta(const QJsonArray& ops);
|
||||||
|
void setPlainText(const QString& text);
|
||||||
|
|
||||||
|
// 当前内容导出:Delta ops(与原版 deltaContent 兼容)+ 纯文本(description)。
|
||||||
|
QJsonArray delta() const;
|
||||||
|
QString plainText() const;
|
||||||
|
|
||||||
// 注入「保存」可用性:无 cmdRepo/dsId 时禁用保存按钮(占位)。
|
// 注入「保存」可用性:无 cmdRepo/dsId 时禁用保存按钮(占位)。
|
||||||
void setSaveEnabled(bool on);
|
void setSaveEnabled(bool on);
|
||||||
signals:
|
signals:
|
||||||
void saveRequested(const QString& text);
|
void saveRequested();
|
||||||
private:
|
private:
|
||||||
|
void buildToolbar(QToolBar* tb);
|
||||||
|
|
||||||
QTextEdit* edit_;
|
QTextEdit* edit_;
|
||||||
QPushButton* saveBtn_;
|
QPushButton* saveBtn_;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
#include "panels/QuillDelta.hpp"
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QTextBlock>
|
||||||
|
#include <QTextCharFormat>
|
||||||
|
#include <QTextDocument>
|
||||||
|
#include <QTextList>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// ── 序列化方向(QTextDocument → Delta)─────────────────────────────────────
|
||||||
|
|
||||||
|
// 颜色转 Quill 习惯的小写 #rrggbb(与原版 ql-color/ql-background 选项一致)。
|
||||||
|
QString hexOf(const QColor& c) { return c.name(QColor::HexRgb); }
|
||||||
|
|
||||||
|
// 行内样式 → attributes 对象(bold/italic/underline/color/background/size)。
|
||||||
|
QJsonObject inlineAttrs(const QTextCharFormat& fmt) {
|
||||||
|
QJsonObject a;
|
||||||
|
if (fmt.fontWeight() >= QFont::Bold) a[QStringLiteral("bold")] = true;
|
||||||
|
if (fmt.fontItalic()) a[QStringLiteral("italic")] = true;
|
||||||
|
if (fmt.fontUnderline()) a[QStringLiteral("underline")] = true;
|
||||||
|
if (fmt.foreground().style() != Qt::NoBrush)
|
||||||
|
a[QStringLiteral("color")] = hexOf(fmt.foreground().color());
|
||||||
|
if (fmt.background().style() != Qt::NoBrush)
|
||||||
|
a[QStringLiteral("background")] = hexOf(fmt.background().color());
|
||||||
|
const double pt = fmt.fontPointSize();
|
||||||
|
if (pt > 0.0) // 以 px 表达(原版 ql-size 用 "NNpx";pt→px 约 *4/3 取整)。
|
||||||
|
a[QStringLiteral("size")] = QStringLiteral("%1px").arg(qRound(pt * 4.0 / 3.0));
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 块级样式 → attributes 对象(header/list/align),挂在换行 op 上。
|
||||||
|
QJsonObject blockAttrs(const QTextBlock& block) {
|
||||||
|
QJsonObject a;
|
||||||
|
const int headingLevel = block.blockFormat().headingLevel();
|
||||||
|
if (headingLevel >= 1 && headingLevel <= 4) a[QStringLiteral("header")] = headingLevel;
|
||||||
|
if (QTextList* lst = block.textList()) {
|
||||||
|
const QTextListFormat::Style s = lst->format().style();
|
||||||
|
a[QStringLiteral("list")] = (s == QTextListFormat::ListDecimal)
|
||||||
|
? QStringLiteral("ordered")
|
||||||
|
: QStringLiteral("bullet");
|
||||||
|
}
|
||||||
|
switch (block.blockFormat().alignment() & Qt::AlignHorizontal_Mask) {
|
||||||
|
case Qt::AlignHCenter: a[QStringLiteral("align")] = QStringLiteral("center"); break;
|
||||||
|
case Qt::AlignRight: a[QStringLiteral("align")] = QStringLiteral("right"); break;
|
||||||
|
case Qt::AlignJustify: a[QStringLiteral("align")] = QStringLiteral("justify"); break;
|
||||||
|
default: break; // 左对齐为默认,不写 attribute。
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加一个文本 op(带可选 attributes)。
|
||||||
|
void pushInsert(QJsonArray& ops, const QString& text, const QJsonObject& attrs) {
|
||||||
|
if (text.isEmpty()) return;
|
||||||
|
QJsonObject op{{QStringLiteral("insert"), text}};
|
||||||
|
if (!attrs.isEmpty()) op[QStringLiteral("attributes")] = attrs;
|
||||||
|
ops.append(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加一个「换行」op(带可选块级 attributes)。Quill 中块级样式作用于其前整行。
|
||||||
|
void pushNewline(QJsonArray& ops, const QJsonObject& attrs) {
|
||||||
|
pushInsert(ops, QStringLiteral("\n"), attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 序列化单个文本块的所有 fragment(按行内样式分段)。
|
||||||
|
void serializeBlock(const QTextBlock& block, QJsonArray& ops) {
|
||||||
|
for (auto it = block.begin(); it != block.end(); ++it) {
|
||||||
|
const QTextFragment frag = it.fragment();
|
||||||
|
if (frag.isValid()) pushInsert(ops, frag.text(), inlineAttrs(frag.charFormat()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 反序列化方向(Delta → QTextDocument)──────────────────────────────────
|
||||||
|
|
||||||
|
// 把 inline attributes 应用到字符格式。
|
||||||
|
void applyInlineAttrs(const QJsonObject& attrs, QTextCharFormat& fmt) {
|
||||||
|
if (attrs.value(QStringLiteral("bold")).toBool()) fmt.setFontWeight(QFont::Bold);
|
||||||
|
if (attrs.value(QStringLiteral("italic")).toBool()) fmt.setFontItalic(true);
|
||||||
|
if (attrs.value(QStringLiteral("underline")).toBool()) fmt.setFontUnderline(true);
|
||||||
|
const QString color = attrs.value(QStringLiteral("color")).toString();
|
||||||
|
if (QColor(color).isValid()) fmt.setForeground(QColor(color));
|
||||||
|
const QString bg = attrs.value(QStringLiteral("background")).toString();
|
||||||
|
if (QColor(bg).isValid()) fmt.setBackground(QColor(bg));
|
||||||
|
QString size = attrs.value(QStringLiteral("size")).toString();
|
||||||
|
if (size.endsWith(QStringLiteral("px"))) {
|
||||||
|
const double px = size.chopped(2).toDouble();
|
||||||
|
if (px > 0.0) fmt.setFontPointSize(px * 3.0 / 4.0); // px→pt。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 把 block attributes 应用到块格式 / 列表(作用于换行前的当前块)。
|
||||||
|
void applyBlockAttrs(const QJsonObject& attrs, QTextCursor& cur) {
|
||||||
|
QTextBlockFormat bf = cur.blockFormat();
|
||||||
|
const int header = attrs.value(QStringLiteral("header")).toInt();
|
||||||
|
if (header >= 1 && header <= 4) bf.setHeadingLevel(header);
|
||||||
|
const QString align = attrs.value(QStringLiteral("align")).toString();
|
||||||
|
if (align == QStringLiteral("center")) bf.setAlignment(Qt::AlignHCenter);
|
||||||
|
else if (align == QStringLiteral("right")) bf.setAlignment(Qt::AlignRight);
|
||||||
|
else if (align == QStringLiteral("justify")) bf.setAlignment(Qt::AlignJustify);
|
||||||
|
cur.setBlockFormat(bf);
|
||||||
|
const QString list = attrs.value(QStringLiteral("list")).toString();
|
||||||
|
if (list == QStringLiteral("ordered")) cur.createList(QTextListFormat::ListDecimal);
|
||||||
|
else if (list == QStringLiteral("bullet")) cur.createList(QTextListFormat::ListDisc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入一段不含换行的文本(带行内样式)。
|
||||||
|
void insertSegment(QTextCursor& cur, const QString& text, const QJsonObject& attrs) {
|
||||||
|
QTextCharFormat fmt;
|
||||||
|
applyInlineAttrs(attrs, fmt);
|
||||||
|
cur.insertText(text, fmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理单个 insert op:按换行拆段。每个换行:先对当前块应用块级样式(header/list/align),
|
||||||
|
// 再开新块承接后续内容。Delta 末尾必有一个收尾换行,会多造一个尾部空块,由
|
||||||
|
// deltaToDocument 末尾统一裁掉(与 QTextDocument 起始即含一个空块的语义对齐)。
|
||||||
|
void applyOp(const QString& insert, const QJsonObject& attrs, QTextCursor& cur) {
|
||||||
|
const QStringList lines = insert.split(QLatin1Char('\n'));
|
||||||
|
for (int i = 0; i < lines.size(); ++i) {
|
||||||
|
insertSegment(cur, lines.at(i), attrs);
|
||||||
|
if (i + 1 < lines.size()) { // 该位置原本是一个换行符。
|
||||||
|
applyBlockAttrs(attrs, cur); // 块级样式作用于换行之前的整行。
|
||||||
|
cur.insertBlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 裁掉文档末尾因 Delta 收尾换行而多出的空块(仅当其确为空且非唯一块)。
|
||||||
|
void trimTrailingEmptyBlock(QTextDocument& doc) {
|
||||||
|
if (doc.blockCount() <= 1) return;
|
||||||
|
const QTextBlock last = doc.lastBlock();
|
||||||
|
if (!last.text().isEmpty()) return;
|
||||||
|
QTextCursor cur(&doc);
|
||||||
|
cur.movePosition(QTextCursor::End);
|
||||||
|
cur.deletePreviousChar(); // 删除上一个块结尾的换行,合并末尾空块。
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QJsonArray documentToDelta(const QTextDocument& doc) {
|
||||||
|
QJsonArray ops;
|
||||||
|
for (QTextBlock block = doc.begin(); block.isValid(); block = block.next()) {
|
||||||
|
serializeBlock(block, ops);
|
||||||
|
// 每个块以换行 op 收尾,携带该块的块级样式(最后一个块也写,对应 Quill 末尾换行)。
|
||||||
|
pushNewline(ops, blockAttrs(block));
|
||||||
|
}
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
void deltaToDocument(const QJsonArray& ops, QTextDocument& doc) {
|
||||||
|
doc.clear();
|
||||||
|
QTextCursor cur(&doc);
|
||||||
|
for (const QJsonValue& v : ops) {
|
||||||
|
const QJsonObject op = v.toObject();
|
||||||
|
const QJsonValue insert = op.value(QStringLiteral("insert"));
|
||||||
|
if (!insert.isString()) continue; // 仅支持文本 insert(图片/嵌入等降级丢弃)。
|
||||||
|
applyOp(insert.toString(), op.value(QStringLiteral("attributes")).toObject(), cur);
|
||||||
|
}
|
||||||
|
trimTrailingEmptyBlock(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QJsonArray>
|
||||||
|
|
||||||
|
class QTextDocument;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// Quill Delta ↔ QTextDocument 互转(纯函数,仅依赖 Qt Core/Gui,无 Widgets/MOC)。
|
||||||
|
//
|
||||||
|
// 背景:原版 web(contourPage.vue)描述用 Quill 富文本,保存
|
||||||
|
// attachedParameters.deltaContent = quill.getContents().ops(Quill Delta ops 数组),
|
||||||
|
// description = quill.getText()(纯文本)。读取时 quill.setContents(deltaContent)。
|
||||||
|
// 客户端无 Quill,这里用 QTextDocument 承载富文本,并在两种表示间转换以与原版互通。
|
||||||
|
//
|
||||||
|
// 边界(无法做到字节级 1:1,目标是「常见格式往返可用」):
|
||||||
|
// - 支持的 inline attributes:bold / italic / underline / color / background / size("NNpx")。
|
||||||
|
// - 支持的 block attributes(挂在换行 op 上):header(1-4)/ list("ordered"|"bullet")/ align。
|
||||||
|
// - 不支持的 attributes 容错降级:保留 insert 文本,丢弃无法表达的样式,不崩。
|
||||||
|
// - 拆出独立 TU 便于 gtest(往返断言常见格式)。
|
||||||
|
//
|
||||||
|
// Quill Delta 结构:ops 为数组,每个 op = { "insert": "文本", "attributes": {...} }。
|
||||||
|
// 行内样式挂在文本 op 上;块级样式(标题/列表/对齐)挂在「单个换行」op 的 attributes 上,
|
||||||
|
// 作用于该换行之前的整行。文档以隐含的「最后一个换行」结尾。
|
||||||
|
|
||||||
|
// 把 QTextDocument 序列化为 Quill Delta ops 数组(与原版 quill.getContents().ops 兼容)。
|
||||||
|
QJsonArray documentToDelta(const QTextDocument& doc);
|
||||||
|
|
||||||
|
// 把 Quill Delta ops 数组反序列化进 QTextDocument(与原版 quill.setContents(ops) 对应)。
|
||||||
|
// 先清空 doc 再写入;无法识别的 attributes 跳过。
|
||||||
|
void deltaToDocument(const QJsonArray& ops, QTextDocument& doc);
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
#include "panels/chart/ChartPickGeometry.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
std::vector<int> pointsInRect(const geopro::core::ScatterField& field, const QRectF& rect) {
|
||||||
|
std::vector<int> hits;
|
||||||
|
const auto& xs = field.x;
|
||||||
|
const auto& ys = field.y;
|
||||||
|
const std::size_t n = std::min(xs.size(), ys.size());
|
||||||
|
const bool hasStatus = field.displayStatus.size() == n;
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
const double x = xs[i];
|
||||||
|
const double y = ys[i];
|
||||||
|
if (!std::isfinite(x) || !std::isfinite(y)) continue; // 脏数据跳过
|
||||||
|
if (hasStatus && field.displayStatus[i] != 0) continue; // 隐藏点不参与框选
|
||||||
|
if (rect.contains(x, y)) hits.push_back(static_cast<int>(i));
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
int minPointsForMarkType(int markType) {
|
||||||
|
if (markType == 2) return 2; // 线
|
||||||
|
if (markType == 3) return 3; // 面
|
||||||
|
return 1; // 点(1)/文字(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<QPointF> normalizeDrawnPoints(const std::vector<QPointF>& pts, int markType) {
|
||||||
|
if (pts.empty()) return {};
|
||||||
|
// 点/文字:单点定位,仅取首点(即便误收集多点)。
|
||||||
|
if (markType == 1 || markType == 4) return {pts.front()};
|
||||||
|
// 线/面:保留全部顶点(不足/闭合由调用方校验)。
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QRectF>
|
||||||
|
|
||||||
|
#include "model/Field.hpp"
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 图上交互的纯几何逻辑(无 Qt Widgets / Qwt 依赖,可独立单测)。
|
||||||
|
// M14 框选命中 + I9 绘形归一化共用。
|
||||||
|
|
||||||
|
// M14 框选命中:返回散点 x/y 落在数据坐标矩形 rect 内的下标集合。
|
||||||
|
// rect 用数据坐标(调用方已把像素橡皮筋反变换为数据坐标,并 normalized)。
|
||||||
|
// 只测有限值;x/y 长度不一致取 min 防越界;隐藏点(displayStatus!=0)跳过(与原版
|
||||||
|
// box-select 仅命中可见点一致)。
|
||||||
|
std::vector<int> pointsInRect(const geopro::core::ScatterField& field, const QRectF& rect);
|
||||||
|
|
||||||
|
// I9 绘形:判断多边形是否「可闭合」(至少 3 个顶点)。线≥2、点/文字==1 的最少点数判断
|
||||||
|
// 见 ExceptionGeometry::minPointsForMarkType。此处仅多边形语义糖。
|
||||||
|
inline bool canClosePolygon(int vertexCount) { return vertexCount >= 3; }
|
||||||
|
|
||||||
|
// I9 绘形:把一串数据坐标点裁成「该标注类型的有效几何」:
|
||||||
|
// 点(1)/文字(4) → 仅取首点;线(2) → 全部(至少 2);面(3) → 全部(至少 3)。
|
||||||
|
// 超出/不足由调用方在完成时校验(minPointsForMarkType)。本函数仅做截断(点/文字取首点)。
|
||||||
|
std::vector<QPointF> normalizeDrawnPoints(const std::vector<QPointF>& pts, int markType);
|
||||||
|
|
||||||
|
// I9 绘形:各标注类型的最少点数(点1/线2/面3/文字1)。markType 为 "1".."4" 对应的整数。
|
||||||
|
int minPointsForMarkType(int markType);
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
#include "panels/chart/ContourDrawTool.hpp"
|
||||||
|
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPen>
|
||||||
|
#include <QPolygon>
|
||||||
|
#include <QResizeEvent>
|
||||||
|
#include <QToolTip>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include <qwt_plot.h>
|
||||||
|
#include <qwt_plot_canvas.h>
|
||||||
|
#include <qwt_scale_map.h>
|
||||||
|
|
||||||
|
#include "panels/chart/ChartPickGeometry.hpp"
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 透明预览覆盖层(贴 canvas):画已落点 + 连线 + 到光标的橡皮筋。无 Q_OBJECT(仅重写
|
||||||
|
// paintEvent,无信号槽),不入 MOC。透明且不吃事件(穿透给 canvas 上的 ContourDrawTool)。
|
||||||
|
class ContourDrawOverlay : public QWidget {
|
||||||
|
public:
|
||||||
|
explicit ContourDrawOverlay(QWidget* parent) : QWidget(parent) {
|
||||||
|
setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||||
|
setAttribute(Qt::WA_NoSystemBackground, true);
|
||||||
|
setAttribute(Qt::WA_TranslucentBackground, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设当前绘制态(像素坐标点 + 光标 + 类型)。markType 3=面(闭合预览)。
|
||||||
|
void setState(const QVector<QPoint>& pts, const QPoint& cursor, int markType, bool drawing) {
|
||||||
|
pts_ = pts;
|
||||||
|
cursor_ = cursor;
|
||||||
|
markType_ = markType;
|
||||||
|
drawing_ = drawing;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent*) override {
|
||||||
|
if (!drawing_) return;
|
||||||
|
QPainter p(this);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
const QColor accent(24, 144, 255); // 品牌蓝预览
|
||||||
|
QPen pen(accent, 1.5);
|
||||||
|
p.setPen(pen);
|
||||||
|
p.setBrush(Qt::NoBrush);
|
||||||
|
|
||||||
|
// 已落点连线(线/面)。
|
||||||
|
if (pts_.size() >= 2) p.drawPolyline(QPolygon(pts_));
|
||||||
|
// 橡皮筋:最后一点 → 光标。
|
||||||
|
if (!pts_.isEmpty() && !cursor_.isNull()) {
|
||||||
|
QPen dash(accent, 1.2, Qt::DashLine);
|
||||||
|
p.setPen(dash);
|
||||||
|
p.drawLine(pts_.back(), cursor_);
|
||||||
|
// 面:光标 → 首点(闭合预览)。
|
||||||
|
if (markType_ == 3 && pts_.size() >= 2) p.drawLine(cursor_, pts_.front());
|
||||||
|
p.setPen(pen);
|
||||||
|
}
|
||||||
|
// 顶点小方块。
|
||||||
|
p.setBrush(accent);
|
||||||
|
for (const QPoint& pt : pts_) p.drawRect(pt.x() - 3, pt.y() - 3, 6, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QVector<QPoint> pts_;
|
||||||
|
QPoint cursor_;
|
||||||
|
int markType_ = 0;
|
||||||
|
bool drawing_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
ContourDrawTool::ContourDrawTool(QwtPlot* plot, int xAxis, int yAxis, QObject* parent)
|
||||||
|
: QObject(parent), plot_(plot), xAxis_(xAxis), yAxis_(yAxis) {
|
||||||
|
if (plot_ && plot_->canvas()) {
|
||||||
|
overlay_ = new ContourDrawOverlay(plot_->canvas());
|
||||||
|
overlay_->setGeometry(plot_->canvas()->rect());
|
||||||
|
overlay_->hide();
|
||||||
|
// 后装于 LivePanner/hover → 绘制期优先消费事件(含 canvas resize 同步 overlay)。
|
||||||
|
plot_->canvas()->installEventFilter(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ContourDrawTool::begin(int markType) {
|
||||||
|
if (!plot_ || !plot_->canvas()) return;
|
||||||
|
active_ = true;
|
||||||
|
markType_ = markType;
|
||||||
|
dataPts_.clear();
|
||||||
|
lastCursor_ = QPoint();
|
||||||
|
plot_->canvas()->setCursor(Qt::CrossCursor);
|
||||||
|
savedFocus_ = plot_->canvas()->focusPolicy(); // 记录原焦点策略,退出时还原
|
||||||
|
savedFocusValid_ = true;
|
||||||
|
plot_->canvas()->setFocusPolicy(Qt::StrongFocus);
|
||||||
|
plot_->canvas()->setFocus(); // 让 canvas 接收回车/Esc 键
|
||||||
|
if (overlay_) {
|
||||||
|
overlay_->setGeometry(plot_->canvas()->rect());
|
||||||
|
overlay_->raise();
|
||||||
|
overlay_->show();
|
||||||
|
refreshOverlay();
|
||||||
|
}
|
||||||
|
const QString hint = (markType == 2 || markType == 3)
|
||||||
|
? QStringLiteral("逐点单击采集,双击或回车结束,Esc 取消")
|
||||||
|
: QStringLiteral("单击落点,Esc 取消");
|
||||||
|
QToolTip::showText(plot_->canvas()->mapToGlobal(QPoint(8, 8)), hint, plot_->canvas());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ContourDrawTool::restoreCanvas() {
|
||||||
|
if (plot_ && plot_->canvas()) {
|
||||||
|
plot_->canvas()->unsetCursor();
|
||||||
|
if (savedFocusValid_) plot_->canvas()->setFocusPolicy(savedFocus_); // 还原焦点策略
|
||||||
|
}
|
||||||
|
savedFocusValid_ = false;
|
||||||
|
if (overlay_) {
|
||||||
|
overlay_->setState({}, QPoint(), 0, false);
|
||||||
|
overlay_->hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ContourDrawTool::cancel() {
|
||||||
|
active_ = false;
|
||||||
|
dataPts_.clear();
|
||||||
|
restoreCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
QPointF ContourDrawTool::toData(const QPoint& canvasPos) const {
|
||||||
|
if (!plot_) return {};
|
||||||
|
const QwtScaleMap xMap = plot_->canvasMap(xAxis_);
|
||||||
|
const QwtScaleMap yMap = plot_->canvasMap(yAxis_);
|
||||||
|
return QPointF(xMap.invTransform(canvasPos.x()), yMap.invTransform(canvasPos.y()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ContourDrawTool::addVertex(const QPoint& canvasPos) {
|
||||||
|
dataPts_.push_back(toData(canvasPos));
|
||||||
|
// 点/文字:单点即完成。
|
||||||
|
if (markType_ == 1 || markType_ == 4) { finish(); return; }
|
||||||
|
refreshOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ContourDrawTool::finish() {
|
||||||
|
if (!active_) return;
|
||||||
|
auto pts = normalizeDrawnPoints(dataPts_, markType_);
|
||||||
|
if (static_cast<int>(pts.size()) < minPointsForMarkType(markType_)) {
|
||||||
|
// 点数不足(如线只点了 1 个就双击):保持绘制态,等用户补点。
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
active_ = false;
|
||||||
|
restoreCanvas();
|
||||||
|
if (onComplete_) onComplete_(pts);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ContourDrawTool::refreshOverlay() {
|
||||||
|
if (!overlay_ || !plot_) return;
|
||||||
|
const QwtScaleMap xMap = plot_->canvasMap(xAxis_);
|
||||||
|
const QwtScaleMap yMap = plot_->canvasMap(yAxis_);
|
||||||
|
QVector<QPoint> px;
|
||||||
|
px.reserve(static_cast<int>(dataPts_.size()));
|
||||||
|
for (const QPointF& d : dataPts_)
|
||||||
|
px.push_back(QPoint(static_cast<int>(xMap.transform(d.x())),
|
||||||
|
static_cast<int>(yMap.transform(d.y()))));
|
||||||
|
overlay_->setState(px, lastCursor_, markType_, active_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ContourDrawTool::eventFilter(QObject* obj, QEvent* ev) {
|
||||||
|
if (!plot_ || obj != plot_->canvas()) return QObject::eventFilter(obj, ev);
|
||||||
|
|
||||||
|
// 始终同步 overlay 尺寸(即便未激活,保证下次激活时贴合)。
|
||||||
|
if (ev->type() == QEvent::Resize && overlay_) {
|
||||||
|
overlay_->setGeometry(plot_->canvas()->rect());
|
||||||
|
return QObject::eventFilter(obj, ev);
|
||||||
|
}
|
||||||
|
if (!active_) return QObject::eventFilter(obj, ev);
|
||||||
|
|
||||||
|
switch (ev->type()) {
|
||||||
|
case QEvent::MouseButtonDblClick: {
|
||||||
|
auto* me = static_cast<QMouseEvent*>(ev);
|
||||||
|
if (me->button() == Qt::LeftButton) {
|
||||||
|
// Qt 双击序列:Press→Release→DblClick。前一个 Press 已 addVertex 落了一个与
|
||||||
|
// 双击位置重合的伪顶点,结束前移除它,避免线/面末尾出现重复点。
|
||||||
|
if (!dataPts_.empty()) dataPts_.pop_back();
|
||||||
|
finish();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case QEvent::MouseButtonPress: {
|
||||||
|
auto* me = static_cast<QMouseEvent*>(ev);
|
||||||
|
if (me->button() == Qt::LeftButton) {
|
||||||
|
// 双击的第一下会先来一个 Press;线/面靠 DblClick 结束,这里只加点。
|
||||||
|
addVertex(me->pos());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (me->button() == Qt::RightButton) { // 右键取消
|
||||||
|
const bool wasActive = active_;
|
||||||
|
cancel();
|
||||||
|
if (wasActive && onCancel_) onCancel_();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case QEvent::MouseMove: {
|
||||||
|
auto* me = static_cast<QMouseEvent*>(ev);
|
||||||
|
lastCursor_ = me->pos();
|
||||||
|
if (!dataPts_.empty()) refreshOverlay();
|
||||||
|
return true; // 绘制期消费移动(不弹 hover)
|
||||||
|
}
|
||||||
|
case QEvent::KeyPress: {
|
||||||
|
auto* ke = static_cast<QKeyEvent*>(ev);
|
||||||
|
if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) { finish(); return true; }
|
||||||
|
if (ke->key() == Qt::Key_Escape) {
|
||||||
|
cancel();
|
||||||
|
if (onCancel_) onCancel_();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return QObject::eventFilter(obj, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <Qt> // Qt::FocusPolicy
|
||||||
|
|
||||||
|
class QwtPlot;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
class ContourDrawOverlay;
|
||||||
|
|
||||||
|
// I9 图上绘形工具:开启后在等值面图上用鼠标采集几何,实时预览,完成后回调数据坐标点。
|
||||||
|
// 复刻原版 contour overlay 绘制交互(先弹窗选类型→再图上画→drawingComplete):
|
||||||
|
// markType 1 点 / 4 文字:单击落点立即完成;
|
||||||
|
// markType 2 线:逐点单击,双击或回车结束(≥2 点);
|
||||||
|
// markType 3 面:逐点单击,双击或回车闭合(≥3 点)。
|
||||||
|
// Esc 取消。绘制时事件优先于 LivePanner/hover 消费(开启期间禁用平移)。
|
||||||
|
// 不拥有 plot(外部持有,QPointer 守护);overlay 父=canvas 随之析构。
|
||||||
|
class ContourDrawTool : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
// xAxis/yAxis:等值面所在轴(GridDataChartView 为 xBottom/yLeft)。
|
||||||
|
ContourDrawTool(QwtPlot* plot, int xAxis, int yAxis, QObject* parent = nullptr);
|
||||||
|
|
||||||
|
// 完成回调:参数为数据坐标点序列(已按类型归一化:点/文字 1 点,线≥2,面≥3)。
|
||||||
|
void setOnComplete(std::function<void(const std::vector<QPointF>&)> cb) { onComplete_ = std::move(cb); }
|
||||||
|
// 取消回调(Esc / 右键 / 外部 cancel):调用方据此恢复 UI(如重新开放工具条)。
|
||||||
|
void setOnCancel(std::function<void()> cb) { onCancel_ = std::move(cb); }
|
||||||
|
|
||||||
|
// 开始绘制指定标注类型("1".."4" 对应整数)。会重置已采集点并显示提示。
|
||||||
|
void begin(int markType);
|
||||||
|
// 外部强制取消(不触发 onCancel_)。
|
||||||
|
void cancel();
|
||||||
|
bool isActive() const { return active_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject* obj, QEvent* ev) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPointF toData(const QPoint& canvasPos) const; // 像素 → 数据坐标
|
||||||
|
void addVertex(const QPoint& canvasPos);
|
||||||
|
void finish(); // 校验最少点数 → onComplete_
|
||||||
|
void refreshOverlay(); // 把当前已落点(数据坐标)映射回像素喂给 overlay 预览
|
||||||
|
|
||||||
|
QPointer<QwtPlot> plot_;
|
||||||
|
int xAxis_;
|
||||||
|
int yAxis_;
|
||||||
|
std::function<void(const std::vector<QPointF>&)> onComplete_;
|
||||||
|
std::function<void()> onCancel_;
|
||||||
|
|
||||||
|
ContourDrawOverlay* overlay_ = nullptr; // 父=canvas
|
||||||
|
bool active_ = false;
|
||||||
|
int markType_ = 0;
|
||||||
|
std::vector<QPointF> dataPts_; // 已采集点(数据坐标)
|
||||||
|
QPoint lastCursor_; // 当前光标(橡皮筋预览到此)
|
||||||
|
Qt::FocusPolicy savedFocus_ = Qt::NoFocus; // begin 前 canvas 焦点策略(退出还原)
|
||||||
|
bool savedFocusValid_ = false;
|
||||||
|
void restoreCanvas(); // 退出绘制态:还原光标/焦点策略 + 隐藏 overlay
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -5,14 +5,19 @@
|
||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
#include <QPointer>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QTableView>
|
#include <QTableView>
|
||||||
#include <QToolTip>
|
#include <QToolTip>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "panels/chart/InversionFormDialog.hpp"
|
#include "panels/chart/InversionFormDialog.hpp"
|
||||||
|
#include "panels/chart/ScatterDataOps.hpp" // toggledDisplayStatus
|
||||||
#include "panels/chart/TablePager.hpp"
|
#include "panels/chart/TablePager.hpp"
|
||||||
|
#include "repo/IDatasetCommandRepository.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
|
|
@ -70,6 +75,38 @@ geopro::core::TableColumnKind TablePayloadModel::columnKind(int column) const {
|
||||||
return payload_.columns[static_cast<size_t>(column)].kind;
|
return payload_.columns[static_cast<size_t>(column)].kind;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int TablePayloadModel::toggleColumn() const {
|
||||||
|
for (size_t i = 0; i < payload_.columns.size(); ++i)
|
||||||
|
if (payload_.columns[i].kind == geopro::core::TableColumnKind::Toggle)
|
||||||
|
return static_cast<int>(i);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TablePayloadModel::rowId(int row) const {
|
||||||
|
if (row < 0 || row >= static_cast<int>(payload_.rowIds.size())) return {};
|
||||||
|
return payload_.rowIds[static_cast<size_t>(row)];
|
||||||
|
}
|
||||||
|
|
||||||
|
int TablePayloadModel::rowDisplayStatus(int row) const {
|
||||||
|
const int col = toggleColumn();
|
||||||
|
if (col < 0 || row < 0 || row >= static_cast<int>(payload_.rows.size())) return 0;
|
||||||
|
const auto& cells = payload_.rows[static_cast<size_t>(row)];
|
||||||
|
if (col >= static_cast<int>(cells.size())) return 0;
|
||||||
|
// Toggle 单元 "1"=ON/可见 → displayStatus 0;否则隐藏 → 1。
|
||||||
|
return cells[static_cast<size_t>(col)] == QLatin1String("1") ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TablePayloadModel::setRowDisplayStatus(int row, int status) {
|
||||||
|
const int col = toggleColumn();
|
||||||
|
if (col < 0 || row < 0 || row >= static_cast<int>(payload_.rows.size())) return;
|
||||||
|
auto& cells = payload_.rows[static_cast<size_t>(row)];
|
||||||
|
if (col >= static_cast<int>(cells.size())) return;
|
||||||
|
// status 0=显示 → 单元 "1"(ON);status 1=隐藏 → "0"(OFF)。
|
||||||
|
cells[static_cast<size_t>(col)] = (status == 0) ? QStringLiteral("1") : QStringLiteral("0");
|
||||||
|
const QModelIndex idx = index(row, col);
|
||||||
|
emit dataChanged(idx, idx, {Qt::DisplayRole});
|
||||||
|
}
|
||||||
|
|
||||||
ToggleSwitchDelegate::ToggleSwitchDelegate(const TablePayloadModel* model, QObject* parent)
|
ToggleSwitchDelegate::ToggleSwitchDelegate(const TablePayloadModel* model, QObject* parent)
|
||||||
: QStyledItemDelegate(parent), model_(model) {}
|
: QStyledItemDelegate(parent), model_(model) {}
|
||||||
|
|
||||||
|
|
@ -141,6 +178,8 @@ DataTableView::DataTableView(QWidget* parent) : QWidget(parent) {
|
||||||
|
|
||||||
// Toggle 列委托:把“隐藏/显示”列画成蓝色药丸开关。
|
// Toggle 列委托:把“隐藏/显示”列画成蓝色药丸开关。
|
||||||
table_->setItemDelegate(new ToggleSwitchDelegate(model_, table_));
|
table_->setItemDelegate(new ToggleSwitchDelegate(model_, table_));
|
||||||
|
// M2:点击 Toggle 列(仅 measurement 可交互)→ 行级显隐切换。
|
||||||
|
connect(table_, &QTableView::clicked, this, &DataTableView::onCellClicked);
|
||||||
|
|
||||||
lay->addWidget(table_);
|
lay->addWidget(table_);
|
||||||
|
|
||||||
|
|
@ -227,4 +266,42 @@ void DataTableView::onFunctionButton(const QString& code) {
|
||||||
dlg.exec(); // 提交反馈由对话框内部处理;列表无需刷新(原版亦仅 Message.success 提示)。
|
dlg.exec(); // 提交反馈由对话框内部处理;列表无需刷新(原版亦仅 Message.success 提示)。
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DataTableView::onCellClicked(const QModelIndex& index) {
|
||||||
|
// M2 行级显隐:仅 measurement 列表(toggleInteractive)的 Toggle 列响应点击;其余视图无操作。
|
||||||
|
if (!index.isValid() || !model_->isToggleInteractive()) return;
|
||||||
|
if (model_->columnKind(index.column()) != geopro::core::TableColumnKind::Toggle) return;
|
||||||
|
|
||||||
|
const int row = index.row();
|
||||||
|
const QString id = model_->rowId(row);
|
||||||
|
const int cur = model_->rowDisplayStatus(row); // 0=显示 1=隐藏
|
||||||
|
const int next = toggledDisplayStatus(cur); // 取反(对照原版 record.displayStatus ? 0 : 1)
|
||||||
|
|
||||||
|
// popconfirm 文案:当前显示(0)→将隐藏;当前隐藏→将显示(对照原版 scatterPopHide/scatterPopShow)。
|
||||||
|
const QString text = (cur == 0) ? QStringLiteral("该操作会隐藏该散点,确认?")
|
||||||
|
: QStringLiteral("该操作会显示该散点,确认?");
|
||||||
|
if (QMessageBox::question(this, QStringLiteral("提示"), text,
|
||||||
|
QMessageBox::Ok | QMessageBox::Cancel) != QMessageBox::Ok)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||||
|
if (!cmdRepo_ || dsId.isEmpty() || id.isEmpty()) {
|
||||||
|
// 无仓储/无 dsId/无行 id → 仅本地切换(退化,不持久化)。
|
||||||
|
model_->setRowDisplayStatus(row, next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray ids;
|
||||||
|
ids.append(id);
|
||||||
|
QPointer<DataTableView> self(this);
|
||||||
|
cmdRepo_->saveDisplayStatus(dsId, ids, next, [self, row, next](bool ok, QString msg) {
|
||||||
|
if (!self) return;
|
||||||
|
if (!ok) {
|
||||||
|
QMessageBox::warning(self, QStringLiteral("提示"),
|
||||||
|
msg.isEmpty() ? QStringLiteral("操作失败") : msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->model_->setRowDisplayStatus(row, next); // 持久化成功后更新该行状态
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,17 @@ public:
|
||||||
// 列渲染种类(供委托判断是否画开关)。越界返回 Text。
|
// 列渲染种类(供委托判断是否画开关)。越界返回 Text。
|
||||||
geopro::core::TableColumnKind columnKind(int column) const;
|
geopro::core::TableColumnKind columnKind(int column) const;
|
||||||
|
|
||||||
|
// M2:该 Toggle 列是否可交互(仅 measurement 载荷为 true)。
|
||||||
|
bool isToggleInteractive() const { return payload_.toggleInteractive; }
|
||||||
|
// M2:取行点 id(越界/无 id → 空串)。
|
||||||
|
QString rowId(int row) const;
|
||||||
|
// M2:取行当前显隐状态(0=显示 1=隐藏;据 Toggle 单元 "1"=ON/可见反推)。
|
||||||
|
int rowDisplayStatus(int row) const;
|
||||||
|
// M2:把某行 Toggle 单元就地设为指定状态(status 0=显示 → "1"/ON;持久化成功后调用)。
|
||||||
|
void setRowDisplayStatus(int row, int status);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
int toggleColumn() const; // Toggle 列下标(无则 -1)
|
||||||
geopro::core::TablePayload payload_;
|
geopro::core::TablePayload payload_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -81,6 +91,7 @@ signals:
|
||||||
private:
|
private:
|
||||||
void rebuildToolbar(const std::vector<geopro::core::TableFunctionButton>& buttons);
|
void rebuildToolbar(const std::vector<geopro::core::TableFunctionButton>& buttons);
|
||||||
void onFunctionButton(const QString& code); // 功能按钮路由(仅 inversion 起效)
|
void onFunctionButton(const QString& code); // 功能按钮路由(仅 inversion 起效)
|
||||||
|
void onCellClicked(const QModelIndex& index); // M2 行级显隐切换(仅 measurement Toggle 列)
|
||||||
|
|
||||||
QWidget* toolbar_; // 顶部功能按钮行容器(functionButtons 空时隐藏)
|
QWidget* toolbar_; // 顶部功能按钮行容器(functionButtons 空时隐藏)
|
||||||
QHBoxLayout* toolbarLay_; // 功能按钮布局(重建时清空重填)
|
QHBoxLayout* toolbarLay_; // 功能按钮布局(重建时清空重填)
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,8 @@ ExceptionDialog::ExceptionDialog(geopro::data::IDatasetCommandRepository* repo,
|
||||||
cardLay->addLayout(form);
|
cardLay->addLayout(form);
|
||||||
root->addWidget(card);
|
root->addWidget(card);
|
||||||
|
|
||||||
// 坐标表(x/y 多行),下方加/减行按钮。
|
// 坐标兜底表(x/y 多行):留空 → 确定后在图上绘形采集(主路径);手填 → 直接提交。
|
||||||
root->addWidget(new QLabel(QStringLiteral("坐标(x,y):"), this));
|
root->addWidget(new QLabel(QStringLiteral("坐标(x,y,留空则在图上绘制):"), this));
|
||||||
coordTable_ = new QTableWidget(0, 2, this);
|
coordTable_ = new QTableWidget(0, 2, this);
|
||||||
coordTable_->setHorizontalHeaderLabels({QStringLiteral("x"), QStringLiteral("y")});
|
coordTable_->setHorizontalHeaderLabels({QStringLiteral("x"), QStringLiteral("y")});
|
||||||
coordTable_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
coordTable_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||||
|
|
@ -115,10 +115,35 @@ QString ExceptionDialog::markTypeValue() const {
|
||||||
return markTypeCombo_->currentData().toString();
|
return markTypeCombo_->currentData().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString ExceptionDialog::exceptionTypeId() const {
|
||||||
|
return exceptionTypeCombo_->currentData().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ExceptionDialog::exceptionName() const {
|
||||||
|
return nameEdit_->text().trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ExceptionDialog::exceptionRemark() const {
|
||||||
|
return remarkEdit_->toPlainText();
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray ExceptionDialog::manualCoordinates() const {
|
||||||
|
QJsonArray coords;
|
||||||
|
for (int r = 0; r < coordTable_->rowCount(); ++r) {
|
||||||
|
auto* ix = coordTable_->item(r, 0);
|
||||||
|
auto* iy = coordTable_->item(r, 1);
|
||||||
|
if (!ix || !iy || ix->text().trimmed().isEmpty() || iy->text().trimmed().isEmpty()) continue;
|
||||||
|
bool okx = false, oky = false;
|
||||||
|
const double x = ix->text().toDouble(&okx);
|
||||||
|
const double y = iy->text().toDouble(&oky);
|
||||||
|
if (!okx || !oky) continue;
|
||||||
|
coords.append(QJsonObject{{QStringLiteral("x"), x}, {QStringLiteral("y"), y}});
|
||||||
|
}
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
void ExceptionDialog::onTypeChanged() {
|
void ExceptionDialog::onTypeChanged() {
|
||||||
// 调整坐标表行数到该形态最少点数(不足则补行;已多则保留)。
|
// 主路径为图上绘形 → 坐标表默认留空(不自动补行),仅刷新异常类型列表。
|
||||||
const int need = minPoints(markTypeValue());
|
|
||||||
while (coordTable_->rowCount() < need) coordTable_->insertRow(coordTable_->rowCount());
|
|
||||||
loadExceptionTypes();
|
loadExceptionTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,18 +194,11 @@ void ExceptionDialog::onConfirm() {
|
||||||
QMessageBox::warning(this, windowTitle(), QStringLiteral("请选择异常类型"));
|
QMessageBox::warning(this, windowTitle(), QStringLiteral("请选择异常类型"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 收集坐标(跳过空行)。
|
// 主路径:坐标表留空 → accept(),由调用方在图上绘形采集坐标后 newException。
|
||||||
QJsonArray coords;
|
const QJsonArray coords = manualCoordinates();
|
||||||
for (int r = 0; r < coordTable_->rowCount(); ++r) {
|
if (coords.isEmpty()) { accept(); return; }
|
||||||
auto* ix = coordTable_->item(r, 0);
|
|
||||||
auto* iy = coordTable_->item(r, 1);
|
// 兜底路径:用户手填了坐标 → 校验点数后直接弹窗内提交。
|
||||||
if (!ix || !iy || ix->text().trimmed().isEmpty() || iy->text().trimmed().isEmpty()) continue;
|
|
||||||
bool okx = false, oky = false;
|
|
||||||
const double x = ix->text().toDouble(&okx);
|
|
||||||
const double y = iy->text().toDouble(&oky);
|
|
||||||
if (!okx || !oky) continue;
|
|
||||||
coords.append(QJsonObject{{QStringLiteral("x"), x}, {QStringLiteral("y"), y}});
|
|
||||||
}
|
|
||||||
if (coords.size() < minPoints(markTypeValue())) {
|
if (coords.size() < minPoints(markTypeValue())) {
|
||||||
QMessageBox::warning(this, windowTitle(),
|
QMessageBox::warning(this, windowTitle(),
|
||||||
QStringLiteral("坐标点数不足(点/文字≥1,线≥2,面≥3)"));
|
QStringLiteral("坐标点数不足(点/文字≥1,线≥2,面≥3)"));
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
|
@ -17,24 +18,31 @@ class IDatasetCommandRepository;
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
// 异常创建对话框(I9,复刻原版 exceptionDialog + contourPage 保存链路):
|
// 异常创建对话框(I9,复刻原版 exceptionDialog 时序):
|
||||||
// 标注类型(点/线/面/文字 → remarkSourceType "1"/"2"/"3"/"4") + 异常类型(listExceptionTypes)
|
// 先弹窗选 标注类型(点/线/面/文字 → remarkSourceType "1".."4") + 异常类型(listExceptionTypes)
|
||||||
// + 名称(getExceptionName 建议) + 备注 + 坐标表。
|
// + 名称(getExceptionName 建议) + 备注;确认后由调用方在图上交互绘形采集坐标,再 newException。
|
||||||
// 确认 → newException(body),成功 accept(),调用方随后 reloadGrid。
|
// 时序对照原版 contourPage/exceptionDialog:弹窗仅收元信息(不收坐标),accept() 后调用方读
|
||||||
// 说明:原版「图上交互式绘制几何」在 Qwt 成本高,本实现以坐标表(x/y 多行)采集 location,
|
// markTypeValue()/exceptionTypeId()/name()/remark() 启动 ContourDrawTool 绘形 → 完成提交。
|
||||||
// 覆盖点(1 行)/线(≥2)/面(≥3)/文字(1) 全形态,打通完整创建链路;on-chart 拖拽绘制为后置项。
|
// 兜底:坐标表仍保留(无法图上绘形时可手填),有有效坐标行则直接走旧版「弹窗内提交」链路。
|
||||||
class ExceptionDialog : public QDialog {
|
class ExceptionDialog : public QDialog {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
ExceptionDialog(geopro::data::IDatasetCommandRepository* repo, QString projectId,
|
ExceptionDialog(geopro::data::IDatasetCommandRepository* repo, QString projectId,
|
||||||
QString remarkSourceId, QWidget* parent = nullptr);
|
QString remarkSourceId, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// accept() 后供调用方读取(驱动图上绘形 + newException)。
|
||||||
|
QString markTypeValue() const; // 标注类型 "1".."4"(remarkSourceType)
|
||||||
|
QString exceptionTypeId() const; // 异常类型 id
|
||||||
|
QString exceptionName() const; // 名称
|
||||||
|
QString exceptionRemark() const; // 备注
|
||||||
|
// 兜底坐标(用户在表里手填的有效行);空 = 走图上绘形主路径。
|
||||||
|
QJsonArray manualCoordinates() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void onTypeChanged(); // 标注类型变 → 重拉异常类型列表 + 调整坐标表最少行
|
void onTypeChanged(); // 标注类型变 → 重拉异常类型列表 + 调整坐标表最少行
|
||||||
void loadExceptionTypes(); // listExceptionTypes(projectId, remarkSourceType)
|
void loadExceptionTypes(); // listExceptionTypes(projectId, remarkSourceType)
|
||||||
void suggestName(); // getExceptionName(exceptionTypeId, remarkSourceId) → 名称建议
|
void suggestName(); // getExceptionName(exceptionTypeId, remarkSourceId) → 名称建议
|
||||||
void onConfirm(); // 校验 → newException
|
void onConfirm(); // 校验 → 有手填坐标则直接 newException,否则 accept() 交给绘形
|
||||||
QString markTypeValue() const; // 当前标注类型字符串("1".."4")
|
|
||||||
|
|
||||||
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
|
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
|
||||||
QString projectId_;
|
QString projectId_;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QPointF>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
|
#include <vector>
|
||||||
#include <QSignalBlocker>
|
#include <QSignalBlocker>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QSlider>
|
#include <QSlider>
|
||||||
|
|
@ -33,6 +35,7 @@
|
||||||
#include "panels/chart/AutoAnnotationDialog.hpp"
|
#include "panels/chart/AutoAnnotationDialog.hpp"
|
||||||
#include "panels/chart/ColorBarWidget.hpp"
|
#include "panels/chart/ColorBarWidget.hpp"
|
||||||
#include "panels/chart/ColorMapService.hpp"
|
#include "panels/chart/ColorMapService.hpp"
|
||||||
|
#include "panels/chart/ContourDrawTool.hpp"
|
||||||
#include "panels/chart/ContourHoverTip.hpp"
|
#include "panels/chart/ContourHoverTip.hpp"
|
||||||
#include "panels/chart/ContourPlotItem.hpp"
|
#include "panels/chart/ContourPlotItem.hpp"
|
||||||
#include "panels/chart/ExceptionDetailDialog.hpp"
|
#include "panels/chart/ExceptionDetailDialog.hpp"
|
||||||
|
|
@ -217,13 +220,16 @@ GridDataChartView::GridDataChartView(QWidget* parent) : QWidget(parent) {
|
||||||
|
|
||||||
// 描述保存(I14)。
|
// 描述保存(I14)。
|
||||||
connect(descriptionPanel_, &DescriptionPanel::saveRequested, this,
|
connect(descriptionPanel_, &DescriptionPanel::saveRequested, this,
|
||||||
[this](const QString& t) { saveDescription(t); });
|
[this]() { saveDescription(); });
|
||||||
|
|
||||||
// I7 显示等值线提示信息:hover tooltip 显隐(本地,挂画布事件过滤器)。
|
// I7 显示等值线提示信息:hover tooltip 显隐(本地,挂画布事件过滤器)。
|
||||||
contourTip_ = new ContourHoverTip(plot_, QwtPlot::xBottom, QwtPlot::yLeft, this);
|
contourTip_ = new ContourHoverTip(plot_, QwtPlot::xBottom, QwtPlot::yLeft, this);
|
||||||
connect(chkContourTip, &QCheckBox::toggled, this,
|
connect(chkContourTip, &QCheckBox::toggled, this,
|
||||||
[this](bool on) { if (contourTip_) contourTip_->setEnabled(on); });
|
[this](bool on) { if (contourTip_) contourTip_->setEnabled(on); });
|
||||||
|
|
||||||
|
// I9 图上绘形工具(后装于 LivePanner/hover,绘制期优先消费事件)。默认空闲。
|
||||||
|
drawTool_ = new ContourDrawTool(plot_, QwtPlot::xBottom, QwtPlot::yLeft, this);
|
||||||
|
|
||||||
// 主题配色:当前主题套一次 + 监听切换热更新。
|
// 主题配色:当前主题套一次 + 监听切换热更新。
|
||||||
applyChartPlotTheme(plot_);
|
applyChartPlotTheme(plot_);
|
||||||
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, plot_,
|
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, plot_,
|
||||||
|
|
@ -374,9 +380,17 @@ void GridDataChartView::openWhitening() {
|
||||||
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||||
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
|
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
|
||||||
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
|
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
|
||||||
// tmObjectId(白化模板列表用)客户端视图未透传 structParentId,按原版兜底空串。
|
// tmObjectId(白化模板列表用)= 当前数据集的 structParentId(对照原版 dsFileRow.structParentId)。
|
||||||
WhiteningDialog dlg(cmdRepo_, dsId, projectId, QString(), this);
|
// 客户端 open 链路未透传该字段 → 方案 A:懒拉 getDsObjectDetail(dsId) 取 structParentId,
|
||||||
if (dlg.exec() == QDialog::Accepted) reloadGrid();
|
// 再打开白化对话框。即便取不到(空串)也照常打开(与原版兜底一致,仅模板列表为空)。
|
||||||
|
QPointer<GridDataChartView> self(this);
|
||||||
|
cmdRepo_->getDsObjectDetail(dsId, [self, dsId, projectId](bool ok, QJsonObject data, QString) {
|
||||||
|
if (!self) return;
|
||||||
|
const QString tmObjectId =
|
||||||
|
ok ? data.value(QStringLiteral("structParentId")).toString() : QString();
|
||||||
|
WhiteningDialog dlg(self->cmdRepo_, dsId, projectId, tmObjectId, self);
|
||||||
|
if (dlg.exec() == QDialog::Accepted) self->reloadGrid();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void GridDataChartView::openFilter() {
|
void GridDataChartView::openFilter() {
|
||||||
|
|
@ -406,9 +420,57 @@ void GridDataChartView::openExceptionDialog() {
|
||||||
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||||
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
|
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
|
||||||
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
|
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
|
||||||
// remarkSourceId = dsObjectId(异常挂当前等值面数据集)。
|
// 时序复刻原版:先弹窗选 标注类型/异常类型/名称/备注(remarkSourceId = dsObjectId)。
|
||||||
ExceptionDialog dlg(cmdRepo_, projectId, dsId, this);
|
ExceptionDialog dlg(cmdRepo_, projectId, dsId, this);
|
||||||
if (dlg.exec() == QDialog::Accepted) reloadGrid();
|
if (dlg.exec() != QDialog::Accepted) return;
|
||||||
|
|
||||||
|
// 兜底:用户手填了坐标 → 对话框内部已提交,仅重载。
|
||||||
|
if (!dlg.manualCoordinates().isEmpty()) { reloadGrid(); return; }
|
||||||
|
|
||||||
|
// 主路径:弹窗后在图上交互绘形 → 完成回调组装 newException(对照原版 startDraw*→
|
||||||
|
// drawingComplete→newExceptionInProfileInversion)。
|
||||||
|
const QString markType = dlg.markTypeValue();
|
||||||
|
const QString typeId = dlg.exceptionTypeId();
|
||||||
|
const QString name = dlg.exceptionName();
|
||||||
|
const QString remark = dlg.exceptionRemark();
|
||||||
|
if (!drawTool_) return;
|
||||||
|
QPointer<GridDataChartView> self(this);
|
||||||
|
drawTool_->setOnComplete([self, markType, typeId, name, remark](const std::vector<QPointF>& pts) {
|
||||||
|
if (!self) return;
|
||||||
|
QJsonArray coords;
|
||||||
|
for (const QPointF& p : pts)
|
||||||
|
coords.append(QJsonObject{{QStringLiteral("x"), p.x()}, {QStringLiteral("y"), p.y()}});
|
||||||
|
self->submitDrawnException(markType, typeId, name, remark, coords);
|
||||||
|
});
|
||||||
|
drawTool_->setOnCancel([] {}); // 取消绘形:无操作(不提交)
|
||||||
|
drawTool_->begin(markType.toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridDataChartView::submitDrawnException(const QString& markType, const QString& typeId,
|
||||||
|
const QString& name, const QString& remark,
|
||||||
|
const QJsonArray& coords) {
|
||||||
|
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||||
|
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
|
||||||
|
if (!cmdRepo_ || dsId.isEmpty()) return;
|
||||||
|
QJsonObject body{
|
||||||
|
{QStringLiteral("exceptionName"), name},
|
||||||
|
{QStringLiteral("exceptionTypeId"), typeId},
|
||||||
|
{QStringLiteral("remark"), remark},
|
||||||
|
{QStringLiteral("remarkSourceType"), markType}, // 几何形态字符串 "1".."4"
|
||||||
|
{QStringLiteral("remarkSourceId"), dsId}, // = dsObjectId
|
||||||
|
{QStringLiteral("projectId"), projectId},
|
||||||
|
{QStringLiteral("location"), QJsonObject{{QStringLiteral("coordinate"), coords}}},
|
||||||
|
};
|
||||||
|
QPointer<GridDataChartView> self(this);
|
||||||
|
cmdRepo_->newException(body, [self](bool ok, QString msg) {
|
||||||
|
if (!self) return;
|
||||||
|
if (!ok) {
|
||||||
|
QMessageBox::warning(self, QStringLiteral("新建异常"),
|
||||||
|
msg.isEmpty() ? QStringLiteral("创建失败") : msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->reloadGrid(); // 成功后重载(列表 + 图层同步)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void GridDataChartView::openAutoAnnotation() {
|
void GridDataChartView::openAutoAnnotation() {
|
||||||
|
|
@ -472,32 +534,33 @@ void GridDataChartView::loadDescription() {
|
||||||
QPointer<GridDataChartView> self(this);
|
QPointer<GridDataChartView> self(this);
|
||||||
cmdRepo_->getDsObjectDetail(dsId, [self](bool ok, QJsonObject data, QString) {
|
cmdRepo_->getDsObjectDetail(dsId, [self](bool ok, QJsonObject data, QString) {
|
||||||
if (!self || !ok) return;
|
if (!self || !ok) return;
|
||||||
// 原版从 attachedParameters.deltaContent 取 Quill Delta;Qt 退化为纯文本:
|
// 原版从 attachedParameters.deltaContent 取 Quill Delta 回填编辑器(quill.setContents)。
|
||||||
// 优先 description 字段,否则拼接 delta ops 的 insert 文本。
|
// 客户端用 QuillDelta::deltaToDocument 还原富文本;无 deltaContent 时回退 description 纯文本。
|
||||||
QString text = data.value(QStringLiteral("description")).toString();
|
const QJsonArray ops = data.value(QStringLiteral("attachedParameters"))
|
||||||
if (text.isEmpty()) {
|
.toObject()
|
||||||
const QJsonArray ops = data.value(QStringLiteral("attachedParameters"))
|
.value(QStringLiteral("deltaContent"))
|
||||||
.toObject()
|
.toArray();
|
||||||
.value(QStringLiteral("deltaContent"))
|
if (!ops.isEmpty())
|
||||||
.toArray();
|
self->descriptionPanel_->setDelta(ops);
|
||||||
for (const QJsonValue& op : ops)
|
else
|
||||||
text += op.toObject().value(QStringLiteral("insert")).toString();
|
self->descriptionPanel_->setPlainText(
|
||||||
}
|
data.value(QStringLiteral("description")).toString());
|
||||||
self->descriptionPanel_->setText(text);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void GridDataChartView::saveDescription(const QString& text) {
|
void GridDataChartView::saveDescription() {
|
||||||
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||||
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
|
if (!cmdRepo_ || dsId.isEmpty() || !descriptionPanel_) {
|
||||||
// attachedParameters.deltaContent:以最简单 op 包纯文本(reload 时可还原为纯文本)。
|
showNotImplemented(nullptr);
|
||||||
QJsonArray ops;
|
return;
|
||||||
if (!text.isEmpty()) ops.append(QJsonObject{{QStringLiteral("insert"), text}});
|
}
|
||||||
|
// 与原版 saveQuillEditorContent 对齐:
|
||||||
|
// description = 纯文本(quill.getText());deltaContent = Quill Delta ops(quill.getContents().ops)。
|
||||||
QJsonObject body{
|
QJsonObject body{
|
||||||
{QStringLiteral("dsObjectId"), dsId},
|
{QStringLiteral("dsObjectId"), dsId},
|
||||||
{QStringLiteral("description"), text},
|
{QStringLiteral("description"), descriptionPanel_->plainText()},
|
||||||
{QStringLiteral("attachedParameters"),
|
{QStringLiteral("attachedParameters"),
|
||||||
QJsonObject{{QStringLiteral("deltaContent"), ops}}},
|
QJsonObject{{QStringLiteral("deltaContent"), descriptionPanel_->delta()}}},
|
||||||
};
|
};
|
||||||
QPointer<GridDataChartView> self(this);
|
QPointer<GridDataChartView> self(this);
|
||||||
cmdRepo_->updateDsObject(body, [self](bool ok, QString msg) {
|
cmdRepo_->updateDsObject(body, [self](bool ok, QString msg) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
class QJsonArray;
|
||||||
|
|
||||||
#include "model/Anomaly.hpp"
|
#include "model/Anomaly.hpp"
|
||||||
#include "model/ColorScale.hpp"
|
#include "model/ColorScale.hpp"
|
||||||
#include "model/Field.hpp"
|
#include "model/Field.hpp"
|
||||||
|
|
@ -32,6 +34,7 @@ class ColorBarWidget;
|
||||||
class ColorMapService;
|
class ColorMapService;
|
||||||
class ContourPlotItem;
|
class ContourPlotItem;
|
||||||
class ContourHoverTip;
|
class ContourHoverTip;
|
||||||
|
class ContourDrawTool;
|
||||||
|
|
||||||
// 网格数据图表视图:工具条 + QwtPlot(白底 + 真实比尺 + 实时平移/滚轮缩放,x 轴在底部)
|
// 网格数据图表视图:工具条 + QwtPlot(白底 + 真实比尺 + 实时平移/滚轮缩放,x 轴在底部)
|
||||||
// + 独立色阶条 + 底部双页签(异常列表/描述)。
|
// + 独立色阶条 + 底部双页签(异常列表/描述)。
|
||||||
|
|
@ -72,13 +75,16 @@ private:
|
||||||
void applySimplify(); // I8:把当前滑块容差透传给 ContourPlotItem 并重绘
|
void applySimplify(); // I8:把当前滑块容差透传给 ContourPlotItem 并重绘
|
||||||
void showNotImplemented(QWidget* anchor); // 占位提示(无仓储/无 dsId)
|
void showNotImplemented(QWidget* anchor); // 占位提示(无仓储/无 dsId)
|
||||||
|
|
||||||
void openExceptionDialog(); // I9 异常创建
|
void openExceptionDialog(); // I9 异常创建(弹窗选类型 → 图上绘形 → 提交)
|
||||||
|
// I9 图上绘形完成:组装 body 提交 newException(成功 reloadGrid)。
|
||||||
|
void submitDrawnException(const QString& markType, const QString& typeId, const QString& name,
|
||||||
|
const QString& remark, const QJsonArray& coords);
|
||||||
void openAutoAnnotation(); // I13 自动标注
|
void openAutoAnnotation(); // I13 自动标注
|
||||||
void deleteAnomaly(int index); // I10 异常删除
|
void deleteAnomaly(int index); // I10 异常删除
|
||||||
void showAnomalyDetail(int index); // I11 异常详情/编辑
|
void showAnomalyDetail(int index); // I11 异常详情/编辑
|
||||||
void locateAnomaly(int index); // I12 异常定位(高亮 + 缩放)
|
void locateAnomaly(int index); // I12 异常定位(高亮 + 缩放)
|
||||||
void loadDescription(); // I14 进入时回填描述
|
void loadDescription(); // I14 进入时回填描述
|
||||||
void saveDescription(const QString& text); // I14 保存描述
|
void saveDescription(); // I14 保存描述(从面板取 Delta + 纯文本)
|
||||||
|
|
||||||
QwtPlot* plot_ = nullptr;
|
QwtPlot* plot_ = nullptr;
|
||||||
QwtPlotRescaler* rescaler_ = nullptr;
|
QwtPlotRescaler* rescaler_ = nullptr;
|
||||||
|
|
@ -90,6 +96,7 @@ private:
|
||||||
QCheckBox* chkShowLabels_ = nullptr; // 工具条「显示等值线标注」(线形⚙ 改标注显隐后同步)
|
QCheckBox* chkShowLabels_ = nullptr; // 工具条「显示等值线标注」(线形⚙ 改标注显隐后同步)
|
||||||
QTimer* simplifyDebounce_ = nullptr; // I8 简化容差防抖(~300ms)
|
QTimer* simplifyDebounce_ = nullptr; // I8 简化容差防抖(~300ms)
|
||||||
ContourHoverTip* contourTip_ = nullptr; // I7 等值线提示(hover)
|
ContourHoverTip* contourTip_ = nullptr; // I7 等值线提示(hover)
|
||||||
|
ContourDrawTool* drawTool_ = nullptr; // I9 图上绘形工具(QObject,this 持有)
|
||||||
|
|
||||||
// 渲染状态
|
// 渲染状态
|
||||||
ColorMapService* colorSvc_ = nullptr; // heap,setGridData 重建
|
ColorMapService* colorSvc_ = nullptr; // heap,setGridData 重建
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
#include "panels/chart/ScatterDataOps.hpp"
|
#include "panels/chart/ScatterDataOps.hpp"
|
||||||
#include "panels/chart/ScatterFilterDialog.hpp"
|
#include "panels/chart/ScatterFilterDialog.hpp"
|
||||||
#include "panels/chart/ScatterHoverTip.hpp"
|
#include "panels/chart/ScatterHoverTip.hpp"
|
||||||
|
#include "panels/chart/ScatterMarqueePicker.hpp"
|
||||||
#include "panels/chart/ScatterPlotItem.hpp"
|
#include "panels/chart/ScatterPlotItem.hpp"
|
||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -141,6 +142,11 @@ RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) {
|
||||||
hoverTip_ = new ScatterHoverTip(plot_, QwtPlot::xTop, QwtPlot::yLeft, this);
|
hoverTip_ = new ScatterHoverTip(plot_, QwtPlot::xTop, QwtPlot::yLeft, this);
|
||||||
hoverTip_->setField(&data_.scatter);
|
hoverTip_->setField(&data_.scatter);
|
||||||
|
|
||||||
|
// M14 框选拾取器(最后装 → 事件链最先收到;active 时优先消费拖拽,禁用平移)。默认关闭。
|
||||||
|
marquee_ = new ScatterMarqueePicker(plot_, QwtPlot::xTop, QwtPlot::yLeft, this);
|
||||||
|
marquee_->setField(&data_.scatter);
|
||||||
|
marquee_->setOnSelected([this](const std::vector<int>& idx) { onMarqueeSelected(idx); });
|
||||||
|
|
||||||
// 允许随停靠面板自由收缩(不强制最小宽度)。
|
// 允许随停靠面板自由收缩(不强制最小宽度)。
|
||||||
plot_->setMinimumSize(0, 0);
|
plot_->setMinimumSize(0, 0);
|
||||||
|
|
||||||
|
|
@ -443,31 +449,55 @@ void RawDataChartView::onShowHide(bool hide) {
|
||||||
QMessageBox::Ok | QMessageBox::Cancel);
|
QMessageBox::Ok | QMessageBox::Cancel);
|
||||||
if (ans != QMessageBox::Ok) return;
|
if (ans != QMessageBox::Ok) return;
|
||||||
|
|
||||||
// 本地切换:显示/隐藏全部数据方块(电极保留)。
|
// 选区联动(M14↔M1):隐藏且有选区 → 只对选中点(原版 getSelectedPointIds);
|
||||||
auto localToggle = [this, hide]() {
|
// 其余(隐藏无选区 / 显示)维持全部(原版显示恒为全部隐藏点)。
|
||||||
|
const bool selective = hide && scatterItem_ && scatterItem_->hasSelection();
|
||||||
|
|
||||||
|
// 本地切换可见性。selective:逐点改 displayStatus(仅选中点隐藏);否则整体显隐全部方块。
|
||||||
|
auto localToggle = [this, hide, selective]() {
|
||||||
if (!scatterItem_) return;
|
if (!scatterItem_) return;
|
||||||
scatterItem_->setScatterVisible(!hide);
|
if (selective) {
|
||||||
|
scatterItem_->setData(data_.scatter, colorSvc_); // 重读 displayStatus 逐点生效
|
||||||
|
scatterItem_->clearSelection();
|
||||||
|
} else {
|
||||||
|
for (int& s : data_.scatter.displayStatus) s = hide ? 1 : 0;
|
||||||
|
scatterItem_->setScatterVisible(!hide);
|
||||||
|
if (!hide) scatterItem_->setData(data_.scatter, colorSvc_); // 显示全部:清逐点隐藏
|
||||||
|
}
|
||||||
plot_->replot();
|
plot_->replot();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 收集要持久化的点 id:selective → 选中点 id;否则隐藏取可见点 / 显示取隐藏点。
|
||||||
|
QJsonArray ids;
|
||||||
|
if (selective) {
|
||||||
|
for (const QString& id : scatterItem_->getSelectedIds()) ids.append(id);
|
||||||
|
// 先把选中点的 displayStatus 标为隐藏(本地,供 localToggle 重读生效)。
|
||||||
|
const auto sel = scatterItem_->getSelectedIds();
|
||||||
|
for (int i = 0; i < static_cast<int>(data_.scatter.id.size()); ++i) {
|
||||||
|
const QString id = QString::fromStdString(data_.scatter.id[i]);
|
||||||
|
if (!id.isEmpty() && sel.end() != std::find(sel.begin(), sel.end(), id))
|
||||||
|
data_.scatter.displayStatus[i] = 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ids = collectScatterIds(data_.scatter, hide);
|
||||||
|
}
|
||||||
|
|
||||||
// 无仓储/无 dsId → 仅本地切换(退化,不持久化)。
|
// 无仓储/无 dsId → 仅本地切换(退化,不持久化)。
|
||||||
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||||
if (!cmdRepo_ || dsId.isEmpty()) { localToggle(); return; }
|
if (!cmdRepo_ || dsId.isEmpty()) { localToggle(); return; }
|
||||||
|
|
||||||
// 收集要持久化的点 id(隐藏取可见点 / 显示取隐藏点),status:0=显示 1=隐藏。
|
|
||||||
const QJsonArray ids = collectScatterIds(data_.scatter, hide);
|
|
||||||
const int status = hide ? 1 : 0;
|
const int status = hide ? 1 : 0;
|
||||||
QPointer<RawDataChartView> self(this);
|
QPointer<RawDataChartView> self(this);
|
||||||
cmdRepo_->saveDisplayStatus(dsId, ids, status, [self, hide, localToggle](bool ok, QString msg) {
|
cmdRepo_->saveDisplayStatus(dsId, ids, status, [self, hide, selective, localToggle](bool ok, QString msg) {
|
||||||
if (!self) return;
|
if (!self) return;
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
QMessageBox::warning(self, QStringLiteral("提示"),
|
QMessageBox::warning(self, QStringLiteral("提示"),
|
||||||
msg.isEmpty() ? QStringLiteral("操作失败") : msg);
|
msg.isEmpty() ? QStringLiteral("操作失败") : msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 持久化成功后同步本地 displayStatus 与方块可见性。
|
// selective 时本地 displayStatus 已在请求前更新;非 selective 同步整体状态。
|
||||||
const int newStatus = hide ? 1 : 0;
|
if (!selective)
|
||||||
for (int& s : self->data_.scatter.displayStatus) s = newStatus;
|
for (int& s : self->data_.scatter.displayStatus) s = hide ? 1 : 0;
|
||||||
localToggle();
|
localToggle();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -475,7 +505,8 @@ void RawDataChartView::onShowHide(bool hide) {
|
||||||
void RawDataChartView::openFilterDialog(QWidget* anchor) {
|
void RawDataChartView::openFilterDialog(QWidget* anchor) {
|
||||||
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||||
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(anchor); return; }
|
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(anchor); return; }
|
||||||
ScatterFilterDialog dlg(cmdRepo_, dsId, currentVFieldCode(), this);
|
// 传当前 V 值数组驱动分布直方图(与图上散点同源,反映当前值类型变换后的分布)。
|
||||||
|
ScatterFilterDialog dlg(cmdRepo_, dsId, currentVFieldCode(), data_.scatter.v, this);
|
||||||
dlg.exec(); // 成功/失败由对话框内部反馈(生成过滤数据集为后端动作)。
|
dlg.exec(); // 成功/失败由对话框内部反馈(生成过滤数据集为后端动作)。
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -648,6 +679,23 @@ void RawDataChartView::showPointInfoAt(const QPoint& canvasPos) {
|
||||||
infoPanel_->raise();
|
infoPanel_->raise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RawDataChartView::toggleMarqueeMode(bool on) {
|
||||||
|
marqueeMode_ = on;
|
||||||
|
if (marquee_) marquee_->setActive(on);
|
||||||
|
if (!on && scatterItem_) {
|
||||||
|
// 退出框选:清选区高亮(与原版 exitSelectMode clearSelection 一致)。
|
||||||
|
scatterItem_->clearSelection();
|
||||||
|
plot_->replot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RawDataChartView::onMarqueeSelected(const std::vector<int>& indices) {
|
||||||
|
// 框选完成:高亮框内散点(红框)。空框 → 清选区。
|
||||||
|
if (!scatterItem_) return;
|
||||||
|
scatterItem_->setSelectedIndices(indices);
|
||||||
|
plot_->replot();
|
||||||
|
}
|
||||||
|
|
||||||
bool RawDataChartView::eventFilter(QObject* obj, QEvent* ev) {
|
bool RawDataChartView::eventFilter(QObject* obj, QEvent* ev) {
|
||||||
// 仅信息模式 + 画布左键点击:找最近散点显示属性,不消费事件(保留平移链路)。
|
// 仅信息模式 + 画布左键点击:找最近散点显示属性,不消费事件(保留平移链路)。
|
||||||
if (infoMode_ && plot_ && obj == plot_->canvas() && ev->type() == QEvent::MouseButtonPress) {
|
if (infoMode_ && plot_ && obj == plot_->canvas() && ev->type() == QEvent::MouseButtonPress) {
|
||||||
|
|
@ -708,9 +756,9 @@ void RawDataChartView::buildMeasurementToolbar(const geopro::core::ScatterToolba
|
||||||
// [i] 信息:切换信息模式(点选散点看 A/B/M/N/DataRow/Pseu_Resis)。
|
// [i] 信息:切换信息模式(点选散点看 A/B/M/N/DataRow/Pseu_Resis)。
|
||||||
btnInfo->setCheckable(true);
|
btnInfo->setCheckable(true);
|
||||||
connect(btnInfo, &QToolButton::toggled, this, [this](bool on) { toggleInfoMode(on); });
|
connect(btnInfo, &QToolButton::toggled, this, [this](bool on) { toggleInfoMode(on); });
|
||||||
// [▣] 框选:本轮后置(Qwt 橡皮筋框选 + 选区联动隐藏成本较高),保持占位提示。
|
// [▣] 框选:可勾选 → 进入框选模式(橡皮筋选框内散点高亮;显示/隐藏改对选中点)。
|
||||||
connect(btnMarquee, &QToolButton::clicked, this,
|
btnMarquee->setCheckable(true);
|
||||||
[this, btnMarquee]() { showNotImplemented(btnMarquee); });
|
connect(btnMarquee, &QToolButton::toggled, this, [this](bool on) { toggleMarqueeMode(on); });
|
||||||
|
|
||||||
// 显示 / 隐藏:popconfirm 确认 → saveDisplayStatus 持久化 → 本地切换(M1)。
|
// 显示 / 隐藏:popconfirm 确认 → saveDisplayStatus 持久化 → 本地切换(M1)。
|
||||||
auto* btnShow = new QPushButton(QStringLiteral("显示"), toolbar);
|
auto* btnShow = new QPushButton(QStringLiteral("显示"), toolbar);
|
||||||
|
|
@ -816,6 +864,7 @@ void RawDataChartView::setData(const geopro::core::ScatterPayload& p) {
|
||||||
data_ = p;
|
data_ = p;
|
||||||
baseV_ = data_.scatter.v; // 缓存原始 v(线性),M7 值类型变换从原值算,不累积误差
|
baseV_ = data_.scatter.v; // 缓存原始 v(线性),M7 值类型变换从原值算,不累积误差
|
||||||
if (hoverTip_) hoverTip_->setField(&data_.scatter); // 显式重绑(地址稳定,消除隐式依赖)
|
if (hoverTip_) hoverTip_->setField(&data_.scatter); // 显式重绑(地址稳定,消除隐式依赖)
|
||||||
|
if (marquee_) marquee_->setField(&data_.scatter); // M14 框选拾取同源重绑
|
||||||
|
|
||||||
// measurement 载荷(toolbar 非空):首次到来时建并替换工具条(视觉 1:1)。反演留空 → 不动。
|
// measurement 载荷(toolbar 非空):首次到来时建并替换工具条(视觉 1:1)。反演留空 → 不动。
|
||||||
if (!p.toolbar.empty() && !measurementToolbar_) buildMeasurementToolbar(p.toolbar);
|
if (!p.toolbar.empty() && !measurementToolbar_) buildMeasurementToolbar(p.toolbar);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ namespace geopro::app {
|
||||||
class ColorBarWidget;
|
class ColorBarWidget;
|
||||||
class ScatterPlotItem;
|
class ScatterPlotItem;
|
||||||
class ScatterHoverTip;
|
class ScatterHoverTip;
|
||||||
|
class ScatterMarqueePicker;
|
||||||
|
|
||||||
// 原数据图表视图:工具条 + QwtPlot(x 轴顶部、Panner/Magnifier)+ 独立色阶条。
|
// 原数据图表视图:工具条 + QwtPlot(x 轴顶部、Panner/Magnifier)+ 独立色阶条。
|
||||||
class RawDataChartView : public QWidget, public IDetailView {
|
class RawDataChartView : public QWidget, public IDetailView {
|
||||||
|
|
@ -76,6 +77,8 @@ private:
|
||||||
void exportDat(); // M12 导出 DAT
|
void exportDat(); // M12 导出 DAT
|
||||||
void toggleInfoMode(bool on); // M13 [i] 信息模式开关
|
void toggleInfoMode(bool on); // M13 [i] 信息模式开关
|
||||||
void showPointInfoAt(const QPoint& canvasPos); // M13 点选显示属性
|
void showPointInfoAt(const QPoint& canvasPos); // M13 点选显示属性
|
||||||
|
void toggleMarqueeMode(bool on); // M14 框选模式开关
|
||||||
|
void onMarqueeSelected(const std::vector<int>& indices); // M14 框选回调:高亮选中点
|
||||||
// 用 colorSvc_ 重绘当前散点(M7/M8 本地变换/色阶变更后复用)。
|
// 用 colorSvc_ 重绘当前散点(M7/M8 本地变换/色阶变更后复用)。
|
||||||
void redrawScatter();
|
void redrawScatter();
|
||||||
QString currentVFieldCode() const; // 当前 V 值下拉 fieldCode
|
QString currentVFieldCode() const; // 当前 V 值下拉 fieldCode
|
||||||
|
|
@ -106,6 +109,8 @@ private:
|
||||||
ColorMapService* colorSvc_ = nullptr; // heap,由 setData 重建
|
ColorMapService* colorSvc_ = nullptr; // heap,由 setData 重建
|
||||||
ScatterPlotItem* scatterItem_ = nullptr;
|
ScatterPlotItem* scatterItem_ = nullptr;
|
||||||
ScatterHoverTip* hoverTip_ = nullptr; // 散点 hover 提示(QObject,this 持有)
|
ScatterHoverTip* hoverTip_ = nullptr; // 散点 hover 提示(QObject,this 持有)
|
||||||
|
ScatterMarqueePicker* marquee_ = nullptr; // M14 框选拾取器(QObject,this 持有)
|
||||||
|
bool marqueeMode_ = false; // M14 框选模式开关
|
||||||
|
|
||||||
// 反演命令仓储 + dsId/projectId 取值回调(注入;空则反演按钮占位)。
|
// 反演命令仓储 + dsId/projectId 取值回调(注入;空则反演按钮占位)。
|
||||||
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
|
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
|
||||||
|
|
|
||||||
|
|
@ -65,4 +65,26 @@ QJsonObject buildSaveRawDataBody(const QString& dsId, int operationType, const Q
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScatterHistogram buildScatterHistogram(const std::vector<double>& v, double min, double max,
|
||||||
|
int binCount) {
|
||||||
|
ScatterHistogram h;
|
||||||
|
h.binMin = min;
|
||||||
|
h.binMax = max;
|
||||||
|
if (binCount <= 0 || !(max > min)) return h; // 退化:返回空 counts(视图渲染空态)
|
||||||
|
h.counts.assign(static_cast<std::size_t>(binCount), 0);
|
||||||
|
h.step = (max - min) / binCount;
|
||||||
|
for (double x : v) {
|
||||||
|
if (!std::isfinite(x) || x < min || x > max) continue; // 区间外/非有限 跳过
|
||||||
|
int idx = static_cast<int>((x - min) / h.step);
|
||||||
|
if (idx >= binCount) idx = binCount - 1; // 末箱右闭(恰等于 max 归入末箱)
|
||||||
|
if (idx < 0) idx = 0;
|
||||||
|
++h.counts[static_cast<std::size_t>(idx)];
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
int toggledDisplayStatus(int currentStatus) {
|
||||||
|
return currentStatus == 0 ? 1 : 0; // 0 显示 → 1 隐藏;其余 → 0 显示
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,23 @@ QJsonObject buildScatterFilterBody(const QString& dsObjectId, const QString& vFi
|
||||||
// {dsId, operationType(1新增/0覆盖)},仅新增(operationType==1)才带 name。
|
// {dsId, operationType(1新增/0覆盖)},仅新增(operationType==1)才带 name。
|
||||||
QJsonObject buildSaveRawDataBody(const QString& dsId, int operationType, const QString& name);
|
QJsonObject buildSaveRawDataBody(const QString& dsId, int operationType, const QString& name);
|
||||||
|
|
||||||
|
// 直方图分箱结果(M3 数据过滤分布图):等宽 binCount 个箱,落在 [min,max] 区间内计数。
|
||||||
|
// binMin/binMax = 分箱区间端点(= 入参 min/max);step = 单箱宽度;counts[i] = 第 i 箱内点数。
|
||||||
|
// 边界归属:左闭右开 [lo,hi),末箱右闭以纳入恰等于 max 的点。
|
||||||
|
struct ScatterHistogram {
|
||||||
|
double binMin = 0.0;
|
||||||
|
double binMax = 0.0;
|
||||||
|
double step = 0.0;
|
||||||
|
std::vector<int> counts;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 对 v 数组在 [min,max] 区间按 binCount 等宽分箱(M3,对照原版 D3 直方图 stepRange=20)。
|
||||||
|
// 非有限值(NaN/inf)跳过;区间外的点不计入;min>=max 或 binCount<=0 → counts 全 0。
|
||||||
|
ScatterHistogram buildScatterHistogram(const std::vector<double>& v, double min, double max,
|
||||||
|
int binCount);
|
||||||
|
|
||||||
|
// 行级显隐状态取反(M2,对照原版 updateStatus = record.displayStatus ? 0 : 1)。
|
||||||
|
// 入参/返回 0=显示、1=隐藏。当前显示(0) → 隐藏(1);当前隐藏(非0) → 显示(0)。
|
||||||
|
int toggledDisplayStatus(int currentStatus);
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
#include "FormKit.hpp"
|
#include "FormKit.hpp"
|
||||||
#include "panels/chart/ScatterDataOps.hpp" // buildScatterFilterBody
|
#include "panels/chart/ScatterDataOps.hpp" // buildScatterFilterBody
|
||||||
|
#include "panels/chart/ScatterHistogram.hpp"
|
||||||
#include "repo/IDatasetCommandRepository.hpp"
|
#include "repo/IDatasetCommandRepository.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
@ -22,20 +23,27 @@ constexpr double kSpinRange = 1e12; // 数值范围足够宽,覆盖电阻率/
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ScatterFilterDialog::ScatterFilterDialog(geopro::data::IDatasetCommandRepository* repo,
|
ScatterFilterDialog::ScatterFilterDialog(geopro::data::IDatasetCommandRepository* repo,
|
||||||
QString dsObjectId, QString vFieldCode, QWidget* parent)
|
QString dsObjectId, QString vFieldCode,
|
||||||
|
std::vector<double> values, QWidget* parent)
|
||||||
: QDialog(parent),
|
: QDialog(parent),
|
||||||
repo_(repo),
|
repo_(repo),
|
||||||
dsObjectId_(std::move(dsObjectId)),
|
dsObjectId_(std::move(dsObjectId)),
|
||||||
vFieldCode_(std::move(vFieldCode)) {
|
vFieldCode_(std::move(vFieldCode)) {
|
||||||
setWindowTitle(QStringLiteral("数据过滤"));
|
setWindowTitle(QStringLiteral("数据过滤"));
|
||||||
setModal(true);
|
setModal(true);
|
||||||
resize(360, 200);
|
resize(640, 300);
|
||||||
|
|
||||||
auto* root = formkit::dialogRoot(this);
|
auto* root = formkit::dialogRoot(this);
|
||||||
|
|
||||||
rangeLabel_ = new QLabel(QStringLiteral("数值范围:—"), this);
|
rangeLabel_ = new QLabel(QStringLiteral("数值范围:—"), this);
|
||||||
root->addWidget(rangeLabel_);
|
root->addWidget(rangeLabel_);
|
||||||
|
|
||||||
|
// 主体:左直方图 + 右范围输入(横向分栏,对照原版左图右控件布局)。
|
||||||
|
auto* bodyLay = new QHBoxLayout();
|
||||||
|
histogram_ = new ScatterHistogramView(this);
|
||||||
|
histogram_->setValues(values);
|
||||||
|
bodyLay->addWidget(histogram_, 1);
|
||||||
|
|
||||||
auto* card = formkit::formCard(this);
|
auto* card = formkit::formCard(this);
|
||||||
auto* cardLay = formkit::cardBody(card);
|
auto* cardLay = formkit::cardBody(card);
|
||||||
|
|
||||||
|
|
@ -51,7 +59,14 @@ ScatterFilterDialog::ScatterFilterDialog(geopro::data::IDatasetCommandRepository
|
||||||
form->addRow(formkit::editLabel(QStringLiteral("最小值")), minSpin_);
|
form->addRow(formkit::editLabel(QStringLiteral("最小值")), minSpin_);
|
||||||
form->addRow(formkit::editLabel(QStringLiteral("最大值")), maxSpin_);
|
form->addRow(formkit::editLabel(QStringLiteral("最大值")), maxSpin_);
|
||||||
cardLay->addLayout(form);
|
cardLay->addLayout(form);
|
||||||
root->addWidget(card);
|
bodyLay->addWidget(card);
|
||||||
|
root->addLayout(bodyLay);
|
||||||
|
|
||||||
|
// min/max 改动 → 实时同步直方图选区高亮(原版输入/滑块联动指示矩形)。
|
||||||
|
connect(minSpin_, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
|
||||||
|
[this](double) { syncHistogramSel(); });
|
||||||
|
connect(maxSpin_, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
|
||||||
|
[this](double) { syncHistogramSel(); });
|
||||||
|
|
||||||
auto* btnLay = new QHBoxLayout();
|
auto* btnLay = new QHBoxLayout();
|
||||||
btnLay->addStretch();
|
btnLay->addStretch();
|
||||||
|
|
@ -68,6 +83,10 @@ ScatterFilterDialog::ScatterFilterDialog(geopro::data::IDatasetCommandRepository
|
||||||
loadConfig();
|
loadConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ScatterFilterDialog::syncHistogramSel() {
|
||||||
|
if (histogram_) histogram_->setSelection(minSpin_->value(), maxSpin_->value());
|
||||||
|
}
|
||||||
|
|
||||||
void ScatterFilterDialog::loadConfig() {
|
void ScatterFilterDialog::loadConfig() {
|
||||||
if (!repo_) return;
|
if (!repo_) return;
|
||||||
QPointer<ScatterFilterDialog> self(this);
|
QPointer<ScatterFilterDialog> self(this);
|
||||||
|
|
@ -81,6 +100,7 @@ void ScatterFilterDialog::loadConfig() {
|
||||||
self->rangeLabel_->setText(QStringLiteral("数值范围:%1 — %2")
|
self->rangeLabel_->setText(QStringLiteral("数值范围:%1 — %2")
|
||||||
.arg(QString::number(mn, 'g', 6),
|
.arg(QString::number(mn, 'g', 6),
|
||||||
QString::number(mx, 'g', 6)));
|
QString::number(mx, 'g', 6)));
|
||||||
|
self->syncHistogramSel(); // 初值就位后同步直方图选区
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
|
@ -12,20 +14,25 @@ class IDatasetCommandRepository;
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
// 「数据过滤」对话框(复刻原版 web dataFilter.vue 的范围过滤部分):
|
class ScatterHistogramView;
|
||||||
// 打开时经 getScatterFilterConfig 取 min/max 初值填入「最小值/最大值」输入框;
|
|
||||||
|
// 「数据过滤」对话框(复刻原版 web dataFilter.vue):
|
||||||
|
// 左侧数值分布直方图(自绘 ScatterHistogram,由打开方传入当前 V 值数组),
|
||||||
|
// 右侧 min/max 输入框;改 min/max 时直方图同步高亮选区(对照原版 .filter-indicator)。
|
||||||
|
// 打开时经 getScatterFilterConfig 取 min/max 初值填入输入框;
|
||||||
// 「应用过滤」经 applyScatterFilter 生成过滤后数据集({sourceDsObjectId, sourceVFieldCode, min, max})。
|
// 「应用过滤」经 applyScatterFilter 生成过滤后数据集({sourceDsObjectId, sourceVFieldCode, min, max})。
|
||||||
// 直方图(原版左侧 D3 分布图)本轮后置:范围过滤为核心,直方图仅可视化辅助。
|
|
||||||
// 回调用 QPointer 守卫(虽 modal exec,仍异步回调)。
|
// 回调用 QPointer 守卫(虽 modal exec,仍异步回调)。
|
||||||
class ScatterFilterDialog : public QDialog {
|
class ScatterFilterDialog : public QDialog {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
|
// values = 当前 V 值数组(与图上散点 v 同源,驱动直方图分布);可空(无值则直方图空态)。
|
||||||
ScatterFilterDialog(geopro::data::IDatasetCommandRepository* repo, QString dsObjectId,
|
ScatterFilterDialog(geopro::data::IDatasetCommandRepository* repo, QString dsObjectId,
|
||||||
QString vFieldCode, QWidget* parent = nullptr);
|
QString vFieldCode, std::vector<double> values, QWidget* parent = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void loadConfig(); // 取 min/max 初值
|
void loadConfig(); // 取 min/max 初值
|
||||||
void onApply(); // 应用过滤
|
void onApply(); // 应用过滤
|
||||||
|
void syncHistogramSel(); // 把当前 min/max 同步到直方图选区高亮
|
||||||
|
|
||||||
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
|
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
|
||||||
QString dsObjectId_;
|
QString dsObjectId_;
|
||||||
|
|
@ -35,6 +42,7 @@ private:
|
||||||
QDoubleSpinBox* minSpin_ = nullptr;
|
QDoubleSpinBox* minSpin_ = nullptr;
|
||||||
QDoubleSpinBox* maxSpin_ = nullptr;
|
QDoubleSpinBox* maxSpin_ = nullptr;
|
||||||
QPushButton* applyBtn_ = nullptr;
|
QPushButton* applyBtn_ = nullptr;
|
||||||
|
ScatterHistogramView* histogram_ = nullptr; // 左侧分布直方图
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
#include "panels/chart/ScatterHistogram.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include <QPaintEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
|
||||||
|
#include "panels/chart/ScatterDataOps.hpp" // buildScatterHistogram
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int kBinCount = 20; // 分箱数(对照原版 D3 stepRange=20)
|
||||||
|
const QColor kBarIn(64, 128, 255); // 选区内柱:蓝(对照原版高亮)
|
||||||
|
const QColor kBarOut(200, 205, 215); // 选区外柱:灰
|
||||||
|
const QColor kIndicator(64, 128, 255, 51); // 选区指示矩形:rgba(64,128,255,0.2)
|
||||||
|
const QColor kAxis(150, 150, 150); // 轴线/刻度
|
||||||
|
constexpr int kPadL = 8; // 左右内边距
|
||||||
|
constexpr int kPadR = 8;
|
||||||
|
constexpr int kPadTop = 8; // 顶部内边距
|
||||||
|
constexpr int kAxisH = 18; // 底部刻度区高度
|
||||||
|
constexpr int kBarGap = 1; // 柱间距(像素)
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ScatterHistogramView::ScatterHistogramView(QWidget* parent) : QWidget(parent) {
|
||||||
|
setMinimumHeight(160);
|
||||||
|
setMinimumWidth(280);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScatterHistogramView::setValues(const std::vector<double>& values) {
|
||||||
|
values_.clear();
|
||||||
|
values_.reserve(values.size());
|
||||||
|
for (double x : values)
|
||||||
|
if (std::isfinite(x)) values_.push_back(x);
|
||||||
|
if (values_.empty()) {
|
||||||
|
dataMin_ = dataMax_ = 0.0;
|
||||||
|
} else {
|
||||||
|
dataMin_ = *std::min_element(values_.begin(), values_.end());
|
||||||
|
dataMax_ = *std::max_element(values_.begin(), values_.end());
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScatterHistogramView::setSelection(double min, double max) {
|
||||||
|
selMin_ = min;
|
||||||
|
selMax_ = max;
|
||||||
|
hasSel_ = (min <= max);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScatterHistogramView::paintEvent(QPaintEvent*) {
|
||||||
|
QPainter p(this);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing, false);
|
||||||
|
|
||||||
|
const QRect r = rect();
|
||||||
|
const int plotL = r.left() + kPadL;
|
||||||
|
const int plotR = r.right() - kPadR;
|
||||||
|
const int plotTop = r.top() + kPadTop;
|
||||||
|
const int plotBottom = r.bottom() - kAxisH;
|
||||||
|
const int plotW = plotR - plotL;
|
||||||
|
const int plotH = plotBottom - plotTop;
|
||||||
|
if (plotW <= 0 || plotH <= 0) return;
|
||||||
|
|
||||||
|
// 数据域无效(无值/退化区间)→ 仅画基线,空态。
|
||||||
|
if (values_.empty() || !(dataMax_ > dataMin_)) {
|
||||||
|
p.setPen(kAxis);
|
||||||
|
p.drawLine(plotL, plotBottom, plotR, plotBottom);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在全量数据域上分箱(每柱代表一个等宽区间)。
|
||||||
|
const auto h = buildScatterHistogram(values_, dataMin_, dataMax_, kBinCount);
|
||||||
|
const int n = static_cast<int>(h.counts.size());
|
||||||
|
if (n <= 0) return;
|
||||||
|
const int maxCount = *std::max_element(h.counts.begin(), h.counts.end());
|
||||||
|
if (maxCount <= 0) {
|
||||||
|
p.setPen(kAxis);
|
||||||
|
p.drawLine(plotL, plotBottom, plotR, plotBottom);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 值 → 像素 x 的映射(数据域 [dataMin,dataMax] → [plotL,plotR])。
|
||||||
|
auto xPix = [&](double val) {
|
||||||
|
return plotL + (val - dataMin_) / (dataMax_ - dataMin_) * plotW;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选区指示矩形(先画,柱叠其上)。
|
||||||
|
if (hasSel_ && selMax_ > selMin_) {
|
||||||
|
const double lo = std::clamp(selMin_, dataMin_, dataMax_);
|
||||||
|
const double hi = std::clamp(selMax_, dataMin_, dataMax_);
|
||||||
|
const QRectF ind(xPix(lo), plotTop, xPix(hi) - xPix(lo), plotH);
|
||||||
|
p.fillRect(ind, kIndicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 画柱:高度按计数归一;选区内蓝、选区外灰。
|
||||||
|
const double binW = static_cast<double>(plotW) / n;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
const double binLo = dataMin_ + i * h.step;
|
||||||
|
const double binHi = binLo + h.step;
|
||||||
|
const int barH = static_cast<int>(static_cast<double>(h.counts[static_cast<std::size_t>(i)]) /
|
||||||
|
maxCount * plotH);
|
||||||
|
const int bx = static_cast<int>(plotL + i * binW) + kBarGap;
|
||||||
|
const int bw = std::max(1, static_cast<int>(binW) - 2 * kBarGap);
|
||||||
|
// 柱中心落在选区内 → 高亮(与原版“区间内点高亮”观感一致)。
|
||||||
|
const double binCenter = (binLo + binHi) / 2.0;
|
||||||
|
const bool inSel = hasSel_ && binCenter >= selMin_ && binCenter <= selMax_;
|
||||||
|
p.fillRect(bx, plotBottom - barH, bw, barH, inSel ? kBarIn : kBarOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部基线 + 两端数值刻度(min/max)。
|
||||||
|
p.setPen(kAxis);
|
||||||
|
p.drawLine(plotL, plotBottom, plotR, plotBottom);
|
||||||
|
p.drawText(QRect(plotL, plotBottom, plotW / 2, kAxisH), Qt::AlignLeft | Qt::AlignVCenter,
|
||||||
|
QString::number(dataMin_, 'g', 4));
|
||||||
|
p.drawText(QRect(plotL + plotW / 2, plotBottom, plotW / 2, kAxisH),
|
||||||
|
Qt::AlignRight | Qt::AlignVCenter, QString::number(dataMax_, 'g', 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 数值分布直方图(M3 数据过滤对话框左侧分布图,复刻原版 dataFilter.vue 的 D3 直方图)。
|
||||||
|
// 自绘 QWidget:按全量 v 值的数据域分箱画柱;当前选定 [min,max] 区间内的柱高亮蓝、区间外灰,
|
||||||
|
// 并在选区上叠加半透明蓝色指示矩形(对照原版 .filter-indicator)。x 轴底部画数值刻度。
|
||||||
|
// 选区由对话框输入框/范围联动 setSelection 更新(本控件只读展示,不发选择信号)。
|
||||||
|
// 命名加 View 后缀以与 ScatterDataOps.hpp 的数据结构 struct ScatterHistogram(分箱结果)区分,
|
||||||
|
// 避免同名 geopro::app::ScatterHistogram 在同一 TU 内冲突。
|
||||||
|
class ScatterHistogramView : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit ScatterHistogramView(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// 设置全量 v 值(取有限值数据域作为分箱总区间,分箱数固定 kBinCount)。
|
||||||
|
void setValues(const std::vector<double>& values);
|
||||||
|
// 设置当前选定区间(区间内柱高亮 + 指示矩形)。min>max 时不画指示矩形。
|
||||||
|
void setSelection(double min, double max);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<double> values_; // 全量有限 v 值(已过滤 NaN/inf)
|
||||||
|
double dataMin_ = 0.0; // 数据域下界(分箱总区间)
|
||||||
|
double dataMax_ = 0.0; // 数据域上界
|
||||||
|
double selMin_ = 0.0; // 当前选区下界
|
||||||
|
double selMax_ = 0.0; // 当前选区上界
|
||||||
|
bool hasSel_ = false; // 选区有效(selMin<=selMax)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
#include "panels/chart/ScatterMarqueePicker.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QRect>
|
||||||
|
#include <QRectF>
|
||||||
|
#include <QRubberBand>
|
||||||
|
|
||||||
|
#include <qwt_plot.h>
|
||||||
|
#include <qwt_plot_canvas.h>
|
||||||
|
#include <qwt_scale_map.h>
|
||||||
|
|
||||||
|
#include "model/Field.hpp"
|
||||||
|
#include "panels/chart/ChartPickGeometry.hpp"
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
ScatterMarqueePicker::ScatterMarqueePicker(QwtPlot* plot, int xAxis, int yAxis, QObject* parent)
|
||||||
|
: QObject(parent), plot_(plot), xAxis_(xAxis), yAxis_(yAxis) {
|
||||||
|
// 后装于 LivePanner → 事件链中先收到(active 时优先消费左键拖拽,禁用平移)。
|
||||||
|
if (plot_ && plot_->canvas()) plot_->canvas()->installEventFilter(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScatterMarqueePicker::setActive(bool on) {
|
||||||
|
active_ = on;
|
||||||
|
if (!on) {
|
||||||
|
dragging_ = false;
|
||||||
|
if (band_) band_->hide();
|
||||||
|
if (plot_ && plot_->canvas()) plot_->canvas()->unsetCursor();
|
||||||
|
} else if (plot_ && plot_->canvas()) {
|
||||||
|
plot_->canvas()->setCursor(Qt::CrossCursor); // 对齐原版 crosshair
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScatterMarqueePicker::eventFilter(QObject* obj, QEvent* ev) {
|
||||||
|
if (!active_ || !plot_ || obj != plot_->canvas()) return QObject::eventFilter(obj, ev);
|
||||||
|
|
||||||
|
switch (ev->type()) {
|
||||||
|
case QEvent::MouseButtonPress: {
|
||||||
|
auto* me = static_cast<QMouseEvent*>(ev);
|
||||||
|
if (me->button() != Qt::LeftButton) break;
|
||||||
|
dragging_ = true;
|
||||||
|
origin_ = me->pos();
|
||||||
|
if (!band_) band_ = new QRubberBand(QRubberBand::Rectangle, plot_->canvas());
|
||||||
|
band_->setGeometry(QRect(origin_, QSize()));
|
||||||
|
band_->show();
|
||||||
|
return true; // 消费 → 不触发 LivePanner 平移
|
||||||
|
}
|
||||||
|
case QEvent::MouseMove: {
|
||||||
|
if (!dragging_) break;
|
||||||
|
auto* me = static_cast<QMouseEvent*>(ev);
|
||||||
|
if (band_) band_->setGeometry(QRect(origin_, me->pos()).normalized());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case QEvent::MouseButtonRelease: {
|
||||||
|
auto* me = static_cast<QMouseEvent*>(ev);
|
||||||
|
if (me->button() != Qt::LeftButton || !dragging_) break;
|
||||||
|
dragging_ = false;
|
||||||
|
if (band_) band_->hide();
|
||||||
|
finishSelection(me->pos());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case QEvent::MouseButtonDblClick: {
|
||||||
|
// 框选态吞掉双击,避免穿透触发 LivePanner(行为一致)。
|
||||||
|
auto* me = static_cast<QMouseEvent*>(ev);
|
||||||
|
if (me->button() == Qt::LeftButton) return true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return QObject::eventFilter(obj, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScatterMarqueePicker::finishSelection(const QPoint& endPos) {
|
||||||
|
if (!field_ || !onSelected_ || !plot_) return;
|
||||||
|
// 近似单击(拖拽距离 < 3px)→ 视为误触,不产生空选回调(避免清掉已有选区)。
|
||||||
|
const QRect pxBox = QRect(origin_, endPos).normalized();
|
||||||
|
if (pxBox.width() < 3 && pxBox.height() < 3) return;
|
||||||
|
// 像素橡皮筋 → 数据坐标矩形(两端点各自反变换后取 normalized)。
|
||||||
|
const QwtScaleMap xMap = plot_->canvasMap(xAxis_);
|
||||||
|
const QwtScaleMap yMap = plot_->canvasMap(yAxis_);
|
||||||
|
const double x0 = xMap.invTransform(origin_.x());
|
||||||
|
const double x1 = xMap.invTransform(endPos.x());
|
||||||
|
const double y0 = yMap.invTransform(origin_.y());
|
||||||
|
const double y1 = yMap.invTransform(endPos.y());
|
||||||
|
QRectF rect(QPointF(std::min(x0, x1), std::min(y0, y1)),
|
||||||
|
QPointF(std::max(x0, x1), std::max(y0, y1)));
|
||||||
|
onSelected_(pointsInRect(*field_, rect));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
|
class QwtPlot;
|
||||||
|
class QRubberBand;
|
||||||
|
|
||||||
|
namespace geopro::core {
|
||||||
|
struct ScatterField;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// M14 框选拾取器:开启后接管画布左键拖拽,画橡皮筋矩形 → 松手把框内散点下标回调出去。
|
||||||
|
// 复刻原版散点「点选模式」(plotly selectmode:'select' 的 box-select 框选):开启时禁用平移
|
||||||
|
// (事件优先于 LivePanner 消费拖拽),关闭时让位平移/hover。本类不拥有 plot/field(外部持有,
|
||||||
|
// 地址稳定);用 QPointer 守 plot,父子树管理生命周期。
|
||||||
|
class ScatterMarqueePicker : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
// xAxis/yAxis:散点所在轴(RawData 为 xTop/yLeft)。field 由 RawDataChartView 持有(其
|
||||||
|
// 成员 data_.scatter,地址稳定);onSelected 回调收框内下标(数据坐标命中)。
|
||||||
|
ScatterMarqueePicker(QwtPlot* plot, int xAxis, int yAxis, QObject* parent = nullptr);
|
||||||
|
|
||||||
|
void setField(const geopro::core::ScatterField* field) { field_ = field; }
|
||||||
|
void setOnSelected(std::function<void(const std::vector<int>&)> cb) { onSelected_ = std::move(cb); }
|
||||||
|
|
||||||
|
// 开/关框选模式。关闭时收起橡皮筋(不清已选——清选区由调用方决定)。
|
||||||
|
void setActive(bool on);
|
||||||
|
bool isActive() const { return active_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject* obj, QEvent* ev) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void finishSelection(const QPoint& endPos);
|
||||||
|
|
||||||
|
QPointer<QwtPlot> plot_;
|
||||||
|
int xAxis_;
|
||||||
|
int yAxis_;
|
||||||
|
const geopro::core::ScatterField* field_ = nullptr;
|
||||||
|
std::function<void(const std::vector<int>&)> onSelected_;
|
||||||
|
|
||||||
|
QRubberBand* band_ = nullptr; // 父=canvas,随之析构
|
||||||
|
bool active_ = false;
|
||||||
|
bool dragging_ = false;
|
||||||
|
QPoint origin_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -47,6 +47,27 @@ QRectF ScatterPlotItem::boundingRect() const {
|
||||||
return bounding_;
|
return bounding_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ScatterPlotItem::setSelectedIndices(const std::vector<int>& indices) {
|
||||||
|
selected_.clear();
|
||||||
|
const int n = static_cast<int>(std::min(field_.x.size(), field_.y.size()));
|
||||||
|
for (int i : indices)
|
||||||
|
if (i >= 0 && i < n) selected_.insert(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScatterPlotItem::clearSelection() {
|
||||||
|
selected_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<QString> ScatterPlotItem::getSelectedIds() const {
|
||||||
|
std::vector<QString> ids;
|
||||||
|
for (int i : selected_) {
|
||||||
|
if (i < 0 || static_cast<std::size_t>(i) >= field_.id.size()) continue;
|
||||||
|
const QString id = QString::fromStdString(field_.id[i]);
|
||||||
|
if (!id.isEmpty()) ids.push_back(id);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
void ScatterPlotItem::draw(QPainter* painter,
|
void ScatterPlotItem::draw(QPainter* painter,
|
||||||
const QwtScaleMap& xMap,
|
const QwtScaleMap& xMap,
|
||||||
const QwtScaleMap& yMap,
|
const QwtScaleMap& yMap,
|
||||||
|
|
@ -82,8 +103,13 @@ void ScatterPlotItem::draw(QPainter* painter,
|
||||||
// 隐藏数据方块:电极已绘,直接收尾(保留电极菱形)。
|
// 隐藏数据方块:电极已绘,直接收尾(保留电极菱形)。
|
||||||
if (!scatterVisible_) { painter->restore(); return; }
|
if (!scatterVisible_) { painter->restore(); return; }
|
||||||
|
|
||||||
painter->setPen(QPen(Qt::white, kPenWidth));
|
const QPen normalPen(Qt::white, kPenWidth);
|
||||||
|
const QPen selectPen(QColor(255, 0, 0), kSelectPenWidth); // M14 选中红框
|
||||||
|
const auto& status = field_.displayStatus; // 0=显示 1=隐藏(M14 选中隐藏后逐点生效)
|
||||||
|
const bool hasStatus = status.size() == n;
|
||||||
for (std::size_t i = 0; i < n; ++i) {
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
// 逐点隐藏(M14:仅隐藏选中点):displayStatus!=0 的点不绘制。
|
||||||
|
if (hasStatus && status[i] != 0) continue;
|
||||||
double px = xMap.transform(xs[i]);
|
double px = xMap.transform(xs[i]);
|
||||||
double py = yMap.transform(ys[i]);
|
double py = yMap.transform(ys[i]);
|
||||||
double val = (i < vs.size()) ? vs[i] : 0.0;
|
double val = (i < vs.size()) ? vs[i] : 0.0;
|
||||||
|
|
@ -91,6 +117,7 @@ void ScatterPlotItem::draw(QPainter* painter,
|
||||||
// 与 RawDataChartView 的 setDataRange 的 isfinite 跳过一致。
|
// 与 RawDataChartView 的 setDataRange 的 isfinite 跳过一致。
|
||||||
if (!std::isfinite(val)) continue;
|
if (!std::isfinite(val)) continue;
|
||||||
auto c = colorSvc_->colorAtContinuous(val);
|
auto c = colorSvc_->colorAtContinuous(val);
|
||||||
|
painter->setPen(selected_.count(static_cast<int>(i)) ? selectPen : normalPen);
|
||||||
painter->setBrush(QColor(c.r, c.g, c.b, c.a));
|
painter->setBrush(QColor(c.r, c.g, c.b, c.a));
|
||||||
painter->drawRect(QRectF(px - kHalfSide, py - kHalfSide,
|
painter->drawRect(QRectF(px - kHalfSide, py - kHalfSide,
|
||||||
kHalfSide * 2.0, kHalfSide * 2.0));
|
kHalfSide * 2.0, kHalfSide * 2.0));
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "model/Field.hpp"
|
#include "model/Field.hpp"
|
||||||
#include "panels/chart/ColorMapService.hpp"
|
#include "panels/chart/ColorMapService.hpp"
|
||||||
#include <qwt_plot_item.h>
|
#include <qwt_plot_item.h>
|
||||||
#include <QRectF>
|
#include <QRectF>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
class QPainter;
|
class QPainter;
|
||||||
class QwtScaleMap;
|
class QwtScaleMap;
|
||||||
|
|
@ -22,6 +26,13 @@ public:
|
||||||
// 显示/隐藏数据方块(measurement 工具条“显示/隐藏”)。电极菱形不受影响,始终绘制。
|
// 显示/隐藏数据方块(measurement 工具条“显示/隐藏”)。电极菱形不受影响,始终绘制。
|
||||||
void setScatterVisible(bool on) { scatterVisible_ = on; }
|
void setScatterVisible(bool on) { scatterVisible_ = on; }
|
||||||
|
|
||||||
|
// M14 框选:设/清选中点下标集(红色加粗描边高亮,对齐原版 marker.line red/2)。
|
||||||
|
void setSelectedIndices(const std::vector<int>& indices);
|
||||||
|
void clearSelection();
|
||||||
|
// M14:当前选中点的 id(对照原版 getSelectedPointIds,供 saveDisplayStatus)。空 id 跳过。
|
||||||
|
std::vector<QString> getSelectedIds() const;
|
||||||
|
bool hasSelection() const { return !selected_.empty(); }
|
||||||
|
|
||||||
int rtti() const override { return QwtPlotItem::Rtti_PlotUserItem; }
|
int rtti() const override { return QwtPlotItem::Rtti_PlotUserItem; }
|
||||||
|
|
||||||
QRectF boundingRect() const override;
|
QRectF boundingRect() const override;
|
||||||
|
|
@ -36,6 +47,7 @@ private:
|
||||||
ColorMapService* colorSvc_ = nullptr; // 不拥有,由 RawDataChartView 持有
|
ColorMapService* colorSvc_ = nullptr; // 不拥有,由 RawDataChartView 持有
|
||||||
QRectF bounding_;
|
QRectF bounding_;
|
||||||
bool scatterVisible_ = true; // false:仅画电极,隐藏数据方块
|
bool scatterVisible_ = true; // false:仅画电极,隐藏数据方块
|
||||||
|
std::set<int> selected_; // M14 选中点下标(红框高亮)
|
||||||
|
|
||||||
// 数据方块:原版 Plotly marker.size 12px(绝对像素直径)→ 半边长 6px,真 1:1。
|
// 数据方块:原版 Plotly marker.size 12px(绝对像素直径)→ 半边长 6px,真 1:1。
|
||||||
static constexpr double kHalfSide = 6.0; // 方块半边长(像素,全宽 12px)
|
static constexpr double kHalfSide = 6.0; // 方块半边长(像素,全宽 12px)
|
||||||
|
|
@ -44,6 +56,8 @@ private:
|
||||||
// 相对数据方块 ≈1.33×(16 vs 12),实心 #BEBEBE 填充 + 白色 2px 描边。
|
// 相对数据方块 ≈1.33×(16 vs 12),实心 #BEBEBE 填充 + 白色 2px 描边。
|
||||||
static constexpr double kElectrodeHalfSide = 8.0; // 半对角(像素,全宽 16px)
|
static constexpr double kElectrodeHalfSide = 8.0; // 半对角(像素,全宽 16px)
|
||||||
static constexpr double kElectrodePenWidth = 2.0; // 电极白色描边宽度(像素)
|
static constexpr double kElectrodePenWidth = 2.0; // 电极白色描边宽度(像素)
|
||||||
|
// M14 选中描边:红 2px(对齐原版 marker.line color:'red' width:2)。
|
||||||
|
static constexpr double kSelectPenWidth = 2.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,11 @@ struct TablePayload {
|
||||||
int pageNo = 1; // 当前页(1 基);分页用
|
int pageNo = 1; // 当前页(1 基);分页用
|
||||||
int pageSize = 0; // 每页条数;>0 才渲染分页器(vxe-pager),0=不分页
|
int pageSize = 0; // 每页条数;>0 才渲染分页器(vxe-pager),0=不分页
|
||||||
std::vector<TableFunctionButton> functionButtons; // dd_grid 功能按钮(其余场景空)
|
std::vector<TableFunctionButton> functionButtons; // dd_grid 功能按钮(其余场景空)
|
||||||
|
// M2 行级显隐:仅 measurement 列表置 true(载荷驱动门控,其余视图复用 DataTableView 保持只读)。
|
||||||
|
// 为 true 时 Toggle 列可点击 → popconfirm → saveDisplayStatus(rowIds[i], 取反)。
|
||||||
|
bool toggleInteractive = false;
|
||||||
|
// 每行点 id(与 rows 同序、一一对应,saveDisplayStatus ids[] 用);仅 measurement 填充。
|
||||||
|
std::vector<QString> rowIds;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 柱状图系列:名称(图例/legend)+ 各类目的 y 值 + 填充色(hex,如 #5470c6;数据色,两主题一致)。
|
// 柱状图系列:名称(图例/legend)+ 各类目的 y 值 + 填充色(hex,如 #5470c6;数据色,两主题一致)。
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,14 @@ QString strAt(const QJsonArray& arr, int i) {
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 取对象里的字符串字段(id 可能为字符串或数字形态,统一转字符串)。
|
||||||
|
QString objStr(const QJsonObject& obj, const QString& key) {
|
||||||
|
const QJsonValue v = obj.value(key);
|
||||||
|
if (v.isString()) return v.toString();
|
||||||
|
if (v.isDouble()) return QString::number(v.toDouble(), 'f', 0);
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
// 把 JSON 值预格式化为单元格 QString(数字按原值,null/缺省→空串)。
|
// 把 JSON 值预格式化为单元格 QString(数字按原值,null/缺省→空串)。
|
||||||
QString cellText(const QJsonValue& v) {
|
QString cellText(const QJsonValue& v) {
|
||||||
if (v.isDouble()) {
|
if (v.isDouble()) {
|
||||||
|
|
@ -149,6 +157,8 @@ TablePayload parseMeasurementTable(const QJsonObject& data) {
|
||||||
toggle.title = QStringLiteral("隐藏/显示");
|
toggle.title = QStringLiteral("隐藏/显示");
|
||||||
toggle.kind = TableColumnKind::Toggle;
|
toggle.kind = TableColumnKind::Toggle;
|
||||||
t.columns.push_back(toggle);
|
t.columns.push_back(toggle);
|
||||||
|
// M2:仅 measurement 列表的 Toggle 列可交互(载荷驱动门控,其余复用视图保持只读)。
|
||||||
|
t.toggleInteractive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 行:每格按列码先查 vmap,再查行对象顶层。
|
// 行:每格按列码先查 vmap,再查行对象顶层。
|
||||||
|
|
@ -169,6 +179,8 @@ TablePayload parseMeasurementTable(const QJsonObject& data) {
|
||||||
cells.push_back(cellText(v));
|
cells.push_back(cellText(v));
|
||||||
}
|
}
|
||||||
t.rows.push_back(std::move(cells));
|
t.rows.push_back(std::move(cells));
|
||||||
|
// M2:每行点 id(saveDisplayStatus ids[] 用);仅 measurement(hasToggleCol) 填充。
|
||||||
|
if (hasToggleCol) t.rowIds.push_back(objStr(obj, QStringLiteral("id")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 总数(分页用):measurement 用 __rowListTotal,回退到本批行数。
|
// 总数(分页用):measurement 用 __rowListTotal,回退到本批行数。
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,11 @@ target_sources(geopro_tests PRIVATE
|
||||||
app/test_scatter_data_ops.cpp
|
app/test_scatter_data_ops.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/app/panels/chart/ScatterDataOps.cpp
|
${CMAKE_SOURCE_DIR}/src/app/panels/chart/ScatterDataOps.cpp
|
||||||
)
|
)
|
||||||
|
# 图上交互纯几何(M14 框选命中 pointsInRect / I9 绘形归一化,QtCore QPointF/QRectF + core model)。
|
||||||
|
target_sources(geopro_tests PRIVATE
|
||||||
|
app/test_chart_pick_geometry.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/app/panels/chart/ChartPickGeometry.cpp
|
||||||
|
)
|
||||||
# 反演处理类纯逻辑(网格化/白化/滤波 请求体组装 + code 映射,仅 Qt6::Core JSON)。
|
# 反演处理类纯逻辑(网格化/白化/滤波 请求体组装 + code 映射,仅 Qt6::Core JSON)。
|
||||||
target_sources(geopro_tests PRIVATE
|
target_sources(geopro_tests PRIVATE
|
||||||
app/test_inversion_process_ops.cpp
|
app/test_inversion_process_ops.cpp
|
||||||
|
|
@ -160,6 +165,13 @@ target_sources(geopro_tests PRIVATE
|
||||||
app/test_contour_simplify.cpp
|
app/test_contour_simplify.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/app/panels/chart/ContourSimplify.cpp
|
${CMAKE_SOURCE_DIR}/src/app/panels/chart/ContourSimplify.cpp
|
||||||
)
|
)
|
||||||
|
# Quill Delta ↔ QTextDocument 互转(I14 描述富文本,纯函数,Qt6::Core/Gui)。
|
||||||
|
find_package(Qt6 COMPONENTS Gui REQUIRED)
|
||||||
|
target_sources(geopro_tests PRIVATE
|
||||||
|
app/test_quill_delta.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/app/panels/QuillDelta.cpp
|
||||||
|
)
|
||||||
|
target_link_libraries(geopro_tests PRIVATE Qt6::Gui)
|
||||||
|
|
||||||
# controller 层:DatasetDetailController 编排集成测试(QSignalSpy 验证 datasetOpened/tabReady/loadFailed)。
|
# controller 层:DatasetDetailController 编排集成测试(QSignalSpy 验证 datasetOpened/tabReady/loadFailed)。
|
||||||
find_package(Qt6 COMPONENTS Test REQUIRED)
|
find_package(Qt6 COMPONENTS Test REQUIRED)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QRectF>
|
||||||
|
|
||||||
|
#include "panels/chart/ChartPickGeometry.hpp"
|
||||||
|
|
||||||
|
using namespace geopro::app;
|
||||||
|
using geopro::core::ScatterField;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
ScatterField makeField() {
|
||||||
|
ScatterField f;
|
||||||
|
f.x = {0.0, 1.0, 2.0, 3.0};
|
||||||
|
f.y = {0.0, 1.0, 2.0, 3.0};
|
||||||
|
f.id = {"a", "b", "c", "d"};
|
||||||
|
f.displayStatus = {0, 0, 0, 0};
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(ChartPickGeometry, PointsInRectSelectsInterior) {
|
||||||
|
auto f = makeField();
|
||||||
|
// 矩形 [0.5,2.5]×[0.5,2.5] → 命中下标 1(1,1) 与 2(2,2)。
|
||||||
|
auto hits = pointsInRect(f, QRectF(QPointF(0.5, 0.5), QPointF(2.5, 2.5)));
|
||||||
|
ASSERT_EQ(hits.size(), 2u);
|
||||||
|
EXPECT_EQ(hits[0], 1);
|
||||||
|
EXPECT_EQ(hits[1], 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ChartPickGeometry, PointsInRectEmptyWhenOutside) {
|
||||||
|
auto f = makeField();
|
||||||
|
auto hits = pointsInRect(f, QRectF(QPointF(10.0, 10.0), QPointF(20.0, 20.0)));
|
||||||
|
EXPECT_TRUE(hits.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ChartPickGeometry, PointsInRectSkipsHidden) {
|
||||||
|
auto f = makeField();
|
||||||
|
f.displayStatus = {0, 1, 0, 0}; // 下标 1 隐藏 → 不参与框选
|
||||||
|
auto hits = pointsInRect(f, QRectF(QPointF(0.5, 0.5), QPointF(2.5, 2.5)));
|
||||||
|
ASSERT_EQ(hits.size(), 1u);
|
||||||
|
EXPECT_EQ(hits[0], 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ChartPickGeometry, PointsInRectSkipsNonFinite) {
|
||||||
|
ScatterField f;
|
||||||
|
f.x = {0.0, std::nan("")};
|
||||||
|
f.y = {0.0, 0.0};
|
||||||
|
auto hits = pointsInRect(f, QRectF(QPointF(-1.0, -1.0), QPointF(1.0, 1.0)));
|
||||||
|
ASSERT_EQ(hits.size(), 1u);
|
||||||
|
EXPECT_EQ(hits[0], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ChartPickGeometry, MinPointsForMarkType) {
|
||||||
|
EXPECT_EQ(minPointsForMarkType(1), 1); // 点
|
||||||
|
EXPECT_EQ(minPointsForMarkType(2), 2); // 线
|
||||||
|
EXPECT_EQ(minPointsForMarkType(3), 3); // 面
|
||||||
|
EXPECT_EQ(minPointsForMarkType(4), 1); // 文字
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ChartPickGeometry, NormalizePointTakesFirstOnly) {
|
||||||
|
std::vector<QPointF> pts{{1.0, 2.0}, {3.0, 4.0}};
|
||||||
|
auto out = normalizeDrawnPoints(pts, /*point*/ 1);
|
||||||
|
ASSERT_EQ(out.size(), 1u);
|
||||||
|
EXPECT_DOUBLE_EQ(out[0].x(), 1.0);
|
||||||
|
// 文字同样取首点。
|
||||||
|
EXPECT_EQ(normalizeDrawnPoints(pts, /*text*/ 4).size(), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ChartPickGeometry, NormalizeLineKeepsAll) {
|
||||||
|
std::vector<QPointF> pts{{1.0, 2.0}, {3.0, 4.0}, {5.0, 6.0}};
|
||||||
|
EXPECT_EQ(normalizeDrawnPoints(pts, /*line*/ 2).size(), 3u);
|
||||||
|
EXPECT_EQ(normalizeDrawnPoints(pts, /*polygon*/ 3).size(), 3u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ChartPickGeometry, CanClosePolygon) {
|
||||||
|
EXPECT_FALSE(canClosePolygon(2));
|
||||||
|
EXPECT_TRUE(canClosePolygon(3));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QTextBlock>
|
||||||
|
#include <QTextDocument>
|
||||||
|
#include <QTextFragment>
|
||||||
|
|
||||||
|
#include "panels/QuillDelta.hpp"
|
||||||
|
|
||||||
|
using namespace geopro::app;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QJsonArray opsFromJson(const char* json) {
|
||||||
|
return QJsonDocument::fromJson(json).array();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Delta ops 取第 idx 个文本 op 的 attributes(便于断言)。
|
||||||
|
QJsonObject attrsOf(const QJsonArray& ops, int idx) {
|
||||||
|
return ops.at(idx).toObject().value(QStringLiteral("attributes")).toObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ── 反序列化(Delta → 文档):基本行内样式落到字符格式 ──────────────────────
|
||||||
|
TEST(QuillDelta, DeltaToDocumentAppliesInlineFormats) {
|
||||||
|
const auto ops = opsFromJson(R"([
|
||||||
|
{"insert":"Hello","attributes":{"bold":true,"italic":true,"underline":true,
|
||||||
|
"color":"#ff0000","size":"24px"}},
|
||||||
|
{"insert":"\n"}
|
||||||
|
])");
|
||||||
|
QTextDocument doc;
|
||||||
|
deltaToDocument(ops, doc);
|
||||||
|
|
||||||
|
EXPECT_EQ(doc.toPlainText(), QStringLiteral("Hello"));
|
||||||
|
const QTextFragment frag = doc.begin().begin().fragment();
|
||||||
|
ASSERT_TRUE(frag.isValid());
|
||||||
|
const QTextCharFormat f = frag.charFormat();
|
||||||
|
EXPECT_GE(f.fontWeight(), QFont::Bold);
|
||||||
|
EXPECT_TRUE(f.fontItalic());
|
||||||
|
EXPECT_TRUE(f.fontUnderline());
|
||||||
|
EXPECT_EQ(f.foreground().color().name(QColor::HexRgb), QStringLiteral("#ff0000"));
|
||||||
|
EXPECT_NEAR(f.fontPointSize(), 24.0 * 3.0 / 4.0, 0.01); // 24px → 18pt
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 序列化(文档 → Delta):行内样式回写 attributes ─────────────────────────
|
||||||
|
TEST(QuillDelta, DocumentToDeltaEmitsInlineAttrs) {
|
||||||
|
QTextDocument doc;
|
||||||
|
QTextCursor cur(&doc);
|
||||||
|
QTextCharFormat f;
|
||||||
|
f.setFontWeight(QFont::Bold);
|
||||||
|
f.setForeground(QColor(QStringLiteral("#00ff00")));
|
||||||
|
cur.insertText(QStringLiteral("World"), f);
|
||||||
|
|
||||||
|
const QJsonArray ops = documentToDelta(doc);
|
||||||
|
ASSERT_GE(ops.size(), 1);
|
||||||
|
EXPECT_EQ(ops.at(0).toObject().value(QStringLiteral("insert")).toString(),
|
||||||
|
QStringLiteral("World"));
|
||||||
|
const QJsonObject a = attrsOf(ops, 0);
|
||||||
|
EXPECT_TRUE(a.value(QStringLiteral("bold")).toBool());
|
||||||
|
EXPECT_EQ(a.value(QStringLiteral("color")).toString(), QStringLiteral("#00ff00"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 往返:纯文本不丢、不多生成空块 ─────────────────────────────────────────
|
||||||
|
TEST(QuillDelta, RoundTripPlainTextSingleLine) {
|
||||||
|
const auto ops = opsFromJson(R"([{"insert":"just text"},{"insert":"\n"}])");
|
||||||
|
QTextDocument doc;
|
||||||
|
deltaToDocument(ops, doc);
|
||||||
|
EXPECT_EQ(doc.toPlainText(), QStringLiteral("just text"));
|
||||||
|
EXPECT_EQ(doc.blockCount(), 1); // 不应多出尾部空块
|
||||||
|
|
||||||
|
const QJsonArray back = documentToDelta(doc);
|
||||||
|
QString text;
|
||||||
|
for (const QJsonValue& v : back)
|
||||||
|
text += v.toObject().value(QStringLiteral("insert")).toString();
|
||||||
|
EXPECT_EQ(text, QStringLiteral("just text\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 往返:多行 + 行内样式保持 ──────────────────────────────────────────────
|
||||||
|
TEST(QuillDelta, RoundTripMultilineWithBoldPreserved) {
|
||||||
|
const auto ops = opsFromJson(R"([
|
||||||
|
{"insert":"line1 "},
|
||||||
|
{"insert":"bold","attributes":{"bold":true}},
|
||||||
|
{"insert":"\nline2\n"}
|
||||||
|
])");
|
||||||
|
QTextDocument doc;
|
||||||
|
deltaToDocument(ops, doc);
|
||||||
|
EXPECT_EQ(doc.toPlainText(), QStringLiteral("line1 bold\nline2"));
|
||||||
|
EXPECT_EQ(doc.blockCount(), 2);
|
||||||
|
|
||||||
|
// 第二趟往返稳定:重新序列化后再反序列化文本一致。
|
||||||
|
const QJsonArray back = documentToDelta(doc);
|
||||||
|
QTextDocument doc2;
|
||||||
|
deltaToDocument(back, doc2);
|
||||||
|
EXPECT_EQ(doc2.toPlainText(), QStringLiteral("line1 bold\nline2"));
|
||||||
|
EXPECT_EQ(doc2.blockCount(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 块级:标题落到 headingLevel 并回写 header ──────────────────────────────
|
||||||
|
TEST(QuillDelta, HeaderBlockRoundTrip) {
|
||||||
|
const auto ops = opsFromJson(R"([
|
||||||
|
{"insert":"Title"},
|
||||||
|
{"insert":"\n","attributes":{"header":2}},
|
||||||
|
{"insert":"body"},
|
||||||
|
{"insert":"\n"}
|
||||||
|
])");
|
||||||
|
QTextDocument doc;
|
||||||
|
deltaToDocument(ops, doc);
|
||||||
|
EXPECT_EQ(doc.begin().blockFormat().headingLevel(), 2);
|
||||||
|
|
||||||
|
const QJsonArray back = documentToDelta(doc);
|
||||||
|
// 找到带 header 的换行 op。
|
||||||
|
bool found = false;
|
||||||
|
for (const QJsonValue& v : back) {
|
||||||
|
const QJsonObject op = v.toObject();
|
||||||
|
if (op.value(QStringLiteral("insert")).toString() == QStringLiteral("\n") &&
|
||||||
|
op.value(QStringLiteral("attributes")).toObject().value(QStringLiteral("header")).toInt() == 2) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 块级:有序列表 → ListDecimal,回写 list:ordered ───────────────────────
|
||||||
|
TEST(QuillDelta, OrderedListBlockRoundTrip) {
|
||||||
|
const auto ops = opsFromJson(R"([
|
||||||
|
{"insert":"item"},
|
||||||
|
{"insert":"\n","attributes":{"list":"ordered"}}
|
||||||
|
])");
|
||||||
|
QTextDocument doc;
|
||||||
|
deltaToDocument(ops, doc);
|
||||||
|
ASSERT_NE(doc.begin().textList(), nullptr);
|
||||||
|
|
||||||
|
const QJsonArray back = documentToDelta(doc);
|
||||||
|
bool found = false;
|
||||||
|
for (const QJsonValue& v : back) {
|
||||||
|
const QJsonObject a = v.toObject().value(QStringLiteral("attributes")).toObject();
|
||||||
|
if (a.value(QStringLiteral("list")).toString() == QStringLiteral("ordered")) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 容错:无法识别的 attributes 降级(保留文本,不崩) ──────────────────────
|
||||||
|
TEST(QuillDelta, UnknownAttributesDegradeGracefully) {
|
||||||
|
const auto ops = opsFromJson(R"([
|
||||||
|
{"insert":"keepme","attributes":{"script":"super","strike":true,"link":"http://x"}},
|
||||||
|
{"insert":"\n"}
|
||||||
|
])");
|
||||||
|
QTextDocument doc;
|
||||||
|
deltaToDocument(ops, doc);
|
||||||
|
EXPECT_EQ(doc.toPlainText(), QStringLiteral("keepme"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 容错:非文本 insert(图片/嵌入对象)被丢弃,不崩 ───────────────────────
|
||||||
|
TEST(QuillDelta, NonStringInsertDropped) {
|
||||||
|
const auto ops = opsFromJson(R"([
|
||||||
|
{"insert":"text"},
|
||||||
|
{"insert":{"image":"data:..."}},
|
||||||
|
{"insert":"\n"}
|
||||||
|
])");
|
||||||
|
QTextDocument doc;
|
||||||
|
deltaToDocument(ops, doc);
|
||||||
|
EXPECT_EQ(doc.toPlainText(), QStringLiteral("text"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 空 ops:空文档 ─────────────────────────────────────────────────────────
|
||||||
|
TEST(QuillDelta, EmptyOpsYieldEmptyDocument) {
|
||||||
|
QTextDocument doc;
|
||||||
|
deltaToDocument(QJsonArray{}, doc);
|
||||||
|
EXPECT_TRUE(doc.toPlainText().isEmpty());
|
||||||
|
}
|
||||||
|
|
@ -85,3 +85,40 @@ TEST(ScatterDataOps, SaveRawDataBodyOverwriteOmitsName) {
|
||||||
EXPECT_EQ(body.value("operationType").toInt(), 0);
|
EXPECT_EQ(body.value("operationType").toInt(), 0);
|
||||||
EXPECT_FALSE(body.contains("name")); // 覆盖不带 name
|
EXPECT_FALSE(body.contains("name")); // 覆盖不带 name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(ScatterDataOps, ToggledDisplayStatus) {
|
||||||
|
// 0=显示 → 1=隐藏;1=隐藏 → 0=显示(对照原版 record.displayStatus ? 0 : 1)。
|
||||||
|
EXPECT_EQ(toggledDisplayStatus(0), 1);
|
||||||
|
EXPECT_EQ(toggledDisplayStatus(1), 0);
|
||||||
|
EXPECT_EQ(toggledDisplayStatus(2), 0); // 非 0 视为隐藏 → 显示
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScatterDataOps, HistogramBinsCountInRange) {
|
||||||
|
// 0..10 均匀 11 个点,min=0 max=10 分 5 箱(宽 2):
|
||||||
|
// [0,2):0,1 → 2;[2,4):2,3 → 2;[4,6):4,5 → 2;[6,8):6,7 → 2;[8,10]:8,9,10 → 3(末箱右闭)。
|
||||||
|
std::vector<double> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
||||||
|
auto h = buildScatterHistogram(v, 0.0, 10.0, 5);
|
||||||
|
ASSERT_EQ(h.counts.size(), 5u);
|
||||||
|
EXPECT_DOUBLE_EQ(h.step, 2.0);
|
||||||
|
EXPECT_EQ(h.counts[0], 2);
|
||||||
|
EXPECT_EQ(h.counts[1], 2);
|
||||||
|
EXPECT_EQ(h.counts[2], 2);
|
||||||
|
EXPECT_EQ(h.counts[3], 2);
|
||||||
|
EXPECT_EQ(h.counts[4], 3); // 末箱含恰等于 max 的点
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScatterDataOps, HistogramSkipsOutOfRangeAndNonFinite) {
|
||||||
|
std::vector<double> v{-5, 0, 5, 10, 15, std::nan("")};
|
||||||
|
auto h = buildScatterHistogram(v, 0.0, 10.0, 2);
|
||||||
|
ASSERT_EQ(h.counts.size(), 2u);
|
||||||
|
// 区间外(-5,15)与 NaN 跳过;保留 0,5,10。
|
||||||
|
// [0,5):0 → 1;[5,10]:5,10 → 2。
|
||||||
|
EXPECT_EQ(h.counts[0], 1);
|
||||||
|
EXPECT_EQ(h.counts[1], 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScatterDataOps, HistogramDegenerateReturnsEmpty) {
|
||||||
|
std::vector<double> v{1, 2, 3};
|
||||||
|
EXPECT_TRUE(buildScatterHistogram(v, 5.0, 5.0, 10).counts.empty()); // min==max
|
||||||
|
EXPECT_TRUE(buildScatterHistogram(v, 0.0, 10.0, 0).counts.empty()); // binCount<=0
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -272,3 +272,30 @@ TEST(MeasurementDto, ToggleOffWhenDisplayStatusNonZero) {
|
||||||
EXPECT_EQ(t.rows[0].back().toStdString(), "1"); // displayStatus 0 → ON
|
EXPECT_EQ(t.rows[0].back().toStdString(), "1"); // displayStatus 0 → ON
|
||||||
EXPECT_EQ(t.rows[1].back().toStdString(), "0"); // displayStatus 1 → OFF
|
EXPECT_EQ(t.rows[1].back().toStdString(), "0"); // displayStatus 1 → OFF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(MeasurementDto, MeasurementListIsToggleInteractiveWithRowIds) {
|
||||||
|
// M2:measurement 列表(filedList 驱动)→ toggleInteractive=true + 每行点 id。
|
||||||
|
auto t = parseMeasurementTable(obj(R"({
|
||||||
|
"filedList": [{"fieldCode":"a","name":"A"}],
|
||||||
|
"rowList": [
|
||||||
|
{"id":"x","a":1,"displayStatus":0},
|
||||||
|
{"id":1453611521843201,"a":2,"displayStatus":1}
|
||||||
|
]
|
||||||
|
})"));
|
||||||
|
EXPECT_TRUE(t.toggleInteractive);
|
||||||
|
ASSERT_EQ(t.rowIds.size(), 2u);
|
||||||
|
EXPECT_EQ(t.rowIds[0].toStdString(), "x");
|
||||||
|
EXPECT_EQ(t.rowIds[1].toStdString(), "1453611521843201"); // 数字 id 转字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MeasurementDto, GridListNotToggleInteractive) {
|
||||||
|
// grid/trajectory 列表(gridHeaderDisplay 驱动)→ 无 Toggle 列、不可交互、无 rowIds。
|
||||||
|
auto t = parseMeasurementTable(obj(R"({
|
||||||
|
"gridHeaderDisplay": [{"fieldCode":"a","name":"A"}],
|
||||||
|
"rowList": [ {"a":1}, {"a":2} ]
|
||||||
|
})"));
|
||||||
|
EXPECT_FALSE(t.toggleInteractive);
|
||||||
|
EXPECT_TRUE(t.rowIds.empty());
|
||||||
|
for (const auto& c : t.columns)
|
||||||
|
EXPECT_NE(c.kind, geopro::core::TableColumnKind::Toggle);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue