565 lines
26 KiB
C++
565 lines
26 KiB
C++
#include "ColorScaleConfigDialog.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <utility>
|
||
|
||
#include <QCheckBox>
|
||
#include <QColor>
|
||
#include <QColorDialog>
|
||
#include <QDialogButtonBox>
|
||
#include <QHBoxLayout>
|
||
#include <QHeaderView>
|
||
#include <QInputDialog>
|
||
#include <QLabel>
|
||
#include <QLineEdit>
|
||
#include <QPushButton>
|
||
#include <QTableWidget>
|
||
#include <QTableWidgetItem>
|
||
#include <QVBoxLayout>
|
||
|
||
#include <QFile>
|
||
#include <QFileDialog>
|
||
#include <QMessageBox>
|
||
|
||
#include <QJsonArray>
|
||
#include <QJsonObject>
|
||
#include <QJsonValue>
|
||
#include <QPointer>
|
||
#include <QRegularExpression>
|
||
|
||
#include "ColorGradientDialog.hpp"
|
||
#include "ColorScaleIO.hpp"
|
||
#include "ContourLevelDialog.hpp"
|
||
#include "ContourLevels.hpp"
|
||
#include "ContourLineDialog.hpp"
|
||
#include "repo/IColorTemplateRepository.hpp"
|
||
|
||
namespace geopro::app {
|
||
|
||
namespace {
|
||
QColor toQColor(const geopro::core::Rgba& c) {
|
||
return QColor(c.r, c.g, c.b, c.a);
|
||
}
|
||
geopro::core::Rgba fromQColor(const QColor& c) {
|
||
return geopro::core::Rgba{static_cast<unsigned char>(c.red()),
|
||
static_cast<unsigned char>(c.green()),
|
||
static_cast<unsigned char>(c.blue()),
|
||
static_cast<unsigned char>(c.alpha())};
|
||
}
|
||
// 两端按比例 t∈[0,1] 线性插值(含 alpha),供「新增」取中间色。
|
||
geopro::core::Rgba lerp(const geopro::core::Rgba& a, const geopro::core::Rgba& b, double t) {
|
||
auto mix = [t](unsigned char x, unsigned char y) {
|
||
return static_cast<unsigned char>(x + (y - x) * t + 0.5);
|
||
};
|
||
return geopro::core::Rgba{mix(a.r, b.r), mix(a.g, b.g), mix(a.b, b.b), mix(a.a, b.a)};
|
||
}
|
||
// core::Rgba → 颜色串:不透明用 #RRGGBB,半透明用 rgba(r,g,b,a∈0..1)(与后端 colorBar 互通)。
|
||
QString rgbaToCss(const geopro::core::Rgba& c) {
|
||
if (c.a >= 255)
|
||
return QStringLiteral("#%1%2%3")
|
||
.arg(c.r, 2, 16, QLatin1Char('0'))
|
||
.arg(c.g, 2, 16, QLatin1Char('0'))
|
||
.arg(c.b, 2, 16, QLatin1Char('0'))
|
||
.toUpper();
|
||
return QStringLiteral("rgba(%1, %2, %3, %4)")
|
||
.arg(c.r)
|
||
.arg(c.g)
|
||
.arg(c.b)
|
||
.arg(QString::number(c.a / 255.0, 'g', 3));
|
||
}
|
||
// 颜色串 → core::Rgba:支持 #RRGGBB / #AARRGGBB / rgb()/rgba()(alpha 0..1)/命名黑。
|
||
geopro::core::Rgba parseCssColor(const QString& s) {
|
||
const QString t = s.trimmed();
|
||
if (t.startsWith('#')) {
|
||
const QColor q(t); // QColor 识别 #RRGGBB / #AARRGGBB
|
||
if (q.isValid()) return fromQColor(q);
|
||
}
|
||
static const QRegularExpression re(
|
||
QStringLiteral("rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(?:,\\s*([0-9.]+))?\\)"),
|
||
QRegularExpression::CaseInsensitiveOption);
|
||
const auto m = re.match(t);
|
||
if (m.hasMatch()) {
|
||
const int r = m.captured(1).toInt();
|
||
const int g = m.captured(2).toInt();
|
||
const int b = m.captured(3).toInt();
|
||
double a = m.captured(4).isEmpty() ? 1.0 : m.captured(4).toDouble();
|
||
if (a > 1.0) a = a / 255.0; // 容错:偶有 0..255 alpha
|
||
return geopro::core::Rgba{static_cast<unsigned char>(r), static_cast<unsigned char>(g),
|
||
static_cast<unsigned char>(b),
|
||
static_cast<unsigned char>(a * 255.0 + 0.5)};
|
||
}
|
||
return geopro::core::Rgba{0, 0, 0, 255};
|
||
}
|
||
} // namespace
|
||
|
||
ColorScaleConfigDialog::ColorScaleConfigDialog(const geopro::core::ColorScale& init, double vmin,
|
||
double vmax, std::vector<double> samples,
|
||
const ContourLineConfig& lineInit,
|
||
geopro::data::IColorTemplateRepository* tplRepo,
|
||
QString projectId, QString lvlTemplateId,
|
||
QWidget* parent)
|
||
: QDialog(parent),
|
||
vmin_(vmin),
|
||
vmax_(vmax),
|
||
samples_(std::move(samples)),
|
||
lineCfg_(lineInit),
|
||
tplRepo_(tplRepo),
|
||
projectId_(std::move(projectId)),
|
||
lvlTemplateId_(std::move(lvlTemplateId)) {
|
||
setWindowTitle(QStringLiteral("色阶配置"));
|
||
setModal(true);
|
||
resize(560, 420);
|
||
|
||
// 用初始色阶的升序断点填模型;空色阶兜底成 vmin/vmax 两端蓝红。
|
||
globalOpacity_ = init.globalOpacity(); // 回显真实整体透明度(两级第二级),不再硬编码 1
|
||
for (const auto& [value, color] : init.stops()) rows_.push_back({value, color});
|
||
if (rows_.empty()) {
|
||
rows_.push_back({vmin_, geopro::core::Rgba{0, 0, 255, 255}});
|
||
rows_.push_back({vmax_, geopro::core::Rgba{255, 0, 0, 255}});
|
||
}
|
||
|
||
auto* root = new QVBoxLayout(this);
|
||
auto* mid = new QHBoxLayout();
|
||
root->addLayout(mid, 1);
|
||
|
||
// 左:三列表格(层级 / 线形 / 颜色),每列表头带 ⚙,点击表头打开对应子对话框。
|
||
table_ = new QTableWidget(this);
|
||
table_->setColumnCount(3);
|
||
table_->setHorizontalHeaderLabels(
|
||
{QStringLiteral("层级 ⚙"), QStringLiteral("线形 ⚙"), QStringLiteral("颜色 ⚙")});
|
||
table_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||
table_->horizontalHeader()->setSectionsClickable(true);
|
||
table_->verticalHeader()->setVisible(false);
|
||
table_->setSortingEnabled(false);
|
||
table_->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||
table_->setSelectionMode(QAbstractItemView::SingleSelection);
|
||
table_->setEditTriggers(QAbstractItemView::NoEditTriggers); // 改值/改色走双击
|
||
connect(table_, &QTableWidget::cellDoubleClicked, this,
|
||
&ColorScaleConfigDialog::onCellDoubleClicked);
|
||
connect(table_->horizontalHeader(), &QHeaderView::sectionClicked, this, [this](int section) {
|
||
if (section == 0)
|
||
onLevelScheme();
|
||
else if (section == 1)
|
||
onLineScheme();
|
||
else
|
||
onColorScheme();
|
||
});
|
||
mid->addWidget(table_, 1);
|
||
|
||
// 右:竖排按钮 新增 / 删除 / 另存为 / 导出 / 导入 / 打开(复刻 colorLevel.vue 操作列)。
|
||
auto* rightCol = new QVBoxLayout();
|
||
auto* btnAdd = new QPushButton(QStringLiteral("新增"), this);
|
||
auto* btnDel = new QPushButton(QStringLiteral("删除"), this);
|
||
btnSaveOther_ = new QPushButton(QStringLiteral("另存"), this);
|
||
auto* btnExport = new QPushButton(QStringLiteral("导出"), this);
|
||
auto* btnImport = new QPushButton(QStringLiteral("导入"), this);
|
||
btnOpen_ = new QPushButton(QStringLiteral("打开"), this);
|
||
connect(btnAdd, &QPushButton::clicked, this, &ColorScaleConfigDialog::onAdd);
|
||
connect(btnDel, &QPushButton::clicked, this, &ColorScaleConfigDialog::onRemove);
|
||
connect(btnSaveOther_, &QPushButton::clicked, this, &ColorScaleConfigDialog::onSaveOther);
|
||
connect(btnExport, &QPushButton::clicked, this, &ColorScaleConfigDialog::onExportLvl);
|
||
connect(btnImport, &QPushButton::clicked, this, &ColorScaleConfigDialog::onImportLvl);
|
||
connect(btnOpen_, &QPushButton::clicked, this, &ColorScaleConfigDialog::onOpen);
|
||
for (auto* b : {btnAdd, btnDel, btnSaveOther_, btnExport, btnImport, btnOpen_})
|
||
rightCol->addWidget(b);
|
||
rightCol->addStretch();
|
||
mid->addLayout(rightCol);
|
||
|
||
// 「另存为 / 打开」依赖后端 lvl 模板库(走仓储),无仓储/无项目时禁用。
|
||
const bool hasBackend = tplRepo_ != nullptr && !projectId_.isEmpty();
|
||
btnSaveOther_->setEnabled(hasBackend);
|
||
btnOpen_->setEnabled(hasBackend);
|
||
|
||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||
buttons->button(QDialogButtonBox::Ok)->setText(QStringLiteral("应用"));
|
||
buttons->button(QDialogButtonBox::Cancel)->setText(QStringLiteral("取消"));
|
||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||
root->addWidget(buttons);
|
||
|
||
rebuildTable();
|
||
}
|
||
|
||
void ColorScaleConfigDialog::rebuildTable() {
|
||
const int n = static_cast<int>(rows_.size());
|
||
table_->setRowCount(n);
|
||
const QString solid = QStringLiteral("——————");
|
||
const QString dashed = QStringLiteral("- - - - - - - - -");
|
||
const QColor lineQc = toQColor(lineCfg_.lineColor);
|
||
// 升序显示:低值在上(复刻原版 tableData 自然数组序)。
|
||
for (int r = 0; r < n; ++r) {
|
||
const Row& row = rows_[static_cast<std::size_t>(r)];
|
||
auto* valItem = new QTableWidgetItem(QString::number(row.value, 'g', 6));
|
||
valItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||
table_->setItem(r, 0, valItem);
|
||
|
||
auto* lineItem = new QTableWidgetItem(lineCfg_.dashed ? dashed : solid);
|
||
lineItem->setForeground(lineQc);
|
||
lineItem->setTextAlignment(Qt::AlignCenter);
|
||
table_->setItem(r, 1, lineItem);
|
||
|
||
auto* colItem = new QTableWidgetItem();
|
||
colItem->setBackground(toQColor(row.color));
|
||
table_->setItem(r, 2, colItem);
|
||
}
|
||
}
|
||
|
||
int ColorScaleConfigDialog::selectedModelIndex() const {
|
||
const int r = table_->currentRow();
|
||
if (r < 0 || r >= static_cast<int>(rows_.size())) return -1;
|
||
return r; // 升序显示,行号即模型下标
|
||
}
|
||
|
||
void ColorScaleConfigDialog::onCellDoubleClicked(int row, int col) {
|
||
if (row < 0 || row >= static_cast<int>(rows_.size())) return;
|
||
const int idx = row;
|
||
|
||
if (col == 0) { // 改层级值(复刻 handleLevelDblClick)
|
||
bool ok = false;
|
||
const double v = QInputDialog::getDouble(this, QStringLiteral("修改层级值"),
|
||
QStringLiteral("数据值"), rows_[idx].value, -1e12,
|
||
1e12, 6, &ok);
|
||
if (!ok) return;
|
||
const geopro::core::Rgba color = rows_[idx].color;
|
||
rows_[idx].value = v;
|
||
std::sort(rows_.begin(), rows_.end(),
|
||
[](const Row& a, const Row& b) { return a.value < b.value; });
|
||
rebuildTable();
|
||
for (int i = 0; i < static_cast<int>(rows_.size()); ++i) {
|
||
const Row& ri = rows_[static_cast<std::size_t>(i)];
|
||
if (ri.value == v && ri.color.r == color.r && ri.color.g == color.g &&
|
||
ri.color.b == color.b && ri.color.a == color.a) {
|
||
table_->selectRow(i);
|
||
break;
|
||
}
|
||
}
|
||
} else if (col == 2) { // 改颜色(复刻 handleColorDblClick)
|
||
const QColor cur = toQColor(rows_[idx].color);
|
||
const QColor picked = QColorDialog::getColor(cur, this, QStringLiteral("选择颜色"),
|
||
QColorDialog::ShowAlphaChannel);
|
||
if (!picked.isValid()) return;
|
||
rows_[idx].color = fromQColor(picked);
|
||
rebuildTable();
|
||
table_->selectRow(row);
|
||
}
|
||
// 线形列(col==1)双击无动作,复刻原版(线形改动走表头 ⚙)。
|
||
}
|
||
|
||
void ColorScaleConfigDialog::onAdd() {
|
||
// 复刻 handleAdd:选中行上方插入中点断点;未选中则提示。
|
||
const int idx = selectedModelIndex();
|
||
if (idx < 0) {
|
||
QMessageBox::warning(this, QStringLiteral("新增"), QStringLiteral("请先选择要插入的行。"));
|
||
return;
|
||
}
|
||
const Row& sel = rows_[static_cast<std::size_t>(idx)];
|
||
double newLevel = sel.value;
|
||
if (idx > 0) // 升序:上一行(idx-1)为更低值,取两者中点
|
||
newLevel = (rows_[static_cast<std::size_t>(idx - 1)].value + sel.value) / 2.0;
|
||
rows_.insert(rows_.begin() + idx, Row{newLevel, sel.color});
|
||
rebuildTable();
|
||
table_->selectRow(idx); // 选中新插入行
|
||
}
|
||
|
||
void ColorScaleConfigDialog::onRemove() {
|
||
// 复刻 handleDelete:未选中提示;至少保留 2 行。
|
||
const int idx = selectedModelIndex();
|
||
if (idx < 0) {
|
||
QMessageBox::warning(this, QStringLiteral("删除"), QStringLiteral("请先选择要删除的行。"));
|
||
return;
|
||
}
|
||
if (rows_.size() <= 2) {
|
||
QMessageBox::warning(this, QStringLiteral("删除"), QStringLiteral("至少需要保留两行数据。"));
|
||
return;
|
||
}
|
||
rows_.erase(rows_.begin() + idx);
|
||
rebuildTable();
|
||
table_->clearSelection(); // 复刻 handleDelete:删除后清空选中
|
||
}
|
||
|
||
geopro::core::Rgba ColorScaleConfigDialog::interpColor(double value) const {
|
||
// 复刻 colorUtils.js mapColors:升序断点上钳位 + 找区间 + 线性 RGBA 插值。
|
||
if (rows_.empty()) return geopro::core::Rgba{0, 0, 0, 255};
|
||
const double ysMin = rows_.front().value, ysMax = rows_.back().value;
|
||
if (value <= ysMin) return rows_.front().color;
|
||
if (value >= ysMax) return rows_.back().color;
|
||
std::size_t i = 0;
|
||
while (i + 1 < rows_.size() && value > rows_[i + 1].value) ++i;
|
||
const double x0 = rows_[i].value, x1 = rows_[i + 1].value;
|
||
const double ratio = (x1 > x0) ? (value - x0) / (x1 - x0) : 0.0;
|
||
return lerp(rows_[i].color, rows_[i + 1].color, ratio);
|
||
}
|
||
|
||
void ColorScaleConfigDialog::onLevelScheme() {
|
||
// 由当前断点推导 contourLevel 初值(复刻 colorLevel.vue case 'level')。
|
||
ContourLevelParams init;
|
||
init.method = ContourLevelParams::Method::Normal;
|
||
if (lvlSchemeType_ == QStringLiteral("logarithmic"))
|
||
init.method = ContourLevelParams::Method::Logarithmic;
|
||
else if (lvlSchemeType_ == QStringLiteral("equalArea"))
|
||
init.method = ContourLevelParams::Method::EqualArea;
|
||
init.minValue = rows_.front().value;
|
||
init.maxValue = rows_.back().value;
|
||
init.layerCount = static_cast<int>(rows_.size());
|
||
init.interval =
|
||
(rows_.size() >= 2) ? std::abs(rows_[1].value - rows_[0].value) : (vmax_ - vmin_);
|
||
init.logLinesCount = logLinesCount_;
|
||
init.equalAreaLayerCount = equalAreaLayerCount_;
|
||
const double totalArea =
|
||
samples_.empty() ? 1000.0 : static_cast<double>(samples_.size()); // 等积「区间面积」分母
|
||
ContourLevelDialog dlg(init, vmin_, vmax_, totalArea, this);
|
||
if (dlg.exec() != QDialog::Accepted) return;
|
||
const ContourLevelParams p = dlg.params();
|
||
|
||
// 记录方案字段(另存为 properties 透传,复刻原版)。
|
||
switch (p.method) {
|
||
case ContourLevelParams::Method::Logarithmic:
|
||
lvlSchemeType_ = QStringLiteral("logarithmic");
|
||
break;
|
||
case ContourLevelParams::Method::EqualArea:
|
||
lvlSchemeType_ = QStringLiteral("equalArea");
|
||
break;
|
||
default:
|
||
lvlSchemeType_ = QStringLiteral("normal");
|
||
break;
|
||
}
|
||
logLinesCount_ = p.logLinesCount;
|
||
equalAreaLayerCount_ = p.equalAreaLayerCount;
|
||
|
||
// 1) 按分层方式生成新层级(纯算法)。 2) 旧色阶上插值取色(mapColors),重建表。
|
||
const std::vector<double> levels = generateContourLevels(p, samples_);
|
||
std::vector<Row> next;
|
||
next.reserve(levels.size());
|
||
for (double lv : levels) next.push_back({lv, interpColor(lv)});
|
||
if (next.size() < 2) return; // 退化保护
|
||
std::sort(next.begin(), next.end(),
|
||
[](const Row& a, const Row& b) { return a.value < b.value; });
|
||
rows_ = std::move(next);
|
||
rebuildTable();
|
||
}
|
||
|
||
void ColorScaleConfigDialog::onLineScheme() {
|
||
ContourLineDialog dlg(lineCfg_, this);
|
||
if (dlg.exec() == QDialog::Accepted) {
|
||
lineCfg_ = dlg.config();
|
||
rebuildTable(); // 线形列文字/颜色随之刷新
|
||
}
|
||
}
|
||
|
||
void ColorScaleConfigDialog::onColorScheme() {
|
||
if (rows_.size() < 2) return; // 防御:front/back
|
||
// 用当前断点归一化位置作渐变初值(lo..hi → 0..1)。
|
||
const double lo = rows_.front().value, hi = rows_.back().value;
|
||
const double span = (hi > lo) ? (hi - lo) : 1.0;
|
||
std::vector<GradientEditWidget::Stop> seed;
|
||
for (const auto& r : rows_) seed.push_back({(r.value - lo) / span, r.color});
|
||
|
||
ColorGradientDialog dlg(seed, lo, hi, vmin_, vmax_, samples_, globalOpacity_, tplRepo_, projectId_,
|
||
this);
|
||
if (dlg.exec() != QDialog::Accepted) return;
|
||
|
||
const auto grad = dlg.stops();
|
||
if (grad.size() < 2) return;
|
||
globalOpacity_ = dlg.opacity(); // 两级第二级:整体透明度单独存,不烘焙进每色 alpha
|
||
|
||
// 在新渐变上按各层级位置连续采样回填颜色(复刻 mapColors,含每色自有 alpha)。
|
||
auto sampleGrad = [&](double pos) -> geopro::core::Rgba {
|
||
if (pos <= grad.front().pos) return grad.front().color;
|
||
if (pos >= grad.back().pos) return grad.back().color;
|
||
std::size_t i = 0;
|
||
while (i + 1 < grad.size() && pos > grad[i + 1].pos) ++i;
|
||
const double x0 = grad[i].pos, x1 = grad[i + 1].pos;
|
||
const double t = (x1 > x0) ? (pos - x0) / (x1 - x0) : 0.0;
|
||
return lerp(grad[i].color, grad[i + 1].color, t);
|
||
};
|
||
// 只回填颜色(含每色自有 alpha),整体透明度单独存于 globalOpacity_、渲染时才相乘(两级)。
|
||
for (auto& r : rows_) r.color = sampleGrad((r.value - lo) / span);
|
||
rebuildTable();
|
||
}
|
||
|
||
void ColorScaleConfigDialog::onImportLvl() {
|
||
const QString path = QFileDialog::getOpenFileName(this, QStringLiteral("导入 .lvl"), {},
|
||
QStringLiteral("色阶层级文件 (*.lvl)"));
|
||
if (path.isEmpty()) return;
|
||
QFile f(path);
|
||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||
QMessageBox::warning(this, QStringLiteral("导入"), QStringLiteral("无法打开文件。"));
|
||
return;
|
||
}
|
||
const std::vector<LvlRow> parsed = parseLvl(f.readAll().toStdString());
|
||
if (parsed.size() < 2) {
|
||
QMessageBox::warning(this, QStringLiteral("导入"),
|
||
QStringLiteral("文件格式不正确或层级不足。"));
|
||
return;
|
||
}
|
||
rows_.clear();
|
||
for (const auto& lr : parsed) rows_.push_back({lr.level, lr.color});
|
||
std::sort(rows_.begin(), rows_.end(),
|
||
[](const Row& a, const Row& b) { return a.value < b.value; });
|
||
lineCfg_.dashed = parsed.front().dashed; // 线形从首行带入
|
||
lineCfg_.lineColor = parsed.front().lineColor;
|
||
rebuildTable();
|
||
}
|
||
|
||
void ColorScaleConfigDialog::onExportLvl() {
|
||
const QString path = QFileDialog::getSaveFileName(this, QStringLiteral("导出 .lvl"),
|
||
QStringLiteral("等值线配置.lvl"),
|
||
QStringLiteral("色阶层级文件 (*.lvl)"));
|
||
if (path.isEmpty()) return;
|
||
std::vector<LvlRow> out;
|
||
for (const auto& r : rows_)
|
||
out.push_back({r.value, r.color, lineCfg_.dashed, lineCfg_.lineColor});
|
||
QFile f(path);
|
||
if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||
QMessageBox::warning(this, QStringLiteral("导出"), QStringLiteral("无法写入文件。"));
|
||
return;
|
||
}
|
||
const std::string text = generateLvl(out);
|
||
if (f.write(text.c_str(), static_cast<qint64>(text.size())) < 0)
|
||
QMessageBox::warning(this, QStringLiteral("导出"), QStringLiteral("写入失败。"));
|
||
}
|
||
|
||
void ColorScaleConfigDialog::loadColorBar(
|
||
const std::vector<std::pair<double, geopro::core::Rgba>>& bar) {
|
||
if (bar.size() < 2) return;
|
||
rows_.clear();
|
||
for (const auto& [level, color] : bar) rows_.push_back({level, color});
|
||
std::sort(rows_.begin(), rows_.end(),
|
||
[](const Row& a, const Row& b) { return a.value < b.value; });
|
||
rebuildTable();
|
||
if (!rows_.empty()) table_->selectRow(0); // 复刻 handleOpen:载入后默认选中首行
|
||
}
|
||
|
||
void ColorScaleConfigDialog::onSaveOther() {
|
||
if (tplRepo_ == nullptr || projectId_.isEmpty()) return;
|
||
|
||
// 自定义另存为弹窗(复刻 handleSaveOther):名称输入 + 覆盖复选框。
|
||
// 「覆盖」仅当有来源模板 id(lvlTemplateId_ 非空)时可勾选,对照原版 props.data.lvlTemplateId。
|
||
QDialog askDlg(this);
|
||
askDlg.setWindowTitle(QStringLiteral("另存模板配置"));
|
||
askDlg.setModal(true);
|
||
auto* askRoot = new QVBoxLayout(&askDlg);
|
||
auto* nameRow = new QHBoxLayout();
|
||
nameRow->addWidget(new QLabel(QStringLiteral("模板名称:"), &askDlg));
|
||
auto* nameEdit = new QLineEdit(QStringLiteral("等值线配置.lvl"), &askDlg);
|
||
nameRow->addWidget(nameEdit, 1);
|
||
askRoot->addLayout(nameRow);
|
||
auto* overwriteCheck = new QCheckBox(QStringLiteral("覆盖原模板"), &askDlg);
|
||
overwriteCheck->setEnabled(!lvlTemplateId_.isEmpty()); // 无来源模板 → 禁用覆盖
|
||
askRoot->addWidget(overwriteCheck);
|
||
auto* askBtns = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &askDlg);
|
||
askBtns->button(QDialogButtonBox::Ok)->setText(QStringLiteral("应用"));
|
||
askBtns->button(QDialogButtonBox::Cancel)->setText(QStringLiteral("取消"));
|
||
connect(askBtns, &QDialogButtonBox::accepted, &askDlg, &QDialog::accept);
|
||
connect(askBtns, &QDialogButtonBox::rejected, &askDlg, &QDialog::reject);
|
||
askRoot->addWidget(askBtns);
|
||
if (askDlg.exec() != QDialog::Accepted) return;
|
||
const QString name = nameEdit->text().trimmed();
|
||
if (name.isEmpty()) return;
|
||
const bool overwrite = overwriteCheck->isChecked() && !lvlTemplateId_.isEmpty();
|
||
|
||
// 组装 properties(复刻 handleSaveOther)。
|
||
QJsonArray colorBar;
|
||
for (const auto& r : rows_)
|
||
colorBar.append(QJsonArray{QString::number(r.value, 'f', 2), rgbaToCss(r.color)});
|
||
QJsonObject lineConfig{{QStringLiteral("showLines"), lineCfg_.lineShow},
|
||
{QStringLiteral("color"), rgbaToCss(lineCfg_.lineColor)},
|
||
{QStringLiteral("lineType"),
|
||
lineCfg_.dashed ? QStringLiteral("dashed") : QStringLiteral("solid")}};
|
||
QJsonObject labelConfig{{QStringLiteral("showLabels"), lineCfg_.labelShow},
|
||
{QStringLiteral("color"), rgbaToCss(lineCfg_.labelColor)}};
|
||
QJsonObject properties{{QStringLiteral("lineConfig"), lineConfig},
|
||
{QStringLiteral("labelConfig"), labelConfig},
|
||
{QStringLiteral("lvlSchemeType"), lvlSchemeType_},
|
||
{QStringLiteral("logLinesCount"), logLinesCount_},
|
||
{QStringLiteral("equalAreaLayerCount"), equalAreaLayerCount_},
|
||
{QStringLiteral("colorBar"), colorBar}};
|
||
|
||
// 走仓储传输;回调里用 QPointer 守卫 this(模态对话框可能已关)。
|
||
// 勾选覆盖 → PUT 更新来源模板(updateLvlTemplate);否则 → POST 新建(saveLvlTemplate)。
|
||
QPointer<ColorScaleConfigDialog> self(this);
|
||
auto onDone = [self, overwrite](bool ok, QString msg) {
|
||
if (!self) return;
|
||
if (ok)
|
||
QMessageBox::information(
|
||
self, QStringLiteral("另存"),
|
||
overwrite ? QStringLiteral("更新成功。") : QStringLiteral("另存成功。"));
|
||
else
|
||
QMessageBox::warning(self, QStringLiteral("另存"),
|
||
QStringLiteral("另存失败:%1").arg(msg));
|
||
};
|
||
if (overwrite)
|
||
tplRepo_->updateLvlTemplate(lvlTemplateId_, name, properties, std::move(onDone));
|
||
else
|
||
tplRepo_->saveLvlTemplate(projectId_, name, properties, std::move(onDone));
|
||
}
|
||
|
||
void ColorScaleConfigDialog::onOpen() {
|
||
if (tplRepo_ == nullptr || projectId_.isEmpty()) return;
|
||
QPointer<ColorScaleConfigDialog> self(this);
|
||
tplRepo_->listLvlTemplates(projectId_, [self](bool ok, QJsonArray list, QString msg) {
|
||
if (!self) return;
|
||
if (!ok) {
|
||
QMessageBox::warning(self, QStringLiteral("打开"),
|
||
QStringLiteral("获取色阶列表失败:%1").arg(msg));
|
||
return;
|
||
}
|
||
if (list.isEmpty()) {
|
||
QMessageBox::information(self, QStringLiteral("打开"),
|
||
QStringLiteral("暂无可用色阶模板。"));
|
||
return;
|
||
}
|
||
QStringList names;
|
||
for (const auto& it : list)
|
||
names << it.toObject().value(QStringLiteral("templateName")).toString();
|
||
bool picked = false;
|
||
const QString chosen = QInputDialog::getItem(
|
||
self, QStringLiteral("引用色阶"), QStringLiteral("请选择色阶:"), names, 0,
|
||
false, &picked);
|
||
if (!picked) return;
|
||
const int sel = names.indexOf(chosen);
|
||
if (sel < 0) return;
|
||
const QJsonObject props =
|
||
list[sel].toObject().value(QStringLiteral("properties")).toObject();
|
||
const QJsonArray colorBar = props.value(QStringLiteral("colorBar")).toArray();
|
||
std::vector<std::pair<double, geopro::core::Rgba>> bar;
|
||
for (const auto& e : colorBar) {
|
||
const QJsonArray pair = e.toArray();
|
||
if (pair.size() < 2) continue;
|
||
bar.emplace_back(pair[0].toVariant().toDouble(),
|
||
parseCssColor(pair[1].toString()));
|
||
}
|
||
if (bar.size() < 2) {
|
||
QMessageBox::warning(self, QStringLiteral("打开"),
|
||
QStringLiteral("色阶数据无效。"));
|
||
return;
|
||
}
|
||
// 透传方案字段。
|
||
self->lvlSchemeType_ =
|
||
props.value(QStringLiteral("lvlSchemeType")).toString(QStringLiteral("normal"));
|
||
self->logLinesCount_ =
|
||
props.value(QStringLiteral("logLinesCount")).toInt(8);
|
||
self->equalAreaLayerCount_ =
|
||
props.value(QStringLiteral("equalAreaLayerCount")).toInt(10);
|
||
const QJsonObject lc = props.value(QStringLiteral("lineConfig")).toObject();
|
||
if (!lc.isEmpty()) {
|
||
self->lineCfg_.lineShow = lc.value(QStringLiteral("showLines")).toBool(true);
|
||
self->lineCfg_.dashed =
|
||
lc.value(QStringLiteral("lineType")).toString() == QStringLiteral("dashed");
|
||
self->lineCfg_.lineColor =
|
||
parseCssColor(lc.value(QStringLiteral("color")).toString());
|
||
}
|
||
self->loadColorBar(bar);
|
||
});
|
||
}
|
||
|
||
geopro::core::ColorScale ColorScaleConfigDialog::colorScale() const {
|
||
geopro::core::ColorScale cs;
|
||
for (const auto& row : rows_) cs.addStop(row.value, row.color); // 内部按 value 升序
|
||
cs.setGlobalOpacity(globalOpacity_); // 整体透明度独立带出(渲染时与每色 alpha 相乘)
|
||
return cs;
|
||
}
|
||
|
||
} // namespace geopro::app
|