geopro/docs/superpowers/plans/2026-06-12-detail-render-en...

15 KiB
Raw Permalink Blame History

数据详情「按类型渲染」通用引擎 + 迁移 inversion + measurement_data 实现计划

2026-06-12。承接 2026-06-11-dataset-detail-other-dd-types.md(Phase 1 策略分派已 done)。 决策(用户已拍板=选项2):建通用渲染引擎,并把已验收的 dd_inversion_data 一起迁到新引擎、删旧专用代码,终态只有一套。 铁律:不确定必用 Playwright 实看原版;严格 1:1;无活样本类型 BLOCKED。 工作方式:subagent-driven + TDD(真实夹具) + 破坏性接口改形原子落地保持构建绿。

0. 背景与动机

详情视图有多种 dd 类型,每类页签集 + 图形 kind 不同。现状把"两页签(chartReady/gridReady) + 两个专用 Load 句柄 + 写死 inversion 字段"耦合进三层。要支持 measurement_data / gr_data(柱状) / trajectory(轨迹) / grid(白化) 等且可持续扩展,需围绕「页签描述符」统一。

1. Phase 0 实测成果(2026-06-12,真实响应,已抓夹具)

API base:http://tenant.geomative.cn/pop-api/business。Auth header:geomativeauthorization: Geomative <token>。 同一 TM(project=1438889436225536 / structParentId=1438889842614272)集齐 4 类样本:

ddCode dsId 样本 端点 数据形状 页签 → render kind
dd_ert_measurement_data 原始 1453611522236416 GET dd/ert/measurement/scatter/graph?dsObjectId=&vFieldCode=(图) + GET dd/ert/measurement/rows?dsObjectId=(列表) 图:{electrodeList[40], scatterGraphConf{x[2],v[6],y[3],vcalculationMethod[3]}, scatterGraphData{hasCoordinate,hasElevation,min[9],max[9],rows[576]位置数组}};列表:{filedList[12], rowList[576]键控对象,含 vmap{R,V,I,R0_RD,SP,R0}} 散点伪剖面=Scatter + 列表=Table
dd_ert_measurement_gr_data 接地电阻 1453611522285569 GET dd/ert/measurement/gr/rows?dsObjectId= array[40] of {electrodeId,testDate,testTime,p1Rg,p1RgStatus,p2Rg,p2RgStatus} 柱状图=Bar(X=electrodeId,Y=电阻Ω,系列P1) + 列表=Table
dd_trajectory_data 坐标 1438893961060352 GET dd/ert/trajectory/rows?dsObjectId=(+/line?…&frontCrsCode 400 需参数) {rowList[40]{electrodeNo,projectX,projectY,longitude,latitude,elevation,lineNo,channelNo,…}, gridHeaderDisplay[14]} 地图=PolylineMap(平面折线,非GIS底图) + 列表=Table + 高程=LineProfile
dd_grid 白化 1438944148742207 GET dd/ert/grid/rows?dsObjectId=&pageNo=&pageSize= {rowList[N]{x,y,id}, gridHeaderDisplay[2], total} 分页 列表=Table(仅,分页)

夹具:tests/fixtures/dd/ert-measurement-scatter.jsonert-measurement-rows.json(真实响应,rows 裁剪至 20,保 conf/min/max/electrodeList 全量)。

散点位置数组列序(rows[i],与键控 rows 比对得):[0]平距 [1]斜距 [2]layerNo(null) [3]伪深度(负,向下) [4]选中v原值 [5]log10(值) [6]导数态 [7]0 [8]id(str) [9]高程伪深度 [10]a [11]b [12]m [13]n [14]K [15]rowNo [16]选中v值(=色)。默认 vFieldCode 空 → v=视电阻率 R0;x=平距;y=伪深度。色阶 cauto(同反演原数据,按 v 有限值 min/max 归一化)。

渲染 kind 全集:Scatter / FilledContour / Bar / LineProfile / PolylineMap / Table /(未来 Image,GPR BLOCKED)。 通用 Table:{gridHeaderDisplay 或 filedList(列定义) + rowList(对象)} 形状被 measurement列表/grid白化/trajectory列表共用 → 一个 DataTableView + 一个 parseTable 通吃(分页与列格式化为 parse-time/decorator,见 §4)。

2. 目标架构

依赖箭头:controller(ViewKind/TabSpec/Controller) ← app/strategies, app/views ; data(DetailLoad/Repo) 产出 QVariant 载荷

