11 KiB
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:
#include <gtest/gtest.h>
#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<double> x, y;旁):
std::vector<double> z; // [nx*ny] 褶皱面抬升(可空=平面)
std::vector<double> elevation; // [nx] 电极/地表高程
std::vector<double> lat, lon; // [nx] 经纬度
double vmin = 0.0, vmax = 0.0;
ScatterField 增 std::vector<double> projX, projY;(GIS 平面坐标)。
新建 src/core/model/Anomaly.hpp:
#pragma once
#include <string>
#include <vector>
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<Vec2> 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:
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;断言对齐已知事实):
#include <gtest/gtest.h>
#include <fstream>
#include <sstream>
#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<<f.rdbuf();return s.str();}
TEST(Parsers, Grid) {
Grid g = parseGrid(slurp("D:/dev/spike_data/grid.json"));
EXPECT_EQ(g.nx(), 100); EXPECT_EQ(g.ny(), 22);
EXPECT_EQ(g.values().size(), 100u*22u);
EXPECT_NEAR(g.vmin, -57.09, 0.1); EXPECT_NEAR(g.vmax, 828.08, 0.1);
EXPECT_EQ(g.lat.size(), 100u);
}
TEST(Parsers, ColorScale) {
ColorScale cs = parseColorScale(slurp("D:/dev/spike_data/colorbar.json"));
EXPECT_FALSE(cs.empty());
// 17 段;首段值 -57.09 → rgba(0,0,170,255)
auto c = cs.colorAt(-57.0);
EXPECT_EQ(c.b, 170);
}
tests/CMakeLists.txt(spike 之前):
target_sources(geopro_tests PRIVATE data/test_parsers.cpp)
target_link_libraries(geopro_tests PRIVATE geopro_data)
-
Step 3: 配置+编译,确认失败。
-
Step 4: 实现
src/data/parse/SampleParsers.hpp:
#pragma once
#include <string>
#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<geopro::core::Anomaly> 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<GsNode> loadStructure(),Grid loadGrid(const std::string& dsId),ColorScale loadColorScale(dsId),std::vector<Anomaly> 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 锁文件)。