# M1 Phase 1:core 纯逻辑层 实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 实现 `src/core/` 纯业务层(坐标系、领域模型、阶梯色阶、IDW 插值),零 Qt/零 VTK 依赖,全部 gtest 覆盖,以 `tools/validate_samples.py` 的真实样本结论为地面真值。 **Architecture:** 每个组件是一个 `core` 子库,只依赖标准库(+ Eigen / PROJ)。`core` 编译为静态库 `geopro_core`,被 tests 链接。本层产出后,Phase 2(data)解析器把样本填进这些模型,Phase 4(render)把 `ScalarVolume`/`ColorScale` 转 VTK。 **Tech Stack:** C++17 / Eigen / PROJ / GoogleTest。**不含 Qt、不含 VTK。** **前置:** Phase 0 三 spike 通过(工具链已验证)。本层逻辑不依赖 VTK/ADS,但编译依赖 vcpkg 依赖已构建完成。 **关键事实(来自数据核验,务必遵守):** - GIS 为 EPSG:32649(UTM 49N):`projectX`≈516863=**Easting**,`projectY`≈2494259=**Northing**。样本里 `eastCoord/northCoord` 字段名与值**颠倒**,严禁信任字段名。 - 网格 `v`/`z` 为 `[j=y][i=x]`,即外层 22(y)、内层 100(x)。 - 网格规则:dx≈0.7094、dy≈0.7042 恒定。 - colorBar:`[值, 颜色]` 阶梯,取下界;颜色 `#RRGGBB` 或 `rgba(r,g,b,a)`,**alpha 量纲按来源**(网格色阶 0–255、LVL 色阶 0–1)。 --- ## Task 1:core 静态库骨架 + LocalFrame(坐标轴/原点/Z 基准) **Files:** - Create: `src/core/CMakeLists.txt` - Create: `src/core/geo/LocalFrame.hpp` - Create: `src/core/geo/LocalFrame.cpp` - Create: `tests/core/test_local_frame.cpp` - Modify: `src/CMakeLists.txt`(启用 `add_subdirectory(core)`) - Modify: `tests/CMakeLists.txt`(加 core 测试) - [ ] **Step 1: 建 core 库的 CMake** Create `src/core/CMakeLists.txt`: ```cmake find_package(Eigen3 CONFIG REQUIRED) add_library(geopro_core STATIC geo/LocalFrame.cpp ) target_include_directories(geopro_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_core PUBLIC Eigen3::Eigen) target_compile_features(geopro_core PUBLIC cxx_std_17) # 铁律:core 不链接 Qt/VTK ``` 在 `src/CMakeLists.txt` 取消注释/新增 `add_subdirectory(core)`(置于 `add_subdirectory(app)` 之前)。 - [ ] **Step 2: 写失败测试** Create `tests/core/test_local_frame.cpp`: ```cpp #include #include "geo/LocalFrame.hpp" using geopro::core::LocalFrame; TEST(LocalFrame, GisToWorldSubtractsOriginEastNorth) { // 原点取某 UTM 点;projectX=Easting→world.x, projectY=Northing→world.y LocalFrame f(/*originEast=*/516000.0, /*originNorth=*/2494000.0, /*zDatum=*/0.0); auto w = f.gisToWorld(516863.6350992983, 2494259.56246985, 21.0); EXPECT_NEAR(w.x, 863.6350992983, 1e-6); // East 偏移 EXPECT_NEAR(w.y, 259.56246985, 1e-6); // North 偏移 EXPECT_NEAR(w.z, 21.0, 1e-6); // Z 相对基准 } TEST(LocalFrame, RoundTripWorldGis) { LocalFrame f(516000.0, 2494000.0, 5.0); auto w = f.gisToWorld(516500.0, 2494500.0, 30.0); auto g = f.worldToGis(w); EXPECT_NEAR(g.east, 516500.0, 1e-6); EXPECT_NEAR(g.north, 2494500.0, 1e-6); EXPECT_NEAR(g.elevation, 30.0, 1e-6); } ``` - [ ] **Step 3: 运行测试,确认失败** Run: `cmake --build build/debug --target geopro_tests && ctest --test-dir build/debug -R LocalFrame --output-on-failure` Expected: 编译失败(LocalFrame.hpp 不存在)。 - [ ] **Step 4: 实现 LocalFrame** Create `src/core/geo/LocalFrame.hpp`: ```cpp #pragma once namespace geopro::core { struct WorldPoint { double x, y, z; }; // 局部米:x=East偏移, y=North偏移, z=相对Z基准 struct GisPoint { double east, north, elevation; }; // EPSG 投影米 + 高程 // 唯一权威「项目世界系」:以双精度 GIS 原点平移到局部米,规避 VTK float 大坐标抖动。 // 轴向钉死:world.x = Easting(projectX),world.y = Northing(projectY),world.z 向上为正。 class LocalFrame { public: LocalFrame(double originEast, double originNorth, double zDatum); WorldPoint gisToWorld(double east, double north, double elevation) const; GisPoint worldToGis(const WorldPoint& w) const; double originEast() const { return originEast_; } double originNorth() const { return originNorth_; } double zDatum() const { return zDatum_; } private: double originEast_, originNorth_, zDatum_; }; } // namespace geopro::core ``` Create `src/core/geo/LocalFrame.cpp`: ```cpp #include "geo/LocalFrame.hpp" namespace geopro::core { LocalFrame::LocalFrame(double originEast, double originNorth, double zDatum) : originEast_(originEast), originNorth_(originNorth), zDatum_(zDatum) {} WorldPoint LocalFrame::gisToWorld(double east, double north, double elevation) const { return WorldPoint{east - originEast_, north - originNorth_, elevation - zDatum_}; } GisPoint LocalFrame::worldToGis(const WorldPoint& w) const { return GisPoint{w.x + originEast_, w.y + originNorth_, w.z + zDatum_}; } } // namespace geopro::core ``` 在 `tests/CMakeLists.txt`(spike 之前)把 core 测试加入 `geopro_tests`: ```cmake target_sources(geopro_tests PRIVATE core/test_local_frame.cpp) target_link_libraries(geopro_tests PRIVATE geopro_core) ``` - [ ] **Step 5: 运行测试,确认通过** Run: `cmake --build build/debug --target geopro_tests && ctest --test-dir build/debug -R LocalFrame --output-on-failure` Expected: 2 个用例 PASS。 - [ ] **Step 6: 提交** ```powershell git add src/core/ tests/core/test_local_frame.cpp src/CMakeLists.txt tests/CMakeLists.txt git commit -m "feat(core): LocalFrame 坐标系(原点偏移+东北轴向+Z基准)" ``` --- ## Task 2:core 领域模型(ScatterField / Grid / ScalarVolume / Anomaly) **Files:** - Create: `src/core/model/Field.hpp`(纯数据结构) - Create: `tests/core/test_model.cpp` - Modify: `src/core/CMakeLists.txt`(头文件 only,无需新增 cpp;模型先做 header-only struct) - [ ] **Step 1: 写失败测试** Create `tests/core/test_model.cpp`: ```cpp #include #include "model/Field.hpp" using namespace geopro::core; TEST(Model, ScalarVolumeIndexing) { ScalarVolume v(/*nx=*/2, /*ny=*/3, /*nz=*/4); v.at(1, 2, 3) = 42.0; EXPECT_EQ(v.nx(), 2); EXPECT_EQ(v.ny(), 3); EXPECT_EQ(v.nz(), 4); EXPECT_EQ(v.data().size(), 2u * 3u * 4u); EXPECT_DOUBLE_EQ(v.at(1, 2, 3), 42.0); } TEST(Model, GridStoresRegularSpec) { Grid g(/*nx=*/100, /*ny=*/22); EXPECT_EQ(g.values().size(), 100u * 22u); g.valueAt(/*i=*/3, /*j=*/5) = 7.5; // i=x(0..99), j=y(0..21) EXPECT_DOUBLE_EQ(g.valueAt(3, 5), 7.5); } ``` - [ ] **Step 2: 运行,确认失败** Run: `cmake --build build/debug --target geopro_tests` Expected: 编译失败(Field.hpp 不存在)。 - [ ] **Step 3: 实现模型** Create `src/core/model/Field.hpp`: ```cpp #pragma once #include #include namespace geopro::core { // 规则三维标量场(IInterpolator 输出;render 层转 vtkImageData)。 class ScalarVolume { public: ScalarVolume(int nx, int ny, int nz) : nx_(nx), ny_(ny), nz_(nz), data_(static_cast(nx) * ny * nz, 0.0) {} int nx() const { return nx_; } int ny() const { return ny_; } int nz() const { return nz_; } // 点序 i 最快、j 次之、k 最慢(匹配 vtkImageData) double& at(int i, int j, int k) { return data_[idx(i, j, k)]; } double at(int i, int j, int k) const { return data_[idx(i, j, k)]; } const std::vector& data() const { return data_; } std::vector& data() { return data_; } private: size_t idx(int i, int j, int k) const { return (static_cast(k) * ny_ + j) * nx_ + i; } int nx_, ny_, nz_; std::vector data_; }; // 规则二维网格剖面(x=距离 i, y=深度 j)。values 存 [j*nx + i],i 最快。 class Grid { public: Grid(int nx, int ny) : nx_(nx), ny_(ny), values_(static_cast(nx) * ny, 0.0) {} int nx() const { return nx_; } int ny() const { return ny_; } double& valueAt(int i, int j) { return values_[static_cast(j) * nx_ + i]; } double valueAt(int i, int j) const { return values_[static_cast(j) * nx_ + i]; } const std::vector& values() const { return values_; } std::vector& values() { return values_; } std::vector x, y; // 轴坐标(规则) private: int nx_, ny_; std::vector values_; }; // 散点场(剖面原数据):局部坐标 + 值。 struct ScatterField { std::vector x, y, z, v; // 与样本 xlist/ylist/(hlist)/vlist 对应 }; } // namespace geopro::core ``` (Task 2 不新增 cpp,`Field.hpp` 是 header-only;无需改 `src/core/CMakeLists.txt`,但确保 `geopro_core` 的 include 目录已 PUBLIC 暴露,Task 1 已做。) - [ ] **Step 4: 运行,确认通过** Run: `cmake --build build/debug --target geopro_tests && ctest --test-dir build/debug -R Model --output-on-failure` Expected: 2 用例 PASS。 - [ ] **Step 5: 提交** ```powershell git add src/core/model/Field.hpp tests/core/test_model.cpp tests/CMakeLists.txt git commit -m "feat(core): 领域模型 ScalarVolume/Grid/ScatterField(点序 i 最快)" ``` --- ## Task 3:core 阶梯色阶(value→RGBA + 颜色解析 + alpha 来源) **Files:** - Create: `src/core/model/ColorScale.hpp` - Create: `src/core/model/ColorScale.cpp` - Create: `tests/core/test_color_scale.cpp` - Modify: `src/core/CMakeLists.txt`(加 ColorScale.cpp) - [ ] **Step 1: 写失败测试** Create `tests/core/test_color_scale.cpp`: ```cpp #include #include "model/ColorScale.hpp" using namespace geopro::core; TEST(ColorScale, SteppedLookupTakesLowerStop) { // stops: [0]=蓝, [10]=绿, [20]=红 ColorScale cs; cs.addStop(0.0, Rgba{0,0,255,255}); cs.addStop(10.0, Rgba{0,255,0,255}); cs.addStop(20.0, Rgba{255,0,0,255}); EXPECT_EQ(cs.colorAt(5.0).g, 0); // [0,10) → 蓝 EXPECT_EQ(cs.colorAt(5.0).b, 255); EXPECT_EQ(cs.colorAt(15.0).r, 0); // [10,20) → 绿 EXPECT_EQ(cs.colorAt(15.0).g, 255); } TEST(ColorScale, UnderOverHandling) { ColorScale cs; cs.addStop(0.0, Rgba{0,0,0,255}); cs.addStop(10.0, Rgba{255,255,255,255}); cs.setUnder(Rgba{1,2,3,255}); cs.setOver(Rgba{4,5,6,255}); EXPECT_EQ(cs.colorAt(-1.0).r, 1); // under EXPECT_EQ(cs.colorAt(99.0).r, 4); // over } TEST(ColorScale, ParseHexAndRgba) { EXPECT_EQ(parseColor("#0000B3").b, 0xB3); auto c255 = parseColor("rgba(0,0,170,255)", AlphaScale::Bit255); EXPECT_EQ(c255.b, 170); EXPECT_EQ(c255.a, 255); auto c1 = parseColor("rgba(0,0,170,1)", AlphaScale::Unit); EXPECT_EQ(c1.b, 170); EXPECT_EQ(c1.a, 255); // 0–1 的 1.0 → 255 } ``` - [ ] **Step 2: 运行,确认失败** Run: `cmake --build build/debug --target geopro_tests` Expected: 编译失败(ColorScale.hpp 不存在)。 - [ ] **Step 3: 实现** Create `src/core/model/ColorScale.hpp`: ```cpp #pragma once #include #include #include namespace geopro::core { struct Rgba { unsigned char r, g, b, a; }; enum class AlphaScale { Unit, Bit255 }; // rgba alpha 量纲:0–1 或 0–255 Rgba parseColor(const std::string& s, AlphaScale alpha = AlphaScale::Bit255); // 阶梯色阶:值落 [stop_i, stop_{i+1}) 取 stop_i 的颜色(与平台一致)。 class ColorScale { public: void addStop(double value, Rgba color); // 内部保持按 value 升序 Rgba colorAt(double value) const; // 含 under/over/NaN 处理 void setUnder(Rgba c) { under_ = c; } void setOver(Rgba c) { over_ = c; } void setNan(Rgba c) { nan_ = c; } bool empty() const { return stops_.empty(); } private: struct Stop { double value; Rgba color; }; std::vector stops_; std::optional under_, over_, nan_; }; } // namespace geopro::core ``` Create `src/core/model/ColorScale.cpp`: ```cpp #include "model/ColorScale.hpp" #include #include #include namespace geopro::core { static unsigned char clampByte(double v) { if (v < 0) v = 0; if (v > 255) v = 255; return static_cast(v + 0.5); } Rgba parseColor(const std::string& s, AlphaScale alpha) { if (!s.empty() && s[0] == '#') { auto hex = [&](int i) { return static_cast(std::stoi(s.substr(i, 2), nullptr, 16)); }; return Rgba{hex(1), hex(3), hex(5), 255}; } // rgba(r,g,b,a) / rgb(r,g,b) double r = 0, g = 0, b = 0, a = 1; auto p = s.find('('); if (p != std::string::npos) std::sscanf(s.c_str() + p, "(%lf,%lf,%lf,%lf", &r, &g, &b, &a); unsigned char av = (alpha == AlphaScale::Unit) ? clampByte(a * 255.0) : clampByte(a); return Rgba{clampByte(r), clampByte(g), clampByte(b), av}; } void ColorScale::addStop(double value, Rgba color) { stops_.push_back({value, color}); std::sort(stops_.begin(), stops_.end(), [](const Stop& x, const Stop& y) { return x.value < y.value; }); } Rgba ColorScale::colorAt(double value) const { if (std::isnan(value)) return nan_.value_or(Rgba{0, 0, 0, 0}); if (stops_.empty()) return Rgba{0, 0, 0, 0}; if (value < stops_.front().value) return under_.value_or(stops_.front().color); if (value >= stops_.back().value) return over_.value_or(stops_.back().color); // 取下界:最后一个 value<=查询值 的 stop auto it = std::upper_bound(stops_.begin(), stops_.end(), value, [](double v, const Stop& s) { return v < s.value; }); return (it == stops_.begin() ? stops_.front() : *(it - 1)).color; } } // namespace geopro::core ``` 在 `src/core/CMakeLists.txt` 的 `add_library(geopro_core ...)` 源文件列表加 `model/ColorScale.cpp`。 - [ ] **Step 4: 运行,确认通过** Run: `cmake --build build/debug --target geopro_tests && ctest --test-dir build/debug -R ColorScale --output-on-failure` Expected: 3 用例 PASS。 - [ ] **Step 5: 提交** ```powershell git add src/core/model/ColorScale.* tests/core/test_color_scale.cpp src/core/CMakeLists.txt git commit -m "feat(core): 阶梯色阶 colorAt+颜色解析(alpha 量纲按来源)" ``` --- ## Task 4:core IDW 插值(IInterpolator → ScalarVolume,含包络裁剪) **Files:** - Create: `src/core/algo/IInterpolator.hpp` - Create: `src/core/algo/IdwInterpolator.hpp` - Create: `src/core/algo/IdwInterpolator.cpp` - Create: `tests/core/test_idw.cpp` - Modify: `src/core/CMakeLists.txt`(加 IdwInterpolator.cpp) - [ ] **Step 1: 写失败测试** Create `tests/core/test_idw.cpp`: ```cpp #include #include "algo/IdwInterpolator.hpp" using namespace geopro::core; TEST(Idw, ReproducesSampleAtNode) { // 两个已知点,网格节点正好落在某点上 → 该节点值≈该点值 PointSet pts; pts.x = {0.0, 10.0}; pts.y = {0.0, 0.0}; pts.z = {0.0, 0.0}; pts.v = {100.0, 200.0}; GridSpec spec{ /*nx*/3, /*ny*/1, /*nz*/1, /*ox*/0, /*oy*/0, /*oz*/0, /*dx*/5, /*dy*/1, /*dz*/1, /*power*/2.0, /*maxDist*/1e9 }; IdwInterpolator idw; ScalarVolume vol = idw.interpolate(pts, spec); EXPECT_NEAR(vol.at(0, 0, 0), 100.0, 1e-6); // (x=0) 命中点1 EXPECT_NEAR(vol.at(2, 0, 0), 200.0, 1e-6); // (x=10) 命中点2 EXPECT_GT(vol.at(1, 0, 0), 100.0); // 中点在两者之间 EXPECT_LT(vol.at(1, 0, 0), 200.0); } TEST(Idw, BlanksOutsideMaxDist) { PointSet pts; pts.x = {0.0}; pts.y = {0.0}; pts.z = {0.0}; pts.v = {50.0}; GridSpec spec{2,1,1, 0,0,0, 100,1,1, 2.0, /*maxDist*/1.0}; // 远点超出 maxDist IdwInterpolator idw; ScalarVolume vol = idw.interpolate(pts, spec); EXPECT_NEAR(vol.at(0,0,0), 50.0, 1e-6); // 命中 EXPECT_TRUE(std::isnan(vol.at(1,0,0))); // 超距 → NaN(blank) } ``` - [ ] **Step 2: 运行,确认失败** Run: `cmake --build build/debug --target geopro_tests` Expected: 编译失败(头文件不存在)。 - [ ] **Step 3: 实现** Create `src/core/algo/IInterpolator.hpp`: ```cpp #pragma once #include #include "model/Field.hpp" namespace geopro::core { struct PointSet { std::vector x, y, z, v; }; struct GridSpec { int nx, ny, nz; double ox, oy, oz; // 原点 double dx, dy, dz; // 步长 double power; // IDW 幂 double maxDist; // 超过则 blank(NaN),约束插值域(设计 §10) }; class IInterpolator { public: virtual ~IInterpolator() = default; virtual ScalarVolume interpolate(const PointSet& pts, const GridSpec& spec) const = 0; }; } // namespace geopro::core ``` Create `src/core/algo/IdwInterpolator.hpp`: ```cpp #pragma once #include "algo/IInterpolator.hpp" namespace geopro::core { class IdwInterpolator : public IInterpolator { public: ScalarVolume interpolate(const PointSet& pts, const GridSpec& spec) const override; }; } // namespace geopro::core ``` Create `src/core/algo/IdwInterpolator.cpp`: ```cpp #include "algo/IdwInterpolator.hpp" #include #include namespace geopro::core { ScalarVolume IdwInterpolator::interpolate(const PointSet& pts, const GridSpec& s) const { ScalarVolume vol(s.nx, s.ny, s.nz); const double nan = std::numeric_limits::quiet_NaN(); const size_t n = pts.v.size(); for (int k = 0; k < s.nz; ++k) for (int j = 0; j < s.ny; ++j) for (int i = 0; i < s.nx; ++i) { const double gx = s.ox + i * s.dx, gy = s.oy + j * s.dy, gz = s.oz + k * s.dz; double wsum = 0.0, vsum = 0.0, nearest = std::numeric_limits::max(); bool hit = false; double hitVal = 0.0; for (size_t p = 0; p < n; ++p) { const double ddx = gx - pts.x[p], ddy = gy - pts.y[p], ddz = gz - pts.z[p]; const double d2 = ddx * ddx + ddy * ddy + ddz * ddz; const double d = std::sqrt(d2); if (d < nearest) nearest = d; if (d < 1e-12) { hit = true; hitVal = pts.v[p]; break; } const double w = 1.0 / std::pow(d, s.power); wsum += w; vsum += w * pts.v[p]; } if (hit) vol.at(i, j, k) = hitVal; else if (nearest > s.maxDist || wsum == 0.0) vol.at(i, j, k) = nan; // 包络外 blank else vol.at(i, j, k) = vsum / wsum; } return vol; } } // namespace geopro::core ``` 在 `src/core/CMakeLists.txt` 源列表加 `algo/IdwInterpolator.cpp`。 - [ ] **Step 4: 运行,确认通过** Run: `cmake --build build/debug --target geopro_tests && ctest --test-dir build/debug -R Idw --output-on-failure` Expected: 2 用例 PASS。 - [ ] **Step 5: 提交** ```powershell git add src/core/algo/ tests/core/test_idw.cpp src/core/CMakeLists.txt git commit -m "feat(core): IDW 插值器(IInterpolator→ScalarVolume, 含 maxDist 包络裁剪)" ``` --- ## Task 5:core CrsTransform(PROJ 封装,多 CRS 互转) **Files:** - Create: `src/core/geo/CrsTransform.hpp` - Create: `src/core/geo/CrsTransform.cpp` - Create: `tests/core/test_crs_transform.cpp` - Modify: `src/core/CMakeLists.txt`(find_package PROJ + 链接 + 加 cpp) - [ ] **Step 1: 写失败测试** Create `tests/core/test_crs_transform.cpp`: ```cpp #include #include "geo/CrsTransform.hpp" using namespace geopro::core; TEST(CrsTransform, Utm49nToWgs84RoundTrip) { // EPSG:32649 → EPSG:4326,取样本附近一点(lon~114.16, lat~22.55) CrsTransform t("EPSG:32649", "EPSG:4326"); auto ll = t.forward(516868.0, 2494259.0); // (east,north) → (lon,lat) EXPECT_NEAR(ll.x, 114.16, 0.05); // lon EXPECT_NEAR(ll.y, 22.55, 0.05); // lat auto en = t.inverse(ll.x, ll.y); EXPECT_NEAR(en.x, 516868.0, 1.0); EXPECT_NEAR(en.y, 2494259.0, 1.0); } TEST(CrsTransform, WebMercatorToUtm) { // 影像 EPSG:3857 → EPSG:32649(设计 §5:影像与剖面异源 CRS) CrsTransform t("EPSG:3857", "EPSG:32649"); auto p = t.forward(12708343.88, 2577685.90); // tfw 原点 EXPECT_GT(p.x, 400000.0); EXPECT_LT(p.x, 600000.0); // 落在 UTM 49N 合理 Easting 区间 } ``` - [ ] **Step 2: 运行,确认失败** Run: `cmake --build build/debug --target geopro_tests` Expected: 编译失败(CrsTransform.hpp 不存在)。 - [ ] **Step 3: 实现** Create `src/core/geo/CrsTransform.hpp`: ```cpp #pragma once #include #include namespace geopro::core { struct Xy { double x, y; }; // PROJ 封装:源 CRS ↔ 目标 CRS。用于世界系↔GIS↔经纬度、影像异源 CRS 重投影(设计 §5)。 class CrsTransform { public: CrsTransform(const std::string& srcCrs, const std::string& dstCrs); ~CrsTransform(); Xy forward(double x, double y) const; // src → dst Xy inverse(double x, double y) const; // dst → src private: struct Impl; std::unique_ptr impl_; }; } // namespace geopro::core ``` Create `src/core/geo/CrsTransform.cpp`: ```cpp #include "geo/CrsTransform.hpp" #include #include namespace geopro::core { struct CrsTransform::Impl { PJ_CONTEXT* ctx = nullptr; PJ* pj = nullptr; }; CrsTransform::CrsTransform(const std::string& src, const std::string& dst) : impl_(std::make_unique()) { impl_->ctx = proj_context_create(); impl_->pj = proj_create_crs_to_crs(impl_->ctx, src.c_str(), dst.c_str(), nullptr); if (!impl_->pj) throw std::runtime_error("CrsTransform: failed to create " + src + "->" + dst); // 规范化为 (东/经度优先) 轴序,避免 PROJ 默认的 lat/lon 顺序坑 PJ* norm = proj_normalize_for_visualization(impl_->ctx, impl_->pj); if (norm) { proj_destroy(impl_->pj); impl_->pj = norm; } } CrsTransform::~CrsTransform() { if (impl_->pj) proj_destroy(impl_->pj); if (impl_->ctx) proj_context_destroy(impl_->ctx); } Xy CrsTransform::forward(double x, double y) const { PJ_COORD c = proj_coord(x, y, 0, 0); PJ_COORD r = proj_trans(impl_->pj, PJ_FWD, c); return Xy{r.xy.x, r.xy.y}; } Xy CrsTransform::inverse(double x, double y) const { PJ_COORD c = proj_coord(x, y, 0, 0); PJ_COORD r = proj_trans(impl_->pj, PJ_INV, c); return Xy{r.xy.x, r.xy.y}; } } // namespace geopro::core ``` 在 `src/core/CMakeLists.txt`:加 `find_package(PROJ CONFIG REQUIRED)`、源加 `geo/CrsTransform.cpp`、`target_link_libraries(geopro_core PUBLIC PROJ::proj)`。 - [ ] **Step 4: 运行,确认通过** Run: `cmake --build build/debug --target geopro_tests && ctest --test-dir build/debug -R CrsTransform --output-on-failure` Expected: 2 用例 PASS(注:`proj_normalize_for_visualization` 后轴序为 x=经度/东、y=纬度/北)。 - [ ] **Step 5: 提交** ```powershell git add src/core/geo/CrsTransform.* tests/core/test_crs_transform.cpp src/core/CMakeLists.txt git commit -m "feat(core): CrsTransform(PROJ 封装, UTM/WGS84/WebMercator 互转)" ``` --- ## Self-Review 备注 - 覆盖:对应设计 §3(core 分层)、§5(LocalFrame 轴向/Z 基准、CrsTransform 多 CRS)、§6.1(点序 i 最快)、§7(阶梯色阶+alpha 来源)、§10(IInterpolator→ScalarVolume 去 VTK、maxDist 包络裁剪)。 - 无占位:每步含完整代码与命令。 - 类型一致:`ScalarVolume`/`Grid` 点序 `i` 最快贯穿 Task 2/4;`Rgba`/`AlphaScale`/`parseColor` 跨 Task 3 一致;`PointSet`/`GridSpec` 跨 Task 4 一致。 - 铁律:core 仅依赖 std + Eigen + PROJ,**无 Qt/无 VTK**。 - 后续:Phase 2(data 解析器)将真实样本填进这些模型并用 `tools/validate_samples.py` 结论交叉校验。