feat/dataset-detail-chart #5
|
|
@ -24,6 +24,9 @@ set_target_properties(qwt PROPERTIES AUTOMOC ON)
|
||||||
target_include_directories(qwt PUBLIC "${QWT_SRC_DIR}")
|
target_include_directories(qwt PUBLIC "${QWT_SRC_DIR}")
|
||||||
target_link_libraries(qwt PUBLIC Qt6::Widgets Qt6::Concurrent Qt6::PrintSupport Qt6::Svg)
|
target_link_libraries(qwt PUBLIC Qt6::Widgets Qt6::Concurrent Qt6::PrintSupport Qt6::Svg)
|
||||||
target_compile_features(qwt PUBLIC cxx_std_17)
|
target_compile_features(qwt PUBLIC cxx_std_17)
|
||||||
|
# QWT_MOC_INCLUDE=1:启用 Qwt 源文件末尾 #if QWT_MOC_INCLUDE 保护的 #include "moc_xxx.cpp"。
|
||||||
|
# Qwt 原生用 qmake(qwtbuild.pri 里设置),CMake 构建需显式定义,否则 MOC 元对象代码不被编译进 .obj。
|
||||||
|
target_compile_definitions(qwt PRIVATE QWT_MOC_INCLUDE=1)
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
target_compile_options(qwt PRIVATE /bigobj /EHsc /wd4244 /wd4267 /wd4305 /wd4456)
|
target_compile_options(qwt PRIVATE /bigobj /EHsc /wd4244 /wd4267 /wd4305 /wd4456)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
#include "panels/chart/ColorMapService.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
inline double clamp01(double v) {
|
||||||
|
return v < 0.0 ? 0.0 : (v > 1.0 ? 1.0 : v);
|
||||||
|
}
|
||||||
|
inline unsigned char lerpByte(unsigned char a, unsigned char b, double t) {
|
||||||
|
return static_cast<unsigned char>(a + (b - a) * t + 0.5);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ColorMapService::ColorMapService(const core::ColorScale& scale)
|
||||||
|
: scale_(scale) {
|
||||||
|
auto raw = scale.stops();
|
||||||
|
if (raw.empty()) {
|
||||||
|
minVal_ = 0.0;
|
||||||
|
maxVal_ = 1.0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
minVal_ = raw.front().first;
|
||||||
|
maxVal_ = raw.back().first;
|
||||||
|
double range = maxVal_ - minVal_;
|
||||||
|
normStops_.reserve(raw.size());
|
||||||
|
for (const auto& [val, color] : raw) {
|
||||||
|
double pos = (range > 0.0) ? (val - minVal_) / range : 0.0;
|
||||||
|
normStops_.push_back({clamp01(pos), color});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double ColorMapService::normalized(double v) const {
|
||||||
|
double range = maxVal_ - minVal_;
|
||||||
|
if (range <= 0.0) return 0.0;
|
||||||
|
return clamp01((v - minVal_) / range);
|
||||||
|
}
|
||||||
|
|
||||||
|
core::Rgba ColorMapService::colorAtContinuous(double v) const {
|
||||||
|
if (normStops_.empty()) return core::Rgba{0, 0, 0, 255};
|
||||||
|
if (normStops_.size() == 1) return normStops_.front().color;
|
||||||
|
|
||||||
|
double t = normalized(v);
|
||||||
|
|
||||||
|
// 找到 t 落在哪两个 normStop 之间
|
||||||
|
if (t <= normStops_.front().pos) return normStops_.front().color;
|
||||||
|
if (t >= normStops_.back().pos) return normStops_.back().color;
|
||||||
|
|
||||||
|
// 二分查找第一个 pos > t
|
||||||
|
auto it = std::upper_bound(normStops_.begin(), normStops_.end(), t,
|
||||||
|
[](double val, const NormStop& s) { return val < s.pos; });
|
||||||
|
|
||||||
|
const NormStop& hi = *it;
|
||||||
|
const NormStop& lo = *(it - 1);
|
||||||
|
double segLen = hi.pos - lo.pos;
|
||||||
|
double frac = (segLen > 0.0) ? (t - lo.pos) / segLen : 0.0;
|
||||||
|
|
||||||
|
return core::Rgba{
|
||||||
|
lerpByte(lo.color.r, hi.color.r, frac),
|
||||||
|
lerpByte(lo.color.g, hi.color.g, frac),
|
||||||
|
lerpByte(lo.color.b, hi.color.b, frac),
|
||||||
|
lerpByte(lo.color.a, hi.color.a, frac)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
core::Rgba ColorMapService::colorAtDiscrete(double v) const {
|
||||||
|
return scale_.colorAt(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
#pragma once
|
||||||
|
#include "model/ColorScale.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 连续/离散双模式色阶服务,供散点渲染(连续插值)和网格/图例(阶梯)使用。
|
||||||
|
// 连续模式:按归一化位置在相邻断点之间线性插值 RGB,与原版 Plotly colorscale 一致。
|
||||||
|
class ColorMapService {
|
||||||
|
public:
|
||||||
|
explicit ColorMapService(const core::ColorScale& scale);
|
||||||
|
|
||||||
|
// 将数据值归一化到 [0,1](min=首断点值, max=末断点值),超范围 clamp。
|
||||||
|
double normalized(double v) const;
|
||||||
|
|
||||||
|
// 连续插值取色(散点用):按断点位置线性插值 RGB。
|
||||||
|
core::Rgba colorAtContinuous(double v) const;
|
||||||
|
|
||||||
|
// 离散阶梯取色(网格/图例用):复用 ColorScale::colorAt。
|
||||||
|
core::Rgba colorAtDiscrete(double v) const;
|
||||||
|
|
||||||
|
// 返回原始 ColorScale 引用(色阶条图例用)。
|
||||||
|
const core::ColorScale& scale() const { return scale_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::ColorScale scale_;
|
||||||
|
// 归一化位置与颜色预计算缓存(stops 已升序)。
|
||||||
|
struct NormStop {
|
||||||
|
double pos; // normalized position in [0,1]
|
||||||
|
core::Rgba color;
|
||||||
|
};
|
||||||
|
std::vector<NormStop> normStops_;
|
||||||
|
double minVal_;
|
||||||
|
double maxVal_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -40,6 +40,13 @@ std::vector<double> ColorScale::stopValues() const {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::pair<double, Rgba>> ColorScale::stops() const {
|
||||||
|
std::vector<std::pair<double, Rgba>> result;
|
||||||
|
result.reserve(stops_.size());
|
||||||
|
for (const auto& s : stops_) result.emplace_back(s.value, s.color);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
Rgba ColorScale::colorAt(double value) const {
|
Rgba ColorScale::colorAt(double value) const {
|
||||||
if (std::isnan(value)) return nan_.value_or(Rgba{0, 0, 0, 0});
|
if (std::isnan(value)) return nan_.value_or(Rgba{0, 0, 0, 0});
|
||||||
if (stops_.empty()) return Rgba{0, 0, 0, 0};
|
if (stops_.empty()) return Rgba{0, 0, 0, 0};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
namespace geopro::core {
|
namespace geopro::core {
|
||||||
|
|
@ -15,6 +16,8 @@ public:
|
||||||
void addStop(double value, Rgba color); // 内部保持按 value 升序
|
void addStop(double value, Rgba color); // 内部保持按 value 升序
|
||||||
Rgba colorAt(double value) const; // 含 under/over/NaN 处理
|
Rgba colorAt(double value) const; // 含 under/over/NaN 处理
|
||||||
std::vector<double> stopValues() const; // 升序分段值(供等值线用真实非均匀级)
|
std::vector<double> stopValues() const; // 升序分段值(供等值线用真实非均匀级)
|
||||||
|
// 返回升序断点 (value, color) 列表,供连续插值用。
|
||||||
|
std::vector<std::pair<double, Rgba>> stops() const;
|
||||||
std::size_t stopCount() const { return stops_.size(); }
|
std::size_t stopCount() const { return stops_.size(); }
|
||||||
void setUnder(Rgba c) { under_ = c; }
|
void setUnder(Rgba c) { under_ = c; }
|
||||||
void setOver(Rgba c) { over_ = c; }
|
void setOver(Rgba c) { over_ = c; }
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,11 @@ endif()
|
||||||
|
|
||||||
target_include_directories(geopro_tests PRIVATE ${CMAKE_SOURCE_DIR}/src/app)
|
target_include_directories(geopro_tests PRIVATE ${CMAKE_SOURCE_DIR}/src/app)
|
||||||
target_sources(geopro_tests PRIVATE app/test_chart_strategy_registry.cpp)
|
target_sources(geopro_tests PRIVATE app/test_chart_strategy_registry.cpp)
|
||||||
|
# ColorMapService 测试(geopro_desktop 是可执行文件,直接把源加入测试目标)
|
||||||
|
target_sources(geopro_tests PRIVATE
|
||||||
|
app/test_colormap_service.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/app/panels/chart/ColorMapService.cpp
|
||||||
|
)
|
||||||
|
|
||||||
# controller 层:DatasetDetailController 编排集成测试(QSignalSpy 验证 chartReady/loadFailed)。
|
# controller 层:DatasetDetailController 编排集成测试(QSignalSpy 验证 chartReady/loadFailed)。
|
||||||
find_package(Qt6 COMPONENTS Test REQUIRED)
|
find_package(Qt6 COMPONENTS Test REQUIRED)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include "panels/chart/ColorMapService.hpp"
|
||||||
|
using namespace geopro;
|
||||||
|
|
||||||
|
TEST(ColorMapService, ContinuousInterpolatesBetweenStops) {
|
||||||
|
core::ColorScale cs;
|
||||||
|
cs.addStop(0.0, core::Rgba{0, 0, 0, 255});
|
||||||
|
cs.addStop(100.0, core::Rgba{255, 255, 255, 255});
|
||||||
|
app::ColorMapService svc(cs);
|
||||||
|
EXPECT_NEAR(svc.normalized(50.0), 0.5, 1e-9);
|
||||||
|
auto c = svc.colorAtContinuous(50.0); // 黑白中点应约为灰
|
||||||
|
EXPECT_GT(c.r, 100); EXPECT_LT(c.r, 160);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ColorMapService, NormalizedClampsOutOfRange) {
|
||||||
|
core::ColorScale cs;
|
||||||
|
cs.addStop(0.0, core::Rgba{0, 0, 0, 255});
|
||||||
|
cs.addStop(100.0, core::Rgba{255, 255, 255, 255});
|
||||||
|
app::ColorMapService svc(cs);
|
||||||
|
EXPECT_NEAR(svc.normalized(-10.0), 0.0, 1e-9);
|
||||||
|
EXPECT_NEAR(svc.normalized(110.0), 1.0, 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ColorMapService, ColorAtContinuousAtExtremes) {
|
||||||
|
core::ColorScale cs;
|
||||||
|
cs.addStop(0.0, core::Rgba{10, 20, 30, 255});
|
||||||
|
cs.addStop(100.0, core::Rgba{200, 210, 220, 255});
|
||||||
|
app::ColorMapService svc(cs);
|
||||||
|
auto cMin = svc.colorAtContinuous(0.0);
|
||||||
|
EXPECT_EQ(cMin.r, 10);
|
||||||
|
EXPECT_EQ(cMin.g, 20);
|
||||||
|
EXPECT_EQ(cMin.b, 30);
|
||||||
|
auto cMax = svc.colorAtContinuous(100.0);
|
||||||
|
EXPECT_EQ(cMax.r, 200);
|
||||||
|
EXPECT_EQ(cMax.g, 210);
|
||||||
|
EXPECT_EQ(cMax.b, 220);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ColorMapService, ScaleRefReturnsOriginal) {
|
||||||
|
core::ColorScale cs;
|
||||||
|
cs.addStop(0.0, core::Rgba{0, 0, 0, 255});
|
||||||
|
cs.addStop(50.0, core::Rgba{128, 0, 0, 255});
|
||||||
|
cs.addStop(100.0, core::Rgba{255, 0, 0, 255});
|
||||||
|
app::ColorMapService svc(cs);
|
||||||
|
EXPECT_EQ(svc.scale().stopCount(), 3u);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue