geopro/docs/superpowers/plans/2026-06-07-m1-phase2-data.md

204 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 1core 数据模型补全(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 2JSON 解析器(纯函数,无 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 3Repository 接口 + 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 锁文件)。