geopro/src/app/ColorGradientDialog.cpp

434 lines
19 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "ColorGradientDialog.hpp"
#include <algorithm>
#include <cmath>
#include <QColorDialog>
#include <QComboBox>
#include "EmptyAwareComboBox.hpp"
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFile>
#include <QFileDialog>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QIcon>
#include <QInputDialog>
#include <QJsonArray>
#include <QJsonObject>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPainter>
#include <QPixmap>
#include <QPointer>
#include <QPushButton>
#include <QRegularExpression>
#include <QSignalBlocker>
#include <QSlider>
#include <QVBoxLayout>
#include "ColorScaleIO.hpp"
#include "FormKit.hpp"
#include "Theme.hpp"
#include "repo/IColorTemplateRepository.hpp"
namespace geopro::app {
namespace {
using Stop = GradientEditWidget::Stop;
using geopro::core::Rgba;
Rgba hx(unsigned r, unsigned g, unsigned b) {
return Rgba{static_cast<unsigned char>(r), static_cast<unsigned char>(g),
static_cast<unsigned char>(b), 255};
}
QColor toQ(const Rgba& c) { return QColor(c.r, c.g, c.b, c.a); }
Rgba fromQ(const QColor& c) {
return 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())};
}
// 解析后端色阶 color 字符串("#RRGGBB" / "rgb(r,g,b)" / "rgba(r,g,b,a)")→ Rgba。
Rgba parseColorString(const QString& s) {
const QString t = s.trimmed();
if (t.startsWith(QLatin1Char('#')) && t.size() >= 7) {
bool ok = false;
const unsigned r = t.mid(1, 2).toUInt(&ok, 16);
const unsigned g = t.mid(3, 2).toUInt(&ok, 16);
const unsigned b = t.mid(5, 2).toUInt(&ok, 16);
return hx(r, g, b);
}
static const QRegularExpression re(
QStringLiteral("rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)"));
const auto m = re.match(t);
if (m.hasMatch())
return hx(m.captured(1).toUInt(), m.captured(2).toUInt(), m.captured(3).toUInt());
return hx(0, 0, 0);
}
// 生成配色方案预览色条(下拉用),复刻 generateColorPreview。
QPixmap previewPixmap(const std::vector<Stop>& stops, int w = 100, int h = 16) {
QPixmap pm(w, h);
pm.fill(Qt::white);
if (stops.size() >= 2) {
QPainter p(&pm);
QLinearGradient grad(0, 0, w, 0);
for (const auto& s : stops) grad.setColorAt(std::clamp(s.pos, 0.0, 1.0), toQ(s.color));
p.fillRect(QRect(0, 0, w, h), grad);
}
return pm;
}
} // namespace
ColorGradientDialog::ColorGradientDialog(const std::vector<Stop>& init, double minValue,
double maxValue, double originMin, double originMax,
std::vector<double> samples, double opacity,
geopro::data::IColorTemplateRepository* tplRepo,
QString projectId, QWidget* parent)
: QDialog(parent),
originMin_(originMin),
originMax_(originMax),
opacity_(opacity),
tplRepo_(tplRepo),
projectId_(std::move(projectId)) {
setWindowTitle(QStringLiteral("色阶编辑器"));
setModal(true);
auto* root = new QVBoxLayout(this);
// ── 顶部两列 gridgrid-template-columns: 50% 50% ──────────────────────
auto* grid = new QGridLayout();
grid->setHorizontalSpacing(geopro::app::space::kLg); // formkit 标准列距
grid->setVerticalSpacing(geopro::app::space::kMd); // formkit 标准行距
int rowIdx = 0;
// 配色方案(下拉带预览色条)。
schemeCombo_ = new EmptyAwareComboBox(this);
schemeCombo_->setIconSize(QSize(100, 16));
{
auto* cell = new QHBoxLayout();
cell->addWidget(formkit::editLabel(QStringLiteral("配色方案:")));
cell->addWidget(schemeCombo_, 1);
grid->addLayout(cell, rowIdx, 0);
}
// 分布方式disabled, 默认线性)+ 反向。
{
auto* cell = new QHBoxLayout();
cell->addWidget(formkit::editLabel(QStringLiteral("分布方式:")));
auto* distCombo = new EmptyAwareComboBox(this);
distCombo->addItem(QStringLiteral("线性"), QStringLiteral("linear"));
distCombo->addItem(QStringLiteral("对数"), QStringLiteral("log"));
distCombo->setCurrentIndex(0);
distCombo->setEnabled(false);
cell->addWidget(distCombo, 1);
auto* reverseBtn = new QPushButton(QStringLiteral("反转"), this);
cell->addWidget(reverseBtn);
connect(reverseBtn, &QPushButton::clicked, this, [this] { gradient_->reverse(); });
grid->addLayout(cell, rowIdx++, 1);
}
// 数值范围只读originMin~originMax各保留 6 位)。
rangeEdit_ = new QLineEdit(this);
rangeEdit_->setReadOnly(true);
rangeEdit_->setText(QStringLiteral("%1~%2")
.arg(QString::number(originMin_, 'f', 6))
.arg(QString::number(originMax_, 'f', 6)));
{
auto* cell = new QHBoxLayout();
cell->addWidget(formkit::editLabel(QStringLiteral("数值范围:")));
cell->addWidget(rangeEdit_, 1);
grid->addLayout(cell, rowIdx, 0);
}
// 最小值/最大值(可编辑)。
{
auto* cell = new QHBoxLayout();
cell->addWidget(formkit::editLabel(QStringLiteral("最小值:")));
minSpin_ = new QDoubleSpinBox(this);
minSpin_->setDecimals(6);
minSpin_->setRange(-1e12, 1e12);
minSpin_->setValue(minValue);
cell->addWidget(minSpin_, 1);
cell->addWidget(formkit::editLabel(QStringLiteral("最大值:")));
maxSpin_ = new QDoubleSpinBox(this);
maxSpin_->setDecimals(6);
maxSpin_->setRange(-1e12, 1e12);
maxSpin_->setValue(maxValue);
cell->addWidget(maxSpin_, 1);
grid->addLayout(cell, rowIdx++, 1);
}
// 当前数据。
curDataLabel_ = new QLabel(QStringLiteral("-"), this);
{
auto* cell = new QHBoxLayout();
cell->addWidget(formkit::editLabel(QStringLiteral("当前数据值:")));
cell->addWidget(curDataLabel_, 1);
grid->addLayout(cell, rowIdx, 0);
}
// 当前位置。
curPosLabel_ = new QLabel(QStringLiteral("-"), this);
{
auto* cell = new QHBoxLayout();
cell->addWidget(formkit::editLabel(QStringLiteral("当前数据位置:")));
cell->addWidget(curPosLabel_, 1);
grid->addLayout(cell, rowIdx++, 1);
}
// 当前颜色(色块按钮,仅选中手柄时可用)。
curColorBtn_ = new QPushButton(this);
curColorBtn_->setFixedSize(48, 22);
curColorBtn_->setEnabled(false);
{
auto* cell = new QHBoxLayout();
cell->addWidget(formkit::editLabel(QStringLiteral("当前颜色:")));
cell->addWidget(curColorBtn_);
cell->addStretch(1);
grid->addLayout(cell, rowIdx++, 0);
}
root->addLayout(grid);
// ── 渐变画布 ───────────────────────────────────────────────────────────
gradient_ = new GradientEditWidget(this);
gradient_->setMinimumHeight(400);
gradient_->setMinMax(minValue, maxValue);
gradient_->setSamples(std::move(samples));
if (init.size() >= 2) gradient_->setStops(init);
root->addWidget(gradient_);
// ── 整体透明度滑块0~1, step 0.01 ───────────────────────────────────
{
auto* opRow = new QHBoxLayout();
opRow->addWidget(new QLabel(QStringLiteral("整体透明度:")));
opacitySlider_ = new QSlider(Qt::Horizontal, this);
opacitySlider_->setRange(0, 100);
opacitySlider_->setValue(static_cast<int>(opacity_ * 100 + 0.5));
opacityLabel_ = new QLabel(QString::number(opacity_, 'f', 2), this);
opRow->addWidget(opacitySlider_, 1);
opRow->addWidget(opacityLabel_);
root->addLayout(opRow);
}
// ── 底部按钮:左 导入/导出/新建色阶;右 取消/应用 ──────────────────────
{
auto* btns = new QDialogButtonBox(this);
auto* importBtn = btns->addButton(QStringLiteral("导入"), QDialogButtonBox::ActionRole);
auto* exportBtn = btns->addButton(QStringLiteral("导出"), QDialogButtonBox::ActionRole);
newSchemeBtn_ = btns->addButton(QStringLiteral("新建色阶"), QDialogButtonBox::ActionRole);
newSchemeBtn_->setEnabled(tplRepo_ != nullptr && !projectId_.isEmpty());
btns->addButton(QStringLiteral("取消"), QDialogButtonBox::RejectRole);
btns->addButton(QStringLiteral("应用"), QDialogButtonBox::AcceptRole);
root->addWidget(btns);
connect(importBtn, &QPushButton::clicked, this, &ColorGradientDialog::importClr);
connect(exportBtn, &QPushButton::clicked, this, &ColorGradientDialog::exportClr);
connect(newSchemeBtn_, &QPushButton::clicked, this, &ColorGradientDialog::newScheme);
connect(btns, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(btns, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
// ── 信号连接 ───────────────────────────────────────────────────────────
connect(schemeCombo_, QOverload<int>::of(&QComboBox::activated), this,
[this](int i) { applyScheme(i); });
connect(gradient_, &GradientEditWidget::handleSelected, this,
&ColorGradientDialog::onHandleSelected);
connect(gradient_, &GradientEditWidget::selectionCleared, this,
&ColorGradientDialog::onSelectionCleared);
connect(curColorBtn_, &QPushButton::clicked, this, &ColorGradientDialog::pickCurrentColor);
connect(minSpin_, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
[this](double) { onMinMaxChanged(); });
connect(maxSpin_, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
[this](double) { onMinMaxChanged(); });
connect(opacitySlider_, &QSlider::valueChanged, this, [this](int v) {
opacity_ = v / 100.0;
opacityLabel_->setText(QString::number(opacity_, 'f', 2));
});
// 配色方案:内置预设打底,再异步拉取后端 .clr 列表(若有 api
buildBuiltinSchemes();
reloadSchemeCombo();
if (tplRepo_ != nullptr && !projectId_.isEmpty()) queryClrSchemes();
}
std::vector<GradientEditWidget::Stop> ColorGradientDialog::stops() const {
return gradient_->stops();
}
// ── 配色方案 ─────────────────────────────────────────────────────────────────
void ColorGradientDialog::buildBuiltinSchemes() {
schemes_.clear();
// 默认 GMT 17 档(与 colorEditor.vue defaultColorScale 一致)。
schemes_.push_back(
{QStringLiteral("默认 (GMT)"),
{{0.0, hx(0x00, 0x00, 0xAA)}, {0.0625, hx(0x00, 0x00, 0xD3)},
{0.125, hx(0x00, 0x00, 0xFF)}, {0.1875, hx(0x00, 0x80, 0xFF)},
{0.25, hx(0x00, 0xFF, 0xFF)}, {0.3125, hx(0x00, 0xC0, 0x80)},
{0.375, hx(0x00, 0xFF, 0x00)}, {0.4375, hx(0x00, 0x80, 0x00)},
{0.5, hx(0x80, 0xC0, 0x00)}, {0.5625, hx(0xFF, 0xFF, 0x00)},
{0.625, hx(0xBF, 0x80, 0x00)}, {0.6875, hx(0xFF, 0x80, 0x00)},
{0.75, hx(0xFF, 0x00, 0x00)}, {0.8125, hx(0xD3, 0x00, 0x00)},
{0.875, hx(0x84, 0x00, 0x40)}, {0.9375, hx(0x60, 0x00, 0x45)},
{1.0, hx(0x30, 0x00, 0x30)}}});
schemes_.push_back({QStringLiteral("彩虹"),
{{0.0, hx(0, 0, 255)}, {0.25, hx(0, 255, 255)}, {0.5, hx(0, 255, 0)},
{0.75, hx(255, 255, 0)}, {1.0, hx(255, 0, 0)}}});
schemes_.push_back(
{QStringLiteral("蓝白红"),
{{0.0, hx(0, 0, 255)}, {0.5, hx(255, 255, 255)}, {1.0, hx(255, 0, 0)}}});
schemes_.push_back({QStringLiteral("灰度"), {{0.0, hx(0, 0, 0)}, {1.0, hx(255, 255, 255)}}});
}
void ColorGradientDialog::reloadSchemeCombo() {
const QSignalBlocker block(schemeCombo_);
schemeCombo_->clear();
for (const auto& s : schemes_)
schemeCombo_->addItem(QIcon(previewPixmap(s.stops)), s.name);
}
void ColorGradientDialog::applyScheme(int index) {
if (index < 0 || index >= static_cast<int>(schemes_.size())) return;
gradient_->setStops(schemes_[index].stops);
onSelectionCleared();
}
// ── 当前手柄读出 ─────────────────────────────────────────────────────────────
void ColorGradientDialog::onHandleSelected(const QString& colorHex, const QString& valueText,
const QString& percentText) {
curDataLabel_->setText(valueText);
curPosLabel_->setText(percentText);
curColor_ = parseColorString(colorHex);
curColorBtn_->setEnabled(true);
curColorBtn_->setStyleSheet(
QStringLiteral("background-color: %1;").arg(toQ(curColor_).name()));
}
void ColorGradientDialog::onSelectionCleared() {
curDataLabel_->setText(QStringLiteral("-"));
curPosLabel_->setText(QStringLiteral("-"));
curColorBtn_->setEnabled(false);
curColorBtn_->setStyleSheet(QString());
}
void ColorGradientDialog::onMinMaxChanged() {
gradient_->setMinMax(minSpin_->value(), maxSpin_->value());
}
void ColorGradientDialog::pickCurrentColor() {
if (!gradient_->hasSelection()) return;
const QColor picked =
QColorDialog::getColor(toQ(curColor_), this, QStringLiteral("当前颜色"));
if (!picked.isValid()) return;
curColor_ = fromQ(picked);
curColor_.a = 255;
gradient_->setSelectedColor(curColor_);
curColorBtn_->setStyleSheet(
QStringLiteral("background-color: %1;").arg(toQ(curColor_).name()));
}
// ── .clr 导入/导出(复刻 importColorLevel / explortColorLevel ───────────────
void ColorGradientDialog::importClr() {
const QString path = QFileDialog::getOpenFileName(this, QStringLiteral("导入 .clr"), {},
QStringLiteral("色阶文件 (*.clr)"));
if (path.isEmpty()) return;
QFile f(path);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
QMessageBox::warning(this, QStringLiteral("导入"), QStringLiteral("无法打开文件。"));
return;
}
const ClrData clr = parseClr(f.readAll().toStdString());
if (clr.stops.size() < 2) {
QMessageBox::warning(this, QStringLiteral("导入"),
QStringLiteral("文件格式不正确或色阶不足。"));
return;
}
std::vector<Stop> st;
for (const auto& [pos, c] : clr.stops) st.push_back({pos, c});
gradient_->setStops(st);
onSelectionCleared();
opacity_ = clr.opacity;
opacitySlider_->setValue(static_cast<int>(opacity_ * 100 + 0.5));
}
void ColorGradientDialog::exportClr() {
const QString path = QFileDialog::getSaveFileName(this, QStringLiteral("导出 .clr"),
QStringLiteral("色阶配置.clr"),
QStringLiteral("色阶文件 (*.clr)"));
if (path.isEmpty()) return;
ClrData clr;
clr.opacity = opacity_;
for (const auto& s : gradient_->stops()) clr.stops.emplace_back(s.pos, s.color);
QFile f(path);
if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, QStringLiteral("导出"), QStringLiteral("无法写入文件。"));
return;
}
const std::string out = generateClr(clr);
if (f.write(out.c_str(), static_cast<qint64>(out.size())) < 0)
QMessageBox::warning(this, QStringLiteral("导出"), QStringLiteral("写入失败。"));
}
// ── 后端接线 ─────────────────────────────────────────────────────────────────
void ColorGradientDialog::newScheme() {
if (tplRepo_ == nullptr || projectId_.isEmpty()) return;
bool ok = false;
const QString name = QInputDialog::getText(this, QStringLiteral("新建色阶"),
QStringLiteral("色阶名称:"), QLineEdit::Normal,
QStringLiteral("默认色阶"), &ok);
if (!ok || name.trimmed().isEmpty()) return;
// 领域装配colorscale 串/位置)留在对话框;仓储只负责传输。
QJsonArray scale;
for (const auto& s : gradient_->stops())
scale.append(QJsonObject{{QStringLiteral("pos"), s.pos},
{QStringLiteral("color"), toQ(s.color).name().toUpper()},
{QStringLiteral("colorId"), QString()}});
const QJsonObject properties{{QStringLiteral("name"), name},
{QStringLiteral("colorscale"), scale}};
QPointer<ColorGradientDialog> self(this);
tplRepo_->newClrScheme(projectId_, properties, [self](bool ok, QString) {
if (!self) return;
if (ok) {
QMessageBox::information(self, QStringLiteral("新建色阶"),
QStringLiteral("保存成功。"));
self->queryClrSchemes(); // 刷新下拉
} else {
QMessageBox::warning(self, QStringLiteral("新建色阶"),
QStringLiteral("保存失败。"));
}
});
}
void ColorGradientDialog::queryClrSchemes() {
if (tplRepo_ == nullptr || projectId_.isEmpty()) return;
QPointer<ColorGradientDialog> self(this);
tplRepo_->listClrSchemes(projectId_, [self](bool ok, QJsonArray arr, QString) {
if (!self || !ok) return;
if (arr.isEmpty()) return;
// 领域解析properties.name/colorscale留在对话框。
self->buildBuiltinSchemes(); // 重置为内置,再追加后端
for (const auto& v : arr) {
const QJsonObject props = v.toObject().value(QStringLiteral("properties")).toObject();
const QString name = props.value(QStringLiteral("name")).toString();
const QJsonArray cs = props.value(QStringLiteral("colorscale")).toArray();
if (name.isEmpty() || cs.size() < 2) continue;
std::vector<Stop> st;
for (const auto& c : cs) {
const QJsonObject o = c.toObject();
st.push_back({o.value(QStringLiteral("pos")).toDouble(),
parseColorString(o.value(QStringLiteral("color")).toString())});
}
if (st.size() >= 2) self->schemes_.push_back({name, std::move(st)});
}
self->reloadSchemeCombo();
});
}
} // namespace geopro::app