plan: M1 Phase 2 数据层(Repository+解析器+对象树联动) 实现计划
This commit is contained in:
parent
59f4f0a41a
commit
a7d624cdcc
|
|
@ -0,0 +1,203 @@
|
||||||
|
# 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 <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;` 旁):
|
||||||
|
```cpp
|
||||||
|
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`:
|
||||||
|
```cpp
|
||||||
|
#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`:
|
||||||
|
```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 <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 之前):
|
||||||
|
```cmake
|
||||||
|
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`:
|
||||||
|
```cpp
|
||||||
|
#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 锁文件)。
|
||||||
Loading…
Reference in New Issue