# M1 Phase 2:数据层(Repository + 解析器)实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development。Steps 用 `- [ ]`。 **Goal:** 把真实样本文件解析成 core 领域模型,经 Repository 抽象喂给工作台;最终实现「左侧对象树 → 选中数据集 → 中央 QVTK 渲染该数据集」的真实联动(在本地样本上)。 **Architecture:** `data/` 层。纯解析器(无 Qt,nlohmann-json,gtest 可测)+ `LocalSampleRepository`(用 QFile 读真实中文路径文件 → 调解析器,并合成一个小项目结构树)。app 的左/右占位面板替换为真实对象树/数据集列表,选中驱动渲染。 **Tech Stack:** C++17 / nlohmann-json / Qt(QFile,data 层可依赖 Qt;core 仍不依赖)/ gtest。 **前置:** Phase 1 core 完成(LocalFrame/模型/ColorScale/IDW/CrsTransform,12/12 绿)。构建经 `external/dev.bat`,统一 Release。样本 ASCII 拷贝在 `D:/dev/spike_data/`(grid.json/colorbar.json);真实中文文件在 `docs/剖面网格数据的色阶数据2等文件/`。 **已知样本结构(spec §6.1,已核实):** - 剖面原数据N.txt:`data:{min,max,projectXList,projectYList,vlist,xlist,ylist,hlist}`(2597 点) - 剖面网格数据N.txt:`data:{x[100],y[22],v[22][100],z[22][100],elevation[100],lat[100],lon[100],vmin,vmax,sectionType}` - 剖面网格数据的色阶数据N.txt:`data:{properties:{colorBar:[[值,rgba]],lineConfig,labelConfig,lvlMinMax}}`(网格色阶 rgba alpha=0-255) - 异常.txt:`data:[{exceptionName,exceptionMarkType(1点/2线/3面),legend{...},location:{coordinate:[{x,y}]},...}]`(3 条,markType=2) **范围说明:** Phase 2 用**同步**接口(本地小数据,简单可测);async/分页/取消留到 ApiRepository(M1.5,spec §6 已记)。`eastCoord/northCoord` 字段名值颠倒,一律按 `projectX/projectY` 取(spec §5)。 --- ## Task 1:core 数据模型补全(Anomaly + 扩展 Grid/ScatterField) **Files:** `src/core/model/Field.hpp`(扩展)、`src/core/model/Anomaly.hpp`(新建)、`tests/core/test_model_data.cpp`(新建)、`tests/CMakeLists.txt` - [ ] **Step 1: 失败测试** `tests/core/test_model_data.cpp`: ```cpp #include #include "model/Field.hpp" #include "model/Anomaly.hpp" using namespace geopro::core; TEST(DataModel, GridHasMetaFields) { Grid g(2, 3); g.z.assign(6, 0.0); g.elevation = {1,2}; g.lat = {22.5,22.5}; g.lon = {114.1,114.1}; g.vmin = -10; g.vmax = 800; EXPECT_EQ(g.z.size(), 6u); EXPECT_DOUBLE_EQ(g.vmax, 800); } TEST(DataModel, AnomalyHolds) { Anomaly a; a.name = "ERT1-54"; a.markType = AnomalyMarkType::Polyline; a.localPts = {{1.0, 2.0}, {3.0, 4.0}}; EXPECT_EQ(a.markType, AnomalyMarkType::Polyline); EXPECT_EQ(a.localPts.size(), 2u); } ``` 在 `tests/CMakeLists.txt`(spike 之前)加 `target_sources(geopro_tests PRIVATE core/test_model_data.cpp)`。 - [ ] **Step 2:** 配置+编译,确认失败。 - [ ] **Step 3:** 扩展 `Field.hpp` 的 `Grid`,新增成员(在 `std::vector x, y;` 旁): ```cpp std::vector z; // [nx*ny] 褶皱面抬升(可空=平面) std::vector elevation; // [nx] 电极/地表高程 std::vector lat, lon; // [nx] 经纬度 double vmin = 0.0, vmax = 0.0; ``` `ScatterField` 增 `std::vector projX, projY;`(GIS 平面坐标)。 新建 `src/core/model/Anomaly.hpp`: ```cpp #pragma once #include #include namespace geopro::core { enum class AnomalyMarkType { Point = 1, Polyline = 2, Polygon = 3 }; struct Vec2 { double x, y; }; struct Anomaly { std::string name; std::string typeName; // exceptionTypeName AnomalyMarkType markType = AnomalyMarkType::Polyline; std::vector localPts; // location.coordinate(局部坐标) // 样式(legend)后续渲染用,先留最常用字段 std::string lineColor = "#000000"; double lineWidth = 1.0; bool dashed = true; }; } // namespace geopro::core ``` (均 header-only,无需改 core CMakeLists。) - [ ] **Step 4:** 编译+ctest `-R "DataModel"` → 2 PASS。 - [ ] **Step 5:** 提交 `feat(core): 补全数据模型(Anomaly + Grid/ScatterField 元字段)`。 --- ## Task 2:JSON 解析器(纯函数,无 Qt,真实样本 TDD) **Files:** `src/core/io/SampleParsers.hpp`/`.cpp`(放 core?否——解析依赖 nlohmann,core 目前不依赖。**放 data 层**:`src/data/parse/SampleParsers.{hpp,cpp}`)、`src/data/CMakeLists.txt`(新建)、`src/CMakeLists.txt`(加 `add_subdirectory(data)`)、`tests/data/test_parsers.cpp`、`tests/CMakeLists.txt` > 解析器输入 `const std::string& jsonText`(纯,便于测试;文件读取在 Task 3 的 Repository 用 QFile 做)。 - [ ] **Step 1:** 建 `src/data/CMakeLists.txt`: ```cmake find_package(nlohmann_json CONFIG REQUIRED) add_library(geopro_data STATIC parse/SampleParsers.cpp) target_include_directories(geopro_data PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_data PUBLIC geopro_core PRIVATE nlohmann_json::nlohmann_json) target_compile_features(geopro_data PUBLIC cxx_std_17) set_target_properties(geopro_data PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF) ``` `src/CMakeLists.txt` 加 `add_subdirectory(data)`(在 core 之后、app 之前)。 - [ ] **Step 2:** 失败测试 `tests/data/test_parsers.cpp`(读 ASCII 拷贝 `D:/dev/spike_data/grid.json`、`colorbar.json`;断言对齐已知事实): ```cpp #include #include #include #include "parse/SampleParsers.hpp" using namespace geopro::data; using namespace geopro::core; static std::string slurp(const char* p){std::ifstream f(p);std::stringstream s;s< #include "model/Field.hpp" #include "model/ColorScale.hpp" #include "model/Anomaly.hpp" namespace geopro::data { geopro::core::Grid parseGrid(const std::string& jsonText); geopro::core::ScatterField parseScatter(const std::string& jsonText); geopro::core::ColorScale parseColorScale(const std::string& jsonText); std::vector parseAnomalies(const std::string& jsonText); } // namespace geopro::data ``` `SampleParsers.cpp`(用 `nlohmann::json::parse`;`data` 子对象;Grid 的 v 是 `[j][i]`→填 `valueAt(i,j)`;ColorScale 用 `core::parseColor(s, AlphaScale::Bit255)` 逐段 addStop;异常按 `exceptionMarkType` 映射 enum、读 `location.coordinate[].x/y`、`legend.polylineColor/polylineWidth/polylineShape=="dash"`)。完整实现这些字段。 - [ ] **Step 5:** 编译+ctest `-R Parsers` → 2+ PASS。 - [ ] **Step 6:** 提交 `feat(data): 样本 JSON 解析器(grid/scatter/colorscale/anomaly)`。 --- ## Task 3:Repository 接口 + LocalSampleRepository **Files:** `src/data/repo/IDatasetRepository.hpp`、`src/data/repo/IProjectRepository.hpp`、`src/data/repo/LocalSampleRepository.{hpp,cpp}`、`src/data/CMakeLists.txt`(链 Qt6::Core)、`tests/data/test_local_repo.cpp` > LocalSampleRepository 用 **QFile**(正确读中文路径)读 `docs/剖面网格数据的色阶数据2等文件/` 真实文件 → 调解析器。并**合成一个小项目结构**:Project「香港 Volia」→ GS「测区」→ TM「ERT1」→ DS「剖面网格数据1」,供对象树展示。 - [ ] **Step 1:** 失败测试 `tests/data/test_local_repo.cpp`(读真实中文路径文件,验证拿到 Grid 100×22 + 合成树非空)。完整断言:`loadStructure()` 返回 ≥1 个 GS、其下 ≥1 个 TM;`loadGrid(dsId)` 返回 100×22。 - [ ] **Step 2:** 配置+编译失败。 - [ ] **Step 3:** 实现: - `IProjectRepository`/`IDatasetRepository` 接口(同步:`Project loadProject()`,`std::vector loadStructure()`,`Grid loadGrid(const std::string& dsId)`,`ColorScale loadColorScale(dsId)`,`std::vector loadAnomalies(dsId)` 等;`GsNode`/`TmNode`/`DsNode` 轻量树结构放 `repo` 头)。 - `LocalSampleRepository`:构造传样本目录;`loadStructure` 合成树;`loadGrid` 用 `QFile` 读 `剖面网格数据1.txt`→`QString`→`std::string`(UTF-8)→`parseGrid`。 - `src/data/CMakeLists.txt` 加 `find_package(Qt6 COMPONENTS Core)` + 链 `Qt6::Core`(data 层可用 Qt;但仍 AUTOMOC OFF,接口无 Q_OBJECT)。 - [ ] **Step 4:** 编译+ctest `-R "LocalRepo"` → PASS。 - [ ] **Step 5:** 提交 `feat(data): Repository 接口 + LocalSampleRepository(QFile 读中文路径)`。 --- ## Task 4:接入工作台 — 对象树 → 选中数据集 → 渲染(可见里程碑) **Files:** `src/app/main.cpp`(或拆出 `src/view/panels/ObjectTreePanel.*` + 简单 controller)、`src/app/CMakeLists.txt`(链 geopro_data) > 目标:左面板显示真实对象树(QTreeWidget,来自 `LocalSampleRepository::loadStructure`);双击/选中数据集 → 用 Repository 取 Grid+ColorScale → 复用 demo 的 banded contour 渲染到中央 QVTK。右面板显示该数据集属性(名称/通道/vmin-vmax)。 - [ ] **Step 1:** app 链 `geopro_data`;`main.cpp` 用 `LocalSampleRepository` 取结构填 `QTreeWidget`(左 dock)。 - [ ] **Step 2:** 把 demo 的 `buildGridSection` 重构成 `renderGrid(renderer, Grid, ColorScale)`(入参为 core 模型,不再直接读文件);选中数据集时调用之并 `renderWindow->Render()`。 - [ ] **Step 3:** 选中 → 右 dock 显示属性(QLabel/QFormLayout:数据集名、网格 nx×ny、vmin/vmax)。 - [ ] **Step 4:** 构建 + 部署(`windeployqt`)+ 启动;**人工验证**:左树点数据集 → 中央出现 ERT 剖面、右侧出属性。(交互验证由用户确认) - [ ] **Step 5:** 提交 `feat(app): 对象树→选中数据集→渲染剖面 联动(本地样本)`。 --- ## Self-Review 备注 - 覆盖 spec §6(Repository/解析,同步版,async 留 M1.5)、§6.1(解析约定)、§9(对象树→渲染联动雏形)。 - 类型一致:解析器产出 `core::Grid/ScatterField/ColorScale/Anomaly`;Repository 同。 - 铁律:core 仍无 Qt/VTK;data 可用 Qt(QFile)但 AUTOMOC OFF、接口无 VTK。 - 构建经 `external/dev.bat`,Release;改 CMake 后先重配置。app 启动前先杀残留 `geopro_desktop.exe`(否则 LNK1104 锁文件)。