2.1 载荷(payload)——src/model/detail/DetailPayloads.hpp(纯数据,无 Qt-widget 依赖,Q_DECLARE_METATYPE)

  • ScatterPayload { core::ScatterField scatter; core::ColorScale scale; }(反演原数据 + measurement 散点共用;≈现 ChartParts)
  • ContourPayload { core::Grid grid; core::ColorScale scale; std::vector<core::Anomaly> anomalies; }(≈现 GridParts)
  • TablePayload { std::vector<TableColumn> columns; std::vector<std::vector<QString>> rows; int total = 0; };TableColumn { QString code, title; int width; int sort; }
  • (Bar/LineProfile/Polyline 载荷待对应类型阶段再加,YAGNI)

2.2 页签描述符 + 策略——src/controller/DatasetDetailTab.hpp + IDatasetChartStrategy.hpp

enum class ViewKind { Scatter, FilledContour, Bar, LineProfile, PolylineMap, Table /*,Image*/ };
struct TabSpec { QString title; ViewKind kind; QString loaderKey; bool lazy=false; bool paginated=false; };
struct IDatasetChartStrategy { virtual std::string ddCode() const=0; virtual std::vector<TabSpec> tabs() const=0; }; // tabs() 取代 hasGridPhase()

ChartStrategyRegistry 不变。ErtInversionStrategy::tabs() = {{"原数据",Scatter,"inversion.scatter",false},{"网格数据",FilledContour,"inversion.grid",true}}

2.3 通用异步句柄——src/data/api/DatasetLoadHandles.*

合并 ChartLoad/GridLoad → 单 DetailLoad{ virtual void abort(); signals: done(QVariant); failed(QString); } + ApiDetailLoad(包 ApiBatch + Parser=std::function<QVariant(const QList<net::ApiResponse>&)>)。aborted_ 守卫 + 双 try/catch→failed + deleteLater 三件套从 ApiChartLoad 原样照搬。删 ChartParts/GridParts(被 payload 取代)或保留作中间态(由实现者定,优先删以免双份)。

2.4 仓储——IAsyncDatasetRepository::loadAsync(loaderKey,dsId) + ApiDatasetRepository 分派表

唯一端点+解析器集中处。if (key=="inversion.scatter") return makeInversionScatter(dsId); …;未知 key 抛。每 make 建 ApiBatch + 注入返回 QVariant::fromValue(payload) 的解析器。删 loadChartAsync/loadGridAsync

measurement scatter 的 dsObjectIdquery 参数(?dsObjectId=&vFieldCode=,默认 vFieldCode 空),≠反演 path 参数;用现有 enc()

2.5 控制器——DatasetDetailController 变通用(无 per-ddCode if)

slots: openDataset(dsId,ddCode,dsName), loadTab(dsId,ddCode,tabIndex), focusDataset(dsId);
signals: datasetOpened(dsId,dsName, std::vector<TabSpec>), tabLoadStarted(dsId,tabIndex),
         tabReady(dsId,tabIndex,QVariant), loadFailed(dsId,msg), focusRequested(dsId);

openDataset:查 registry,无→loadFailed("暂不支持…")(降级不变);emit datasetOpened(...,strategy->tabs());对每个非 lazy 页签 loadTabloadTab:唯一保存 §5.0 安全不变量处,按 tab 槽位:inflight_=QMap<int,QPointer<DetailLoad>>;abort 旧槽 → 新 load → tabLoadStarted → done 回调里 if (load!=inflight_.value(i)) return;(身份比对)→ tabReady。析构 abort 全部。删 chartReady/gridReady/ChartData/GridData/loadGridData

2.6 视图 + 工厂 + 壳

  • src/app/panels/chart/IDetailView.hpp:{ QWidget* widget(); void setPayload(const QVariant&); }
  • RawDataChartView/GridDataChartView 实现 IDetailView:setPayload 解包 ScatterPayload/ContourPayload → 调原 setData(...)(渲染代码零改)。坏/空 variant → 可见"渲染数据格式错误"不崩。
  • DetailViewFactory.{hpp,cpp}:makeDetailView(ViewKind,parent) switch → 对应视图。
  • DatasetDetailPage:build(tabs) 按 TabSpec 建页签(buildTabbedPanel + 工厂);lazy 页签首次激活发 tabNeeded(dsId,ddCode,i)(泛化现 gridDataNeeded);setTabPayload(i,QVariant)views_[i]->setPayload。删写死 rawView_/gridView_/kGridTabIndex
  • DatasetDetailPanel:onDatasetOpened(dsId,dsName,tabs) 建 page→build;按 dsId 转发 tabReady/tabNeeded。dsId 去重壳逻辑不动。
  • main.cpp:接线改 datasetOpened/tabReady/tabNeeded/tabLoadStarted;registry 注册不变(后续加 MeasurementStrategy)。与控制器改形同提交(原子)。

