#include "ColorScaleConfigDialog.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(c.red()), static_cast(c.green()), static_cast(c.blue()), static_cast(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(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(r), static_cast(g), static_cast(b), static_cast(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 samples, const ContourLineConfig& lineInit, geopro::data::IColorTemplateRepository* tplRepo, QString projectId, QWidget* parent) : QDialog(parent), vmin_(vmin), vmax_(vmax), samples_(std::move(samples)), lineCfg_(lineInit), tplRepo_(tplRepo), projectId_(std::move(projectId)) { setWindowTitle(QStringLiteral("色阶配置")); setModal(true); resize(560, 420); // 用初始色阶的升序断点填模型;空色阶兜底成 vmin/vmax 两端蓝红。 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(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(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(rows_.size())) return -1; return r; // 升序显示,行号即模型下标 } void ColorScaleConfigDialog::onCellDoubleClicked(int row, int col) { if (row < 0 || row >= static_cast(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(rows_.size()); ++i) { const Row& ri = rows_[static_cast(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(idx)]; double newLevel = sel.value; if (idx > 0) // 升序:上一行(idx-1)为更低值,取两者中点 newLevel = (rows_[static_cast(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(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(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 levels = generateContourLevels(p, samples_); std::vector 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 seed; for (const auto& r : rows_) seed.push_back({(r.value - lo) / span, r.color}); ColorGradientDialog dlg(seed, lo, hi, vmin_, vmax_, samples_, 1.0, tplRepo_, projectId_, this); if (dlg.exec() != QDialog::Accepted) return; const auto grad = dlg.stops(); if (grad.size() < 2) return; const double opacity = dlg.opacity(); const unsigned char alpha = static_cast(opacity * 255.0 + 0.5); // 在新渐变上按各层级位置连续采样回填颜色(复刻 mapColors + addAlphaToColor 整体透明度)。 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); }; for (auto& r : rows_) { geopro::core::Rgba c = sampleGrad((r.value - lo) / span); if (opacity < 1.0) c.a = alpha; // 整体透明度覆盖 alpha r.color = c; } 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 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 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(text.size())) < 0) QMessageBox::warning(this, QStringLiteral("导出"), QStringLiteral("写入失败。")); } void ColorScaleConfigDialog::loadColorBar( const std::vector>& 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; bool ok = false; const QString name = QInputDialog::getText(this, QStringLiteral("另存模板配置"), QStringLiteral("模板名称:"), QLineEdit::Normal, QStringLiteral("等值线配置.lvl"), &ok); if (!ok || name.trimmed().isEmpty()) return; // 组装 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(模态对话框可能已关)。 QPointer self(this); tplRepo_->saveLvlTemplate(projectId_, name.trimmed(), properties, [self](bool ok, QString msg) { if (!self) return; if (ok) QMessageBox::information(self, QStringLiteral("另存"), QStringLiteral("另存成功。")); else QMessageBox::warning( self, QStringLiteral("另存"), QStringLiteral("另存失败:%1").arg(msg)); }); } void ColorScaleConfigDialog::onOpen() { if (tplRepo_ == nullptr || projectId_.isEmpty()) return; QPointer 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> 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 升序 return cs; } } // namespace geopro::app