3. 实施序列(任务)

E1a 引擎地基(加性,构建保持绿) [task #1]

新增 payloads / TabSpec/ViewKind / DetailLoad+ApiDetailLoad(与旧句柄并存) / loadAsync 分派(inversion.scatter/grid 产 payload),与旧 loadChartAsync/loadGridAsync 并存。TDD:DTO→payload、句柄桩(tests/data/test_dataset_load_handles.cpp 仿现有桩)、loadAsync 分派。构建+测试全绿,旧路径不动。

E1b 破坏性改形 + inversion 迁移(原子) [task #2]

控制器/策略接口/视图 IDetailView/工厂/Page/Panel/main.cpp 全部切到 tab 引擎;删旧 chartReady/gridReady/ChartData/GridData/ChartLoad/GridLoad/loadChartAsync/loadGridAsync/hasGridPhase。改全部相关测试(controller/strategy_registry/handles)。一个提交内落地,构建+测试绿 + 跑 app 双击标准 ds 1458990939709440 目视回归两页签(散点+网格等值面+异常)正常。

E2 measurement_data(③散点伪剖面 + 列表) [task #3]

Phase 0 色阶补查(2026-06-12,已坐实,夹具齐):measurement 散点有 colorBar,来自 POST lvl/colorGradation/getDetail body {dsObjectId, businessCode:"<vFieldCode,默认R0>", type:3}(注意:businessCode=v字段码、type=3,≠反演的 type1/2;用 businessCode='' 会返回 null)。响应是与反演同构的混合格式 colorBar(hex + rgba alpha 01,18 个对数间距 stops 0.00→#00008B … 208.40→#FF0000 … 1323.20→rgba(48,0,48,1),lvlSchemeType:normal/equalAreaLayerCount/labelConfig/lineConfig)——现有 DatasetChartDto colorBar 解析器(AlphaScale::Unit)直接可用。截图核对:散点按 v 值绝对值离散映射到 colorBar(同反演网格 colorAtDiscrete,反演原数据的连续 cauto),右侧 ColorBarWidget 显示离散级。夹具:tests/fixtures/dd/ert-measurement-{scatter,rows,colorbar}.json;原版截图 meas-scatter-original.jpeg

⚠️ 着色模型修正(2026-06-12 读原版 Plotly _fullData 实测,推翻"离散"判断):原版是 Plotly scattergl,散点连续 cauto 着色(非离散!):marker.color=原始 v 值(col4,含负值,本例范围 -1066.59~1232.44);cauto:truecmin/cmax=数据 v 的真实 min/max(含负离群值);colorscale=连续 18 段,position=colorBar值/maxColorBar值(1323.20);上色=norm(v)=(v-cmin)/(cmax-cmin) 查 colorscale 连续插值。验证 v=50.18→norm 0.4857→暗紫(stop 0.358 与 0.540 之间)。这与 inversion 原数据完全同路(交接 §0.2:cauto 数据范围 + colorscale 位置 val/maxVal),measurement 必须用 discreteColor=false 走同一条已验收路径,而非我 E2 误用的 colorAtDiscrete。负离群值撑大数据范围 → R0=25~250 全挤色阶中段暗紫,故原版几乎全暗紫(仅极端值现橙/蓝)。colorBar 图例(竖排右侧离散色块标 0.00~1323.20)独立于 cauto,显示 colorBar 自身 value→color。hover=浮动黑底框 X/Y/Value/a/b/m/n(另有底部 footer A=B=M=N=DataRow=Pseu_Resis=)。

散点渲染规格(截图确认):x=斜距 slopeDistance(toolbar 默认,col1;≈平距,几乎不可辨,存疑标用户核对)、y=伪深度 pseudoDepth(col3,负向下)、色=视电阻率 R0(col16,离散 colorBar)、x 轴在顶部(同反演原数据)、y=0 处画 40 个灰色菱形=电极位置(electrodeList 的 x)、hover 显示 A= B= M= N= DataRow= Pseu_Resis=(col10-13=A/B/M/N + 值)。工具条(散点图/数据列表 切换、显示/隐藏、数据过滤、x/y/v/计算方法下拉、色阶配置、生成视电阻率、反演运算、另存为、导出)=范围外,MVP 只渲染默认静态视图。

  • 端点:scatter GET dd/ert/measurement/scatter/graph?dsObjectId=&vFieldCode=(空=默认R0)+ colorBar POST lvl/colorGradation/getDetail{dsObjectId,businessCode:"R0",type:3};列表 GET dd/ert/measurement/rows?dsObjectId=。loaderKey ert_measurement.scatter 用 ApiBatch 组 scatter+colorBar 两请求(类比反演 loadChartAsync 的 scatter+type1)。

  • src/data/dto/MeasurementDto.{hpp,cpp}:parseMeasurementScatter(scatterJson, colorBarJson)→ScatterPayload(位置数组 rows:x=col1 斜距/y=col3 伪深度/色值=col16;colorBar 复用现有解析器→离散 ColorScale;电极 electrodeList→灰菱形,可放 ScatterPayload 扩展或单列);parseTable(QJsonObject)→TablePayload(通用:列来自 gridHeaderDisplayfiledList,值预格式化为 QString;measurement 的 vmap 由解析器摊平为列——按 filedList 列码取 vmap[code])。

  • src/app/panels/chart/DataTableView.{hpp,cpp}:IDetailView + QTableView + QAbstractTableModel(由 TablePayload 构),列宽/标题/排序来自 TableColumn。

  • src/app/strategies/MeasurementStrategy.hpp:ddCode=dd_ert_measurement_data,tabs={{"散点伪剖面",Scatter,"ert_measurement.scatter",false},{"列表",Table,"ert_measurement.rows",true}}

  • ApiDatasetRepository:加 ert_measurement.scatter(scatter/graph,query 参数 + 默认 vFieldCode 空)、ert_measurement.rows(measurement/rows)loaderKey。

  • DetailViewFactory:加 Table case。main.cpp:注册 MeasurementStrategy 一行。

  • TDD 用 tests/fixtures/dd 夹具:tests/data/test_measurement_dto.cpp(散点位置解码/y 负号/列提取/vmap 摊平) + 控制器 2 页签谱 + loadAsync 分派。

  • 构建+测试绿 + 跑 app 双击「ERT原始数据」对照原版散点伪剖面 + 列表目视核对。

4. 风险与裁断(architect 评估)

  1. 不要过度泛化控制器信号面:tabReady(dsId,i,QVariant) 单信号已吸收全部近期形状(类型擦除)。激进泛化"句柄/载荷"(合一 DetailLoad/QVariant,是纯去重),但控制器信号保持扁平最小。分页的 pageNo 等真做 dd_grid 再加一个默认参,加性无重构。
  2. QVariant 类型擦除以编译期安全换扩展性:错的 loaderKey→ViewKind→payload 三元组是运行期 cast miss 而非编译错。缓解:downcast 仅在每视图 setPayload 一处;坏 variant 出可见错误不崩(配合现 GuardedApplication::notify);每策略一条 round-trip 测试。接受此权衡(否则 N×3 个 XxxLoad/XxxReady/XxxData 爆炸)。
  3. 不要提前耦合 BLOCKED 的 GPR/radar:引擎结构上可容纳(加 ViewKind::Image/GisMap + 新视图+解析器+loaderKey,控制器/句柄零改),故不被锁死;但现在不加任何 enum 值/桩(1:1 + 无样本)。trajectory 的 PolylineMap 是金丝雀,证明非图非表视图也能套进引擎。

通用 Table 的边界:list 形状(measurement/trajectory/grid)共用对;但 ① 分页(dd_grid 独有)= PaginatedTableView 装饰器或 spec paginated 标志 + page 发 loadTab(...,pageNo),基类 DataTableView 保持哑;② 列格式化/值语义(measurement 的 vmap 摊平、gr 的 status 标志着色)= parse-time 处理(TablePayload 存预格式化 QString),需要彩色状态格时再加 TableColumn.kind 提示(YAGNI)。bar 不是表复用——gr_data 主页签是 Bar,列表页签才复用 parseTable。

5. 安全不变量(spec §5.0,务必保留)

"abort 后绝不回灌"三件套:① 每层 aborted_ 入口守卫;② 控制器句柄身份比对(if (load!=inflight_.value(i)) return;);③ 一律 deleteLater。错误判定:业务 code!=200 || !rawError.isEmpty()(HTTP 200 也可能 code=500)。迁移后这三件套从"按 Chart/Grid 阶段"改"按页签槽位",逐字保留,不得重造削弱。

6. 构建/测试命令

  • 构建:powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1(LNK1104 先 Get-Process geopro_desktop|Stop-Process -Force)。
  • 测试:powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1(先 build)。基线 122/122 绿
  • 单测过滤:build/release/tests/geopro_tests.exe --gtest_filter=<Suite>.*