feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
69 changed files with 5287 additions and 75 deletions
Showing only changes of commit 12813bd8d0 - Show all commits

View File

@ -42,16 +42,20 @@
| ddCode | 详情组件 / 含义 | 空间形态 |
|---|---|---|
| dd_trajectory_data | ElectrodeCom 电极/测线 | **线**lat/lon 序列)|
| dd_transient_electromagnetic_trajectory_data | BatteryComMapLineCom 瞬变电磁测线 | **线** |
| dd_radar_channel_trajectory / dd_radar_rtk_trajectory | 雷达通道/RTK 轨迹 | **线** |
| dd_inversion_data | ContourCom 等值面 | **竖直剖面**(沿测线距离 × 深度)|
| dd_ert_measurement_data | ScattersCom 散点 | **拟剖面**(电极对 × 视深,散点)|
| dd_ert_measurement_gr_data | GroundResistanceCom 接地电阻 | 沿线序列 |
| dd_gpr_channel_image / dd_radar / dd_gpr_channel_detail | 雷达图像/成果 | **竖直 B-scan**(沿轨迹 × 时深)|
| dd_grid | DdGridCom 网格 | 网格(**面 or 剖面,依数据而定,存疑**|
| dd_voxel / dd_Structual3D / dd_Property3D | 体素 / 结构 / 属性 3D | **三维体** |
| dd_3d_show | ThreeModelCom 3D 模型 | **三维模型** |
| //dd_transient_electromagnetic_trajectory_data | BatteryComMapLineCom 瞬变电磁测线 | **线** |
| //dd_radar_channel_trajectory / dd_radar_rtk_trajectory | 雷达通道/RTK 轨迹 | **线** |
| dd_inversion_data | ContourCom 等值面 | **剖面** |
| dd_ert_measurement_data | ScattersCom 散点 | **原始数据**(电极对 × 视深,散点) |
| //dd_ert_measurement_gr_data | GroundResistanceCom 接地电阻 | 沿线序列 |
| //dd_gpr_channel_image / dd_radar / //dd_gpr_channel_detail | 雷达图像/成果//重新分析 | **竖直 B-scan**(沿轨迹 × 时深)|
| dd_grid | DdGridCom 网格//扩展支持接地电阻 | 网格(**面 or 剖面,依数据而定,存疑**|
| dd_voxel / dd_Structual3D / //dd_Property3D | 体素 / 结构 / //属性 3D | **三维体** |
| //dd_3d_show | ThreeModelCom 3D 模型 | **三维模型** |
| dd_slice | 切片 | 三维分析 |
| ??dd_time_sensor | TimingSensor 时序传感器(折线图+数据表)| 时序曲线(非空间)**待确认** |
| ??dd_current_method_indicator | DdElectricDetectionCom 电流法指标(指标表+散点状态图)| 指标/状态(非空间)**待确认** |
> `??` 标记:原版 datasetInfo 有此详情组件,但初版台账漏列,详情视图是否纳入客户端范围**待确认**2026-06-22 补)。雷达/GPR 家族(`//` 标记)因客户端将重构、客户需求未定,整体搁置。
---

View File

@ -0,0 +1,168 @@
# 数据集详情视图 · 交互 100% 复刻台账
> 目标详情视图datasetInfo逐交互 1:1 复刻原版 web`D:\Git\lanbingtech\commercial-admin`)。
> 原则:对照原版源码找全差距;凡已实现的视图,其后端接口均已具备(客户端只需新增对应调用)。
> 范围:仅**已实现的 5 个 ddCode 视图**。雷达/GPR/电磁/3D 模型家族(客户端将重构、需求未定)整体搁置。
> 日期2026-06-22 立账。
## 0. 总览结论
| 视图 (ddCode) | 客户端类 | 复刻完成度 | 差距集中点 |
|---|---|---|---|
| 电极轨迹 dd_trajectory_data | TrajectoryStrategy / TrajectoryMapView 等 | ✅ **无差距** | 地图/列表/高程齐全;原版「导出」按钮原版自身亦未实现 |
| 接地电阻 dd_ert_measurement_gr_data | GrMeasurementStrategy / BarChartView | ✅ **无差距** | 柱状图/列表齐全;原版地面信息/模型/脚本/导出按钮原版自身亦未实现 |
| ERT 原始数据 dd_ert_measurement_data | MeasurementStrategy / RawDataChartView | ✅ **基本接通** | 工具条 1:1写操作全接仅 M14 框选后置(重型,已登记) |
| 反演等值面 dd_inversion_data | ErtInversionStrategy / RawDataChartView(原数据) + GridDataChartView(网格) | ✅ **基本接通** | 网格/白化/滤波/异常CRUD/自动标注/另存为/色阶均接;仅 I9 图上绘制、I14 富文本、I3 tmObjectId 透传后置 |
| 网格白化 dd_grid | GridStrategy / DataTableView | ✅ **已通** | 分页列表 + 「反演」功能按钮(载荷 functionList 驱动,复用 InversionFormDialog) |
**架构事实**:客户端 `ApiDatasetRepository` 当前只有 load操作无写操作。所有反演/保存/过滤/白化/滤波/异常写接口需新增客户端调用,沿用 `ApiClient(get/postJsonAsync) → ApiBatch → ApiDetailLoad` 模式(或为写操作引入独立的 command 调用路径)。
> ⚠️ 实现前务必直接读原版 `src/apis/datasetInfo/index.js` 与对应 `.vue` 复核请求体字段名(本台账 API 字段来自探查 agent 摘录,端点/方法可信,个别字段名以源码为准)。
---
## 1. 共享基础设施(先建,多视图复用)
这些组件被多个交互复用,应优先抽象,避免各视图各写一份。
### 1.1 反演动态表单对话框InversionForm
- **复用于**measurement「反演运算」「生成视电阻率」、grid「反演」。
- **流程**:① 查模型列表 → ② 选模型 → ③ 按 typeId 拉动态表单字段 → ④ 填参 → ⑤ 提交。
- **API**
- 模型列表 `GET /business/outerInversion/query/script?dsObjectId=`measurement/grid 通用)
- 动态表单 `POST /business/project/getDynamicForm` body `{projectId, type:6, typeId}`
- 提交反演 `POST /business/outerInversion/submitInversionTask` body `{dsId, properties, ...}`
- 生成视电阻率 `POST /business/dd/ert/measurement/createVisualResistivityData` body `{dsObjectId, scriptId, scriptParamListJsonStr}`
- **客户端现状**:无。需建 `InversionFormDialog`(动态字段渲染:分组卡片 + Select 为主)。
### 1.2 色阶配置编辑器(已存在,复用)
- **已有**`ColorScaleConfigDialog`(被 GridDataChartView 使用,且与三维体右键色阶共用)。
- **复用目标**measurement 散点「色阶配置」、inversion 原数据散点「色阶配置」。
- **API**:查 `POST /business/lvl/colorGradation/getDetail`,存 `POST /business/lvl/colorGradation`
- **注意**measurement 走 `businessCode=R0, type=3`inversion 原数据 `type=1`、网格 `type=2`。散点上色用 `colorSvc_`,编辑后需重建并重绘散点。
### 1.3 另存为对话框SaveAs
- **复用于**measurement 另存为、inversion 另存为。
- **measurement**`POST /business/dd/ert/measurement/saveRawData` `{dsId, operationType(1新增/0覆盖), name?}`(含新增/覆盖单选)
- **inversion**`POST /business/dd/ert/inversion/saveAsData` `{dsObjectId, name}`(仅名称)
---
## 2. ERT 原始数据measurement / RawDataChartView
客户端 measurement 工具条已 1:1 建出(`buildMeasurementToolbar`)。逐控件差距:
| # | 控件 | 原版行为 | 原版 API | 客户端现状 | 实现要点 |
|---|---|---|---|---|---|
| M1 | 显示/隐藏 | popconfirm→改全部点可见性持久化 | `POST /business/dd/ert/measurement/saveDisplayStatus` `{dsObjectId, ids[], status}` | ✅ **已通**onShowHide确认→调接口→本地切换 | — |
| M2 | 表格行可见性 switch | 行级 popconfirm→改单点 | 同 M1ids=[record.id]status 取反) | 🔸 **后置**DataTableView 行级开关列交互重,源 saveDisplayStatus 已具备) | 见 §6 |
| M3 | 数据过滤 | 弹窗(直方图+min/max)→生成过滤后数据集 | 查 `GET /business/scatterPlotDataFilter/getDataFilterConfig?dsObjectId&vFieldCode`;应用 `POST /business/scatterPlotDataFilter` `{sourceDsObjectId, sourceVFieldCode, min, max}` | ✅ **已通**ScatterFilterDialog范围 min/max + 应用);直方图绘制后置 | 直方图见 §6 |
| M4 | X 轴下拉(平距/斜距) | 本地换列重绘 | 无 | ✅ 已通(replotForAxis) | — |
| M5 | Y 轴下拉(伪深度/+高程/层数) | 本地换列重绘 | 无 | ✅ 已通 | 层数为 no-op原版亦无数据 |
| M6 | V 值下拉 | 重新请求散点+色阶 | `GET .../scatter/graph?vFieldCode=` + `POST .../getDetail{businessCode=新V}` | ✅ **已通**reloadForVValue 带 vFieldCode 重载) | — |
| M7 | 值类型下拉(线性/倒数/对数) | 本地换显示 | 无 | ✅ **已通**applyValueType 本地变换重上色) | — |
| M8 | 色阶配置 | 弹窗编辑+保存 | getDetail/colorGradation见 1.2 | ✅ **已通**(复用 ColorScaleConfigDialogproperties 含 colorBar+lineConfig+labelConfig | — |
| M9 | 生成视电阻率 | 反演弹窗(模型锁定视电阻率脚本) | createVisualResistivityData见 1.1 | ✅ **已通**InversionFormDialog::ApparentResistivity下拉 disabled+锁 `script_visual_resistivity_data`,对齐原版) | — |
| M10 | 反演运算 | 反演弹窗 | submitInversionTask见 1.1 | ✅ **已通**InversionFormDialog::Inversion | — |
| M11 | 另存为 | 新增/覆盖弹窗 | saveRawData见 1.3 | ✅ **已通**SaveAsDialog::RawData | — |
| M12 | 导出 DAT | 下载 base64 | `GET /business/dd/ert/measurement/rs2d/export?dsId&electrodePosition=2&ipDataMark=0&typeMeasurement=0` | ✅ **已通**exportDat参数对齐原版 | — |
| M13 | [i] 信息 | 点选看 A/B/M/N/Pseu/Row | 无(本地) | ✅ **已通**toggleInfoMode信息模式点选散点看属性 | — |
| M14 | 框选/点选模式 | enter/exitSelectMode | 无(本地) | 🔸 **后置**Qwt 橡皮筋框选+选区联动隐藏成本高,保留占位提示) | 见 §6 |
---
## 3. 反演等值面inversion
### 3.1 网格视图GridDataChartView
| # | 控件 | 原版行为 | 原版 API | 客户端现状 | 实现要点 |
|---|---|---|---|---|---|
| I1 | 网格(化) | 2步向导(选算法+参数)→网格化 | `GET .../queryAlgorithmModel/{ds}``GET .../getRawData/{ds}``POST /business/dd/ert/inversion/grid`(actionCode,x/y min/max,**xsize/ysize=点数**,xSpacing/ySpacing=间距,vmin/vmax,saveDataValueType) | ✅ **已通**GridWizardDialog 2步xsize=点数 xPoints/ySize=yPoints间距走 xSpacing/ySpacing对齐原版 toGridTheData | — |
| I2 | 色阶配置 | 弹窗 | getDetail/colorGradation | ✅ **已通**(本地生效;网格视图色阶不持久化到后端,与原版网格路径一致) | — |
| I3 | 白化 | 弹窗(3种方式) | `POST /business/dd/ert/inversion/whitenedData`;文件列表 `POST /business/dsObject/queryWhitenedDataList` | ✅ **已通**(白化弹窗+提交);`tmObjectId` 暂兜底空串 | tmObjectId 透传见 §6 |
| I4 | 滤波处理 | 弹窗(滤波器树+矩阵) | 列表 `GET /business/filter/queryFilter`;增 `POST /business/filter`;删 `DELETE /business/filter/delete/{id}`;应用 `POST /business/dd/ert/inversion/filterData` | ✅ **已通**(滤波弹窗含滤波器 CRUD+应用) | — |
| I5 | 显示异常 | 本地显隐 | 无 | ✅ 已通 | — |
| I6 | 显示等值线标注 | 本地显隐 | 无 | ✅ 已通 | — |
| I7 | 显示等值线提示 tooltip | 本地显隐 | 无 | ✅ **已通**chkContourTip 接 hover tooltip 显隐) | — |
| I8 | 简化容差滑块 | 防抖本地重算等值线(0~2,步0.1) | 无 | ✅ **已通**(防抖 applySimplify→setSimplifyTolerance 真生效) | — |
| I9 | 异常 创建 | 弹窗(类型/名称/备注)+图上绘形 | 类型 `GET .../queryExceptionTypeByProjectIdAndType/{pid}/{type}`;名建议 `POST /business/exception/getExceptionName`;新增 `POST /business/exception` | ✅ **表单已通**ExceptionDialog类型/名建议/备注+提交);🔸 **图上绘形后置** | 图上绘制见 §6 |
| I10 | 异常 删除 | 表格行删 | `DELETE /business/exception/{id}` | ✅ **已通**AnomalyTablePanel deleteRequested | — |
| I11 | 异常 详情/编辑 | 抽屉(名称/备注/样式) | `PUT /business/exception` | ✅ **已通**ExceptionDetailDialog只发 `{id, exceptionName, remark}`,对齐原版局部更新) | — |
| I12 | 异常 定位 | 本地高亮+缩放(防抖) | 无 | ✅ **已通**AnomalyTablePanel locateRequested→图上定位 | — |
| I13 | 自动标注 | 弹窗(规则+预览) | 预演 `POST /business/exception/exception-mark/execute`;批量存 `POST /business/exception/batch/create` | ✅ **已通**openAutoAnnotation 自动标注弹窗) | — |
| I14 | 富文本描述保存 | Quill→保存 | 存 `PUT /business/dsObject/updateDsObject/`;取 `GET /business/dsObject/getDetail/{ds}` | ✅ **保存链路已通**DescriptionPanel saveRequested→saveDescription取/存均通);🔸 **富文本降级为纯文本** | Quill 富文本见 §6 |
| I15 | 另存为 | 弹窗(名称) | `POST /business/dd/ert/inversion/saveAsData` | ✅ **已通**SaveAsDialog | — |
### 3.2 原数据散点视图RawDataChartView 默认工具条)
| # | 控件 | 原版行为 | 客户端现状 | 实现要点 |
|---|---|---|---|---|
| O1 | 网格 | 同 I1 网格化向导 | ✅ **已通**(复用 GridWizardDialog | — |
| O2 | 色阶配置 | 散点色阶(type1businessCode 空) | ✅ **已通**openInversionColorScaleproperties 含 colorBar+lineConfig+labelConfig | — |
| O3 | 另存为 | 同 I15 | ✅ **已通**SaveAsDialog::Inversion | — |
| O4 | 图形格式下拉 | 散点↔2D直方图等值线原版 disabled | ✅ 不做(原版自身禁用) | — |
---
## 4. 网格白化数据grid / DataTableView
| # | 控件 | 原版行为 | 原版 API | 客户端现状 | 实现要点 |
|---|---|---|---|---|---|
| G1 | 反演 | 反演弹窗 | submitInversionTask见 1.1 | ✅ 已接 | DataTableView 顶部功能按钮行(载荷 functionList 驱动,仅 dd_grid 非空),点 inversion → 复用 InversionFormDialog(Mode::Inversion) |
| G2 | 分页 | 服务端分页 | grid/rows pageNo/pageSize | ✅ 已通 | — |
---
## 5. 建议执行顺序(分阶段)
1. **阶段 A · 共享基础设施**
- InversionFormDialog模型列表+动态表单+提交)→ 一次解锁 M9/M10/G1。
- SaveAs 弹窗measurement + inversion 两形态)。
- 色阶配置接入散点(复用 ColorScaleConfigDialog→ M8/O2。
2. **阶段 B · measurement 主交互**
- M1/M2 可见性持久化、M11 另存为、M12 导出、M6 V值重载、M7 值类型、M3 数据过滤。
- M13/M14 信息/框选(交互重,最后)。
3. **阶段 C · inversion 写操作**
- I1/O1 网格化向导、I15/O3 另存为。
- I9~I13 异常 CRUD + 自动标注。
- I3 白化、I4 滤波。
- I7 tooltip、I8 简化容差真生效、I14 描述保存复核。
4. **阶段 D · grid**
- G1 反演按钮(阶段 A 完成后顺带)。
> 每个交互按 TDD先对 repository 写方法/解析写测试mock ApiClient再接 UIUI 视觉对照原版。
---
## 6. 复刻收尾状态2026-06-22
三份审查后的修正项已落地build 通过 + 测试全过285/285
### 6.1 已 100% 接通项
- **measurementM1~M13**显隐持久化、数据过滤范围、X/Y/V/值类型下拉、色阶配置、生成视电阻率(下拉锁 `script_visual_resistivity_data`)、反演运算、另存为、导出 DAT、[i] 信息点选——全部接通并对齐原版。
- **inversion 网格I1~I8、I10~I13、I15**网格化向导xsize/ysize=点数,对齐 toGridTheData、色阶、白化、滤波含滤波器 CRUD、显示异常/标注/tooltip、简化容差真生效、异常删除/详情编辑/定位、自动标注、另存为——全部接通。
- **inversion 原数据散点O1~O3**网格化、色阶type1、另存为——全部接通O4 原版自身禁用,不做。
- **grid 白化G1/G2**反演按钮functionList 驱动)、分页——已通。
### 6.2 审查修正项(本轮)
- **重复 connectM13/M14**:删除 `btnInfo`/`btnMarquee` 残留的 `clicked→showNotImplemented`消除信息按钮多弹「暂未实现」、框选按钮单击弹两次btnInfo 保留 checkable+toggled信息模式btnMarquee 保留单条占位提示。
- **异常详情更新字段I11**:原版 `contourPage.vue onOk``PUT /business/exception` 局部更新,**仅发 `{id, exceptionName, remark}`**(线样式是另一条独立 PUT且抽屉样式控件 disabled。客户端 `ExceptionDetailDialog::onConfirm` 已对齐:不再附带/覆盖 `legend`
- **色阶 properties 补齐M8/O2**:原版散点路径 `newLvlColorLevel``properties``colorBar` + `lineConfig{showLines,color,lineType}` + `labelConfig{showLabels,color}`battery/scatters 仅这三块,不含等值面专属的 lvlSchemeType/logLinesCount/equalAreaLayerCount。客户端两处散点保存已补齐这三块新增 `buildColorScaleProperties` 复用 `dlg.lineConfig()`)。`templateId` 原版非必需,客户端按原版处理。
### 6.3 self-check 结论(无需改)
- **#4 视电阻率模型锁定**`InversionFormDialog::ApparentResistivity` 已 `modelCombo_->setEnabled(false)` 且锁定 `code==script_visual_resistivity_data` 的项——与原版 `InversionDialog.vue`(静态 `disabled` + 锁脚本)一致。
- **#5 网格 xsize/ysize 绑点数**`GridWizardDialog` 的 `xSize_/ySize_` 是「X/Y点数」1~300默认 100`buildGridToBody` 映射 `xsize←xSize`、间距走独立 `xSpacing←xSpacing_`——与原版 `GridDialog.vue toGridTheData``xsize:xPoints`、`xSpacing:xInterval`)一致。
### 6.4 明确后置 / 降级项(本次不实现,重型或 Qt 受限)
| 项 | 原因 | 后续所需 |
|---|---|---|
| **M14 框选/点选模式** | Qwt 橡皮筋框选 + 选区联动隐藏成本高,原版 enter/exitSelectMode 交互重 | 接入 QwtPlotPickerRubberBand 矩形)+ 选区命中→批量 saveDisplayStatus保留占位提示 |
| **M2 行级可见性 switch** | DataTableView 需新增可选开关列 + 行级 popconfirm 交互 | 给 measurement 列表加 optional 开关列,复用 saveDisplayStatusids=[record.id]status 取反) |
| **M3 过滤直方图** | 过滤范围已通,仅缺直方图绘制(须取 getDataFilterConfig 分桶并渲染) | 在 ScatterFilterDialog 加直方图视图(分桶 + min/max 区间叠加) |
| **I9 异常图上绘形** | 表单已通;图上交互绘制多边形/折线/点(橡皮筋 + 顶点编辑)属重型 Qwt 交互 | 接入图上绘制工具(绘形→坐标回填 location与表单提交合流 |
| **I14 Quill 富文本** | 原版 attachedParameters.deltaContent 为 Quill DeltaQt 暂降级为纯文本 | 引入富文本编辑器QTextEdit 富文本 ↔ Delta 互转)或保持纯文本兜底 |
| **I3 白化 tmObjectId 透传** | 客户端视图未透传 `structParentId`(白化模板列表用),现兜底空串 | 上游改造:数据集列表把 `structParentId` 接进视图(属上游数据流改造) |

View File

@ -39,6 +39,20 @@ add_executable(geopro_desktop WIN32
panels/ObjectExceptionPanel.cpp
panels/DescriptionPanel.cpp
panels/chart/RawDataChartView.cpp
panels/chart/InversionFormDialog.cpp
panels/chart/InversionFormParse.cpp
panels/chart/ScatterDataOps.cpp
panels/chart/SaveAsDialog.cpp
panels/chart/ScatterFilterDialog.cpp
panels/chart/InversionProcessOps.cpp
panels/chart/GridWizardDialog.cpp
panels/chart/WhiteningDialog.cpp
panels/chart/FilterDialog.cpp
panels/chart/ExceptionDialog.cpp
panels/chart/ExceptionDetailDialog.cpp
panels/chart/AutoAnnotationDialog.cpp
panels/chart/ContourSimplify.cpp
panels/chart/ContourHoverTip.cpp
panels/chart/GridDataChartView.cpp
panels/chart/DataTableView.cpp
panels/chart/TablePager.cpp

View File

@ -123,6 +123,7 @@
#include "api/ApiProjectRepository.hpp"
#include "api/ApiDatasetRepository.hpp"
#include "api/ApiColorTemplateRepository.hpp"
#include "api/ApiDatasetCommandRepository.hpp"
#include "api/Api3dRepository.hpp"
#include "panels/ObjectTreePanel.hpp"
#include "login/LoginWindow.hpp"
@ -234,6 +235,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
geopro::data::IAsyncProjectRepository& projectRepo,
geopro::data::IAsyncDatasetRepository& datasetRepo,
geopro::data::IColorTemplateRepository& colorTplRepo,
geopro::data::IDatasetCommandRepository& cmdRepo,
geopro::controller::WorkbenchNavController& nav,
geopro::controller::DatasetDetailController& detailCtrl)
{
@ -947,6 +949,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
auto* detailPanel = new geopro::app::DatasetDetailPanel();
// 注入色阶模板仓储 + 当前 projectId 取值回调(网格剖面「色阶配置」编辑器的另存/打开/新建色阶)。
detailPanel->setColorTemplateRepo(&colorTplRepo, [&nav] { return nav.currentProjectId(); });
// 注入反演命令仓储measurement 反演运算/生成视电阻率。projectId 取值仍由页内 projectIdGetter 提供。
detailPanel->setCommandRepo(&cmdRepo);
auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情"));
// ForceNoScrollArea禁止 ADS 默认把整块内容(含标题栏/页签栏)包进 QScrollArea。
// 否则内容最小尺寸超过 dock 时,整个面板(标题+页签+图)一起滚动。改为内容自适应填充;
@ -1844,6 +1848,8 @@ int main(int argc, char* argv[])
geopro::data::ApiDatasetRepository datasetRepo(api);
// 色阶模板仓储lvl 模板 + clr 色阶):同一共享会话 ApiClient注入 2D/3D 色阶编辑器。
geopro::data::ApiColorTemplateRepository colorTplRepo(api);
// 反演命令仓储(反演运算 / 生成视电阻率 / 模型列表 / 动态表单):同一共享会话 ApiClient。
geopro::data::ApiDatasetCommandRepository cmdRepo(api);
// 策略注册表须比 detailCtrl 活得久(同作用域、在前声明)。
geopro::controller::ChartStrategyRegistry chartRegistry;
chartRegistry.add(std::make_unique<geopro::app::ErtInversionStrategy>());
@ -1863,7 +1869,8 @@ int main(int argc, char* argv[])
// 启动防护:工作台构建期间任何同步加载失败(如样本数据缺失)都不应让进程静默退出,
// 而是给出可见错误提示。否则登录后窗口未显示、进程消失,用户无从排查。
try {
buildWorkbench(*window, repo, projectRepo, datasetRepo, colorTplRepo, nav, detailCtrl);
buildWorkbench(*window, repo, projectRepo, datasetRepo, colorTplRepo, cmdRepo, nav,
detailCtrl);
} catch (const std::exception& e) {
QMessageBox::critical(
nullptr, QStringLiteral("启动失败"),

View File

@ -1,8 +1,10 @@
#include "panels/AnomalyTablePanel.hpp"
#include <QVBoxLayout>
#include <QTableWidget>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMessageBox>
#include <QTableWidget>
#include <QToolButton>
#include <QVBoxLayout>
namespace geopro::app {
static QString markName(int t) { return t == 1 ? "" : t == 3 ? "多边形" : "多段线"; }
@ -23,18 +25,45 @@ void AnomalyTablePanel::setAnomalies(const std::vector<geopro::core::Anomaly>& l
table_->setRowCount(static_cast<int>(list.size()));
for (int i = 0; i < static_cast<int>(list.size()); ++i) {
const auto& a = list[i];
// 创建时间/备注:优先用形参(兼容),否则回退 Anomaly 字段DTO 已解析)。
const QString ct = i < (int)createTimes.size() && !createTimes[i].isEmpty()
? createTimes[i] : QString::fromStdString(a.createTime);
const QString rm = i < (int)remarks.size() && !remarks[i].isEmpty()
? remarks[i] : QString::fromStdString(a.remark);
table_->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(a.name)));
table_->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(a.typeName)));
table_->setItem(i, 2, new QTableWidgetItem(markName(static_cast<int>(a.markType))));
table_->setItem(i, 3, new QTableWidgetItem(i < (int)createTimes.size() ? createTimes[i] : ""));
table_->setItem(i, 4, new QTableWidgetItem(i < (int)remarks.size() ? remarks[i] : ""));
auto* eye = new QToolButton(table_); eye->setCheckable(true); eye->setChecked(true);
eye->setText("👁");
table_->setItem(i, 3, new QTableWidgetItem(ct));
table_->setItem(i, 4, new QTableWidgetItem(rm));
// 操作列:定位 / 详情 / 删除(对照原版 contourPage 操作列),保留眼睛显隐。
auto* ops = new QWidget(table_);
auto* opLay = new QHBoxLayout(ops);
opLay->setContentsMargins(2, 0, 2, 0);
opLay->setSpacing(2);
auto* eye = new QToolButton(ops); eye->setCheckable(true); eye->setChecked(true);
eye->setText("👁"); eye->setToolTip(QStringLiteral("显示/隐藏"));
connect(eye, &QToolButton::toggled, this, [this, i](bool on) {
if (on) hidden_.erase(i); else hidden_.insert(i);
emit hiddenChanged(hidden_);
});
table_->setCellWidget(i, 5, eye);
auto* btnLocate = new QToolButton(ops); btnLocate->setText(QStringLiteral("定位"));
connect(btnLocate, &QToolButton::clicked, this, [this, i]() { emit locateRequested(i); });
auto* btnDetail = new QToolButton(ops); btnDetail->setText(QStringLiteral("详情"));
connect(btnDetail, &QToolButton::clicked, this, [this, i]() { emit detailRequested(i); });
auto* btnDelete = new QToolButton(ops); btnDelete->setText(QStringLiteral("删除"));
connect(btnDelete, &QToolButton::clicked, this, [this, i]() {
// 原版 a-popconfirm 二次确认 → 这里用 QMessageBox 确认。
if (QMessageBox::question(this, QStringLiteral("提示"),
QStringLiteral("确定删除该异常?")) == QMessageBox::Yes)
emit deleteRequested(i);
});
opLay->addWidget(eye);
opLay->addWidget(btnLocate);
opLay->addWidget(btnDetail);
opLay->addWidget(btnDelete);
opLay->addStretch();
table_->setCellWidget(i, 5, ops);
}
}
} // namespace geopro::app

View File

@ -6,16 +6,23 @@
class QTableWidget;
namespace geopro::app {
// ds 级异常表:名称/异常类型/几何类型/创建时间/备注/操作(显隐眼睛)。行显隐 → 信号驱动图表叠加。
// ds 级异常表:名称/异常类型/几何类型/创建时间/备注/操作。
// 操作列对照原版 contourPage定位 / 详情 / 删除(删除带二次确认)。
// 行级显隐(眼睛)保留:信号驱动图表叠加(与原版分开,眼睛为客户端既有能力)。
class AnomalyTablePanel : public QWidget {
Q_OBJECT
public:
explicit AnomalyTablePanel(QWidget* parent = nullptr);
// createTimes/remarks 形参保留兼容旧调用;为空时回退到 Anomaly 自带字段。
void setAnomalies(const std::vector<geopro::core::Anomaly>& list,
const std::vector<QString>& createTimes,
const std::vector<QString>& remarks);
const std::vector<QString>& createTimes = {},
const std::vector<QString>& remarks = {});
signals:
void hiddenChanged(const std::set<int>& hiddenIndices);
// 操作列:传出异常在当前列表中的下标(调用方据此取 Anomaly 拿 id/坐标)。
void locateRequested(int index);
void detailRequested(int index);
void deleteRequested(int index);
private:
QTableWidget* table_;
std::set<int> hidden_;

View File

@ -26,6 +26,10 @@ void DatasetDetailPage::setColorTemplateRepo(geopro::data::IColorTemplateReposit
projectIdGetter_ = std::move(projectIdGetter);
}
void DatasetDetailPage::setCommandRepo(geopro::data::IDatasetCommandRepository* repo) {
cmdRepo_ = repo;
}
void DatasetDetailPage::build(const QString& dsId, const QString& ddCode, const QString& dsName,
const std::vector<geopro::controller::TabSpec>& tabs) {
Q_ASSERT(views_.empty()); // build() 仅一次views_ 为已 release 的裸指针,二次构建会泄漏旧视图
@ -43,9 +47,10 @@ void DatasetDetailPage::build(const QString& dsId, const QString& ddCode, const
QVector<PanelTab> panelTabs;
for (size_t i = 0; i < tabs.size(); ++i) {
const auto& spec = tabs[i];
// 仓储与 projectId 回调透传给工厂(仅 FilledContour 视图消费)。
auto view = makeDetailView(spec.kind, this, colorTplRepo_,
projectIdGetter_); // 抛出由调用栈兜底GuardedApplication
// 仓储与 projectId 回调透传给工厂FilledContour 用色阶模板仓储Scatter 用反演命令仓储)。
// dsIdGetter 用本页 dsId_此处已赋值随项目/数据集稳定。
auto view = makeDetailView(spec.kind, this, colorTplRepo_, projectIdGetter_, cmdRepo_,
[this] { return dsId_; }); // 抛出由调用栈兜底GuardedApplication
IDetailView* raw = view.release(); // QWidget 由 this/QwtPlot 父子树接管生命周期
views_[i] = raw;
// lazy 页签:建覆盖该视图的加载遮罩(父为视图 widget随其尺寸覆盖图区

View File

@ -9,6 +9,7 @@
namespace geopro::data {
class IColorTemplateRepository;
class IDatasetCommandRepository;
}
namespace geopro::app {
@ -27,6 +28,10 @@ public:
void setColorTemplateRepo(geopro::data::IColorTemplateRepository* repo,
std::function<QString()> projectIdGetter);
// 反演命令仓储注入measurement 反演运算/生成视电阻率,须在 build 前设置)。
// dsId 用本页 dsId_build 内构造 dsIdGetter此时 dsId_ 已赋值projectId 复用上面的 getter。
void setCommandRepo(geopro::data::IDatasetCommandRepository* repo);
// 按页签集构建页签首次打开调一次。dsId/ddCode/dsName 用于 tabNeeded。
void build(const QString& dsId, const QString& ddCode, const QString& dsName,
const std::vector<geopro::controller::TabSpec>& tabs);
@ -63,6 +68,9 @@ private:
// 色阶模板仓储注入(透传给 makeDetailView → 网格剖面色阶编辑器)。
geopro::data::IColorTemplateRepository* colorTplRepo_ = nullptr;
std::function<QString()> projectIdGetter_;
// 反演命令仓储注入(透传给 makeDetailView → measurement 散点视图反演按钮)。
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
};
} // namespace geopro::app

View File

@ -11,6 +11,10 @@ void DatasetDetailPanel::setColorTemplateRepo(geopro::data::IColorTemplateReposi
projectIdGetter_ = std::move(projectIdGetter);
}
void DatasetDetailPanel::setCommandRepo(geopro::data::IDatasetCommandRepository* repo) {
cmdRepo_ = repo;
}
DatasetDetailPanel::DatasetDetailPanel(QWidget* parent) : QTabWidget(parent) {
setTabsClosable(true);
connect(this, &QTabWidget::tabCloseRequested, this, [this](int i) { widget(i)->deleteLater(); });
@ -35,6 +39,7 @@ void DatasetDetailPanel::onDatasetOpened(const QString& dsId, const QString& ddC
p = new DatasetDetailPage(this);
// 注入须在 build 前build 内造视图时即透传给工厂)。
p->setColorTemplateRepo(colorTplRepo_, projectIdGetter_);
p->setCommandRepo(cmdRepo_);
p->build(dsId, ddCode, dsName, tabs); // ddCode 透传 → 页内 tabNeeded 携带
const QString title = dsName.isEmpty() ? dsId : dsName; // 页签标题用数据名(空则回退 id
const int idx = addTab(p, title);

View File

@ -7,6 +7,7 @@
#include "DatasetDetailTab.hpp" // geopro::controller::TabSpec
namespace geopro::data {
class IColorTemplateRepository;
class IDatasetCommandRepository;
}
namespace geopro::app {
class DatasetDetailPage;
@ -21,6 +22,9 @@ public:
void setColorTemplateRepo(geopro::data::IColorTemplateRepository* repo,
std::function<QString()> projectIdGetter);
// 反演命令仓储透传给每个新建的详情页measurement 反演运算/生成视电阻率用)。
void setCommandRepo(geopro::data::IDatasetCommandRepository* repo);
// 数据集打开find-or-create 页 → build(tabs) → 加/抬该面板页签。
void onDatasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName,
const std::vector<geopro::controller::TabSpec>& tabs);
@ -42,5 +46,8 @@ private:
// 色阶模板仓储注入(新页 build 前 setColorTemplateRepo 透传)。
geopro::data::IColorTemplateRepository* colorTplRepo_ = nullptr;
std::function<QString()> projectIdGetter_;
// 反演命令仓储注入(新页 build 前 setCommandRepo 透传)。
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
};
} // namespace geopro::app

View File

@ -1,22 +1,38 @@
#include "panels/DescriptionPanel.hpp"
#include <QHBoxLayout>
#include <QPushButton>
#include <QTextEdit>
#include <QVBoxLayout>
#include "Theme.hpp"
namespace geopro::app {
DescriptionPanel::DescriptionPanel(QWidget* parent) : QWidget(parent) {
auto* lay = new QVBoxLayout(this);
lay->setContentsMargins(8, 8, 8, 8);
lay->setContentsMargins(geopro::app::space::kMd, geopro::app::space::kMd,
geopro::app::space::kMd, geopro::app::space::kMd);
edit_ = new QTextEdit(this);
edit_->setReadOnly(true);
edit_->setPlaceholderText(QStringLiteral("暂无描述"));
lay->addWidget(edit_);
lay->addWidget(edit_, 1);
auto* btnLay = new QHBoxLayout();
btnLay->addStretch();
saveBtn_ = new QPushButton(QStringLiteral("保存"), this);
saveBtn_->setEnabled(false); // 注入 cmdRepo 后启用
btnLay->addWidget(saveBtn_);
lay->addLayout(btnLay);
connect(saveBtn_, &QPushButton::clicked, this,
[this]() { emit saveRequested(edit_->toPlainText()); });
}
void DescriptionPanel::setText(const QString& text) {
edit_->setPlainText(text);
}
void DescriptionPanel::setText(const QString& text) { edit_->setPlainText(text); }
QString DescriptionPanel::text() const { return edit_->toPlainText(); }
void DescriptionPanel::setSaveEnabled(bool on) { saveBtn_->setEnabled(on); }
} // namespace geopro::app

View File

@ -2,17 +2,26 @@
#include <QWidget>
class QTextEdit;
class QPushButton;
namespace geopro::app {
// 数据集描述面板:只读文本,供网格数据底部页签「描述」使用。
// 数据集描述面板:可编辑文本 + 保存按钮I14
// 原版用 Quill 富文本DeltaQt 无对应控件 → 退化为纯文本编辑 + 保存;
// 保存时由调用方组装 {description, attachedParameters:{deltaContent}}(见 GridDataChartView
class DescriptionPanel : public QWidget {
Q_OBJECT
public:
explicit DescriptionPanel(QWidget* parent = nullptr);
void setText(const QString& text);
QString text() const;
// 注入「保存」可用性:无 cmdRepo/dsId 时禁用保存按钮(占位)。
void setSaveEnabled(bool on);
signals:
void saveRequested(const QString& text);
private:
QTextEdit* edit_;
QPushButton* saveBtn_;
};
} // namespace geopro::app

View File

@ -0,0 +1,255 @@
#include "panels/chart/AutoAnnotationDialog.hpp"
#include <utility>
#include <QComboBox>
#include <QFrame>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QJsonObject>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPointer>
#include <QPushButton>
#include <QSpinBox>
#include <QTableWidget>
#include <QVBoxLayout>
#include "FormKit.hpp"
#include "Theme.hpp" // scaledPx
#include "repo/IDatasetCommandRepository.hpp"
namespace geopro::app {
namespace {
constexpr int kDefaultMinPoints = 4; // 原版 minPointCount 默认 4
// 面异常类型固定 remarkSourceType="3"(原版自动标注仅支持面/polygon
const QString kPolygonType = QStringLiteral("3");
} // namespace
AutoAnnotationDialog::AutoAnnotationDialog(geopro::data::IDatasetCommandRepository* repo,
QString dsObjectId, QString projectId, QWidget* parent)
: QDialog(parent),
repo_(repo),
dsObjectId_(std::move(dsObjectId)),
projectId_(std::move(projectId)) {
setWindowTitle(QStringLiteral("自动标注"));
setModal(true);
resize(820, 520);
auto* root = formkit::dialogRoot(this);
auto* split = new QHBoxLayout();
// ── 左:规则列表 ────────────────────────────────────────────────
auto* leftCol = new QVBoxLayout();
leftCol->addWidget(new QLabel(QStringLiteral("标注规则:"), this));
auto* ruleContainer = new QWidget(this);
ruleHost_ = new QVBoxLayout(ruleContainer);
ruleHost_->setContentsMargins(0, 0, 0, 0);
leftCol->addWidget(ruleContainer);
auto* addBtn = new QPushButton(QStringLiteral("加规则"), this);
connect(addBtn, &QPushButton::clicked, this, [this]() { addRule(); });
leftCol->addWidget(addBtn);
leftCol->addStretch();
split->addLayout(leftCol, 1);
// ── 右:预览表 ──────────────────────────────────────────────────
auto* rightCol = new QVBoxLayout();
rightCol->addWidget(new QLabel(QStringLiteral("预览:"), this));
previewTable_ = new QTableWidget(0, 4, this);
previewTable_->setHorizontalHeaderLabels(
{QStringLiteral("异常名称"), QStringLiteral("异常类型"), QStringLiteral("阈值范围"),
QStringLiteral("阈值模式")});
previewTable_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
previewTable_->setEditTriggers(QAbstractItemView::NoEditTriggers);
rightCol->addWidget(previewTable_, 1);
split->addLayout(rightCol, 1);
root->addLayout(split, 1);
// ── 底部按钮 ────────────────────────────────────────────────────
auto* btnLay = new QHBoxLayout();
btnLay->addStretch();
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
auto* execBtn = new QPushButton(QStringLiteral("执行自动标注"), this);
saveBtn_ = new QPushButton(QStringLiteral("确定保存"), this);
saveBtn_->setEnabled(false); // 必须先执行得到预览才能保存
btnLay->addWidget(cancelBtn);
btnLay->addWidget(execBtn);
btnLay->addWidget(saveBtn_);
root->addLayout(btnLay);
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
connect(execBtn, &QPushButton::clicked, this, &AutoAnnotationDialog::onExecute);
connect(saveBtn_, &QPushButton::clicked, this, &AutoAnnotationDialog::onSave);
addRule(); // 默认一条规则
loadExceptionTypes(); // 拉面异常类型
}
void AutoAnnotationDialog::loadExceptionTypes() {
if (!repo_) return;
QPointer<AutoAnnotationDialog> self(this);
repo_->listExceptionTypes(projectId_, kPolygonType, [self](bool ok, QJsonArray list, QString) {
if (!self || !ok) return;
self->exceptionTypeOptions_ = {};
for (const QJsonValue& v : list) {
const QJsonObject o = v.toObject();
QString name = o.value(QStringLiteral("exceptionTypeName")).toString();
if (name.isEmpty()) name = o.value(QStringLiteral("label")).toString();
QString id = o.value(QStringLiteral("id")).toString();
if (id.isEmpty()) id = o.value(QStringLiteral("value")).toString();
if (!id.isEmpty())
self->exceptionTypeOptions_.append(
QJsonObject{{QStringLiteral("id"), id}, {QStringLiteral("name"), name}});
}
// 回填已存在的规则行下拉。
for (auto& r : self->rules_) {
r.type->clear();
for (const QJsonValue& ov : self->exceptionTypeOptions_) {
const QJsonObject o = ov.toObject();
r.type->addItem(o.value(QStringLiteral("name")).toString(),
o.value(QStringLiteral("id")).toString());
}
}
});
}
void AutoAnnotationDialog::addRule() {
auto* card = new QFrame(this);
card->setFrameShape(QFrame::StyledPanel);
auto* lay = new QHBoxLayout(card);
lay->setContentsMargins(4, 4, 4, 4);
RuleRow row;
row.mode = new QComboBox(card);
row.mode->addItem(QStringLiteral("数值"), 1);
row.mode->addItem(QStringLiteral("百分位"), 2);
row.min = new QLineEdit(card);
row.min->setPlaceholderText(QStringLiteral("min"));
row.min->setFixedWidth(scaledPx(60));
row.max = new QLineEdit(card);
row.max->setPlaceholderText(QStringLiteral("max"));
row.max->setFixedWidth(scaledPx(60));
row.minPoints = new QSpinBox(card);
row.minPoints->setRange(1, 100000);
row.minPoints->setValue(kDefaultMinPoints);
row.type = new QComboBox(card);
for (const QJsonValue& ov : exceptionTypeOptions_) {
const QJsonObject o = ov.toObject();
row.type->addItem(o.value(QStringLiteral("name")).toString(),
o.value(QStringLiteral("id")).toString());
}
lay->addWidget(new QLabel(QStringLiteral("模式"), card));
lay->addWidget(row.mode);
lay->addWidget(row.min);
lay->addWidget(row.max);
lay->addWidget(new QLabel(QStringLiteral("最小点数"), card));
lay->addWidget(row.minPoints);
lay->addWidget(row.type, 1);
rules_.push_back(row);
ruleHost_->addWidget(card);
}
QJsonArray AutoAnnotationDialog::buildRuleList() const {
QJsonArray arr;
for (const auto& r : rules_) {
QJsonObject rule{
{QStringLiteral("exceptionTypeId"), r.type->currentData().toString()},
{QStringLiteral("thresholdMode"), r.mode->currentData().toInt()},
{QStringLiteral("minPointCount"), r.minPoints->value()},
};
// min/max空 → null对照原版 Number(...) 或 null
const QString mn = r.min->text().trimmed();
const QString mx = r.max->text().trimmed();
rule.insert(QStringLiteral("thresholdMin"),
mn.isEmpty() ? QJsonValue(QJsonValue::Null) : QJsonValue(mn.toDouble()));
rule.insert(QStringLiteral("thresholdMax"),
mx.isEmpty() ? QJsonValue(QJsonValue::Null) : QJsonValue(mx.toDouble()));
arr.append(rule);
}
return arr;
}
void AutoAnnotationDialog::onExecute() {
if (!repo_) return;
const QJsonArray rules = buildRuleList();
if (rules.isEmpty()) {
QMessageBox::warning(this, windowTitle(), QStringLiteral("请至少添加一条规则"));
return;
}
QJsonObject body{
{QStringLiteral("dsObjectId"), dsObjectId_},
{QStringLiteral("projectId"), projectId_},
{QStringLiteral("exceptionMarkRuleList"), rules},
};
QPointer<AutoAnnotationDialog> self(this);
repo_->executeExceptionMark(body, [self](bool ok, QJsonObject data, QString msg) {
if (!self) return;
if (!ok) {
QMessageBox::warning(self, self->windowTitle(),
msg.isEmpty() ? QStringLiteral("执行失败") : msg);
return;
}
// 预览异常:兼容 data 直接为数组(wireObject 包成 value) 或 data.list。
QJsonArray list = data.value(QStringLiteral("value")).toArray();
if (list.isEmpty()) list = data.value(QStringLiteral("list")).toArray();
self->previewExceptions_ = list;
self->previewTable_->setRowCount(list.size());
for (int i = 0; i < list.size(); ++i) {
const QJsonObject o = list[i].toObject();
self->previewTable_->setItem(
i, 0, new QTableWidgetItem(o.value(QStringLiteral("exceptionName")).toString()));
self->previewTable_->setItem(
i, 1,
new QTableWidgetItem(o.value(QStringLiteral("exceptionTypeName")).toString()));
self->previewTable_->setItem(
i, 2, new QTableWidgetItem(o.value(QStringLiteral("remark")).toString()));
self->previewTable_->setItem(
i, 3,
new QTableWidgetItem(o.value(QStringLiteral("thresholdModeName")).toString()));
}
self->saveBtn_->setEnabled(!list.isEmpty());
if (list.isEmpty())
QMessageBox::information(self, self->windowTitle(),
QStringLiteral("未生成异常(无满足规则的区域)"));
});
}
void AutoAnnotationDialog::onSave() {
if (!repo_ || previewExceptions_.isEmpty()) return;
// 组装 exceptionList保留 execute 返回项的关键字段。
QJsonArray exceptionList;
for (const QJsonValue& v : previewExceptions_) {
const QJsonObject o = v.toObject();
exceptionList.append(QJsonObject{
{QStringLiteral("remarkSourceType"), o.value(QStringLiteral("exceptionMarkType"))},
{QStringLiteral("exceptionName"), o.value(QStringLiteral("exceptionName"))},
{QStringLiteral("exceptionTypeId"), o.value(QStringLiteral("exceptionTypeId"))},
{QStringLiteral("location"), o.value(QStringLiteral("location"))},
{QStringLiteral("remark"), o.value(QStringLiteral("remark"))},
});
}
QJsonObject body{
{QStringLiteral("projectId"), projectId_},
{QStringLiteral("remarkSourceId"), dsObjectId_},
{QStringLiteral("exceptionList"), exceptionList},
};
saveBtn_->setEnabled(false);
QPointer<AutoAnnotationDialog> self(this);
repo_->batchCreateException(body, [self](bool ok, QString msg) {
if (!self) return;
self->saveBtn_->setEnabled(true);
if (ok) {
self->accept();
} else {
QMessageBox::warning(self, self->windowTitle(),
msg.isEmpty() ? QStringLiteral("保存失败") : msg);
}
});
}
} // namespace geopro::app

View File

@ -0,0 +1,57 @@
#pragma once
#include <QDialog>
#include <QJsonArray>
#include <QString>
#include <vector>
class QComboBox;
class QLineEdit;
class QSpinBox;
class QTableWidget;
class QPushButton;
class QVBoxLayout;
namespace geopro::data {
class IDatasetCommandRepository;
}
namespace geopro::app {
// 自动标注对话框I13复刻原版 AutoAnnotationDialog
// 左:规则列表(阈值模式 数值/百分位、min/max、最小点数、异常类型
// 执行 → executeExceptionMark(预演) → 预览表;确定保存 → batchCreateException → reloadGrid。
// 异常类型仅支持面/polygon原版同故 listExceptionTypes 取 remarkSourceType="3"。
class AutoAnnotationDialog : public QDialog {
Q_OBJECT
public:
AutoAnnotationDialog(geopro::data::IDatasetCommandRepository* repo, QString dsObjectId,
QString projectId, QWidget* parent = nullptr);
private:
struct RuleRow {
QComboBox* mode = nullptr; // 1 数值 / 2 百分位
QLineEdit* min = nullptr;
QLineEdit* max = nullptr;
QSpinBox* minPoints = nullptr;
QComboBox* type = nullptr; // userData = 异常类型 id
};
void loadExceptionTypes(); // 拉面异常类型(填充所有规则行下拉)
void addRule(); // 加一条规则卡片
QJsonArray buildRuleList() const; // 组装 exceptionMarkRuleList
void onExecute(); // executeExceptionMark → 预览
void onSave(); // batchCreateException
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
QString dsObjectId_;
QString projectId_;
QVBoxLayout* ruleHost_ = nullptr;
std::vector<RuleRow> rules_;
QJsonArray exceptionTypeOptions_; // 缓存的类型列表({id,name}),新增规则行复用
QTableWidget* previewTable_ = nullptr;
QJsonArray previewExceptions_; // execute 返回的预览异常confirm 时批量存)
QPushButton* saveBtn_ = nullptr;
};
} // namespace geopro::app

View File

@ -0,0 +1,60 @@
#include "panels/chart/ContourHoverTip.hpp"
#include <cmath>
#include <QEvent>
#include <QMouseEvent>
#include <QToolTip>
#include <qwt_plot.h>
#include <qwt_plot_canvas.h>
#include <qwt_scale_map.h>
#include "panels/chart/ContourPlotItem.hpp"
namespace geopro::app {
ContourHoverTip::ContourHoverTip(QwtPlot* plot, int xAxis, int yAxis, QObject* parent)
: QObject(parent), plot_(plot), xAxis_(xAxis), yAxis_(yAxis) {
if (plot_ && plot_->canvas()) {
plot_->canvas()->setMouseTracking(true); // hover 需开启鼠标跟踪
plot_->canvas()->installEventFilter(this);
}
}
bool ContourHoverTip::eventFilter(QObject* obj, QEvent* ev) {
if (!plot_ || obj != plot_->canvas()) return QObject::eventFilter(obj, ev);
if (!enabled_ || !item_) return false; // 未开启提示则不处理
if (ev->type() == QEvent::Leave) {
QToolTip::hideText();
return false;
}
if (ev->type() != QEvent::MouseMove) return QObject::eventFilter(obj, ev);
auto* me = static_cast<QMouseEvent*>(ev);
if (me->buttons() != Qt::NoButton) return false; // 拖动平移中不弹(交给 LivePanner
const QwtScaleMap xMap = plot_->canvasMap(xAxis_);
const QwtScaleMap yMap = plot_->canvasMap(yAxis_);
const QPointF mp = me->position();
const double dataX = xMap.invTransform(mp.x());
const double dataY = yMap.invTransform(mp.y());
// 像素命中半径换算到数据坐标(取 x 方向比例x:y 等比锁定,足够近似)。
const double dx1 = xMap.invTransform(mp.x() + kHitRadiusPx) - dataX;
const double radius = std::fabs(dx1);
const double level = item_->contourLevelNear(dataX, dataY, radius);
if (std::isnan(level)) {
QToolTip::hideText();
return false;
}
// 对照原版 customHoverFormatter数值 + 坐标。
const QString text = QStringLiteral("等值线信息<br>数值: %1<br>坐标: (%2, %3)")
.arg(level, 0, 'f', 2)
.arg(dataX, 0, 'f', 2)
.arg(dataY, 0, 'f', 2);
QToolTip::showText(me->globalPosition().toPoint(), text, plot_->canvas());
return false; // 不消费
}
} // namespace geopro::app

View File

@ -0,0 +1,34 @@
#pragma once
#include <QObject>
class QwtPlot;
namespace geopro::app {
class ContourPlotItem;
// 等值线提示I7「显示等值线提示信息」监听画布鼠标移动无按键时
// 命中最近等值线则 QToolTip 显示其数值与坐标(对照原版 contour hover tooltip
// 默认关闭,由工具条复选框 setEnabled(true) 开启。不消费事件,与 LivePanner 共存。
class ContourHoverTip : public QObject {
Q_OBJECT
public:
ContourHoverTip(QwtPlot* plot, int xAxis, int yAxis, QObject* parent = nullptr);
void setItem(const ContourPlotItem* item) { item_ = item; }
void setEnabled(bool on) { enabled_ = on; }
protected:
bool eventFilter(QObject* obj, QEvent* ev) override;
private:
QwtPlot* plot_;
int xAxis_;
int yAxis_;
const ContourPlotItem* item_ = nullptr;
bool enabled_ = false;
static constexpr double kHitRadiusPx = 6.0; // 命中半径(像素)
};
} // namespace geopro::app

View File

@ -11,6 +11,7 @@
#include <qwt_scale_map.h>
#include "panels/chart/ColorMapService.hpp"
#include "panels/chart/ContourSimplify.hpp"
namespace geopro::app {
@ -41,6 +42,7 @@ void ContourPlotItem::setData(const core::Grid& g, ColorMapService* svc,
static_cast<int>(g.y.size()) < ny) {
fillImage_ = QImage();
dataBBox_ = QRectF();
linesRaw_.clear();
lines_.clear();
return;
}
@ -57,15 +59,60 @@ void ContourPlotItem::setData(const core::Grid& g, ColorMapService* svc,
opt.upsample = 2;
opt.makeLines = true;
auto res = render::buildContourBands(g, svc->scale(), opt);
lines_ = std::move(res.lines);
linesRaw_ = std::move(res.lines);
lines_ = linesRaw_;
// buildContourBands 当前未回填 level恒 0在此按线上代表点采网格值并吸附到最近色阶级
// 使标注显示真实等值线值。
resolveLineLevels(g, svc->scale());
linesRaw_ = lines_; // level 回填后同步到原始集(简化保留 level
applySimplify(); // 按当前容差抽稀(首次 tol=0 即原样)。
} else {
linesRaw_.clear();
lines_.clear();
}
}
void ContourPlotItem::setSimplifyTolerance(double tol) {
simplifyTol_ = tol < 0.0 ? 0.0 : tol;
applySimplify();
}
void ContourPlotItem::applySimplify() {
if (simplifyTol_ <= 0.0) {
lines_ = linesRaw_;
return;
}
lines_.clear();
lines_.reserve(linesRaw_.size());
for (const auto& ln : linesRaw_) {
render::ContourLine s;
s.level = ln.level;
s.pts = douglasPeucker(ln.pts, simplifyTol_);
lines_.push_back(std::move(s));
}
}
double ContourPlotItem::contourLevelNear(double dataX, double dataY, double hitDataRadius) const {
// 在已绘制等值线(简化后)找最近线段,命中半径内返回该线 level。
double bestD2 = hitDataRadius * hitDataRadius;
double bestLevel = std::nan("");
for (const auto& ln : lines_) {
if (std::isnan(ln.level)) continue;
for (std::size_t i = 1; i < ln.pts.size(); ++i) {
const auto& a = ln.pts[i - 1];
const auto& b = ln.pts[i];
const double dx = b.x - a.x, dy = b.y - a.y;
const double len2 = dx * dx + dy * dy;
double t = len2 > 0 ? ((dataX - a.x) * dx + (dataY - a.y) * dy) / len2 : 0.0;
t = std::clamp(t, 0.0, 1.0);
const double px = a.x + t * dx, py = a.y + t * dy;
const double d2 = (dataX - px) * (dataX - px) + (dataY - py) * (dataY - py);
if (d2 < bestD2) { bestD2 = d2; bestLevel = ln.level; }
}
}
return bestLevel;
}
void ContourPlotItem::resolveLineLevels(const core::Grid& g, const core::ColorScale& cs) {
const auto stops = cs.stopValues();
if (stops.empty() || lines_.empty()) return;
@ -158,6 +205,19 @@ QRectF ContourPlotItem::boundingRect() const {
return dataBBox_;
}
QRectF ContourPlotItem::anomalyBoundingRect(int index) const {
if (index < 0 || index >= static_cast<int>(anoms_.size())) return {};
const auto& pts = anoms_[index].localPts;
if (pts.empty()) return {};
double minX = pts.front().x, maxX = pts.front().x;
double minY = pts.front().y, maxY = pts.front().y;
for (const auto& p : pts) {
minX = std::min(minX, p.x); maxX = std::max(maxX, p.x);
minY = std::min(minY, p.y); maxY = std::max(maxY, p.y);
}
return QRectF(minX, minY, maxX - minX, maxY - minY);
}
void ContourPlotItem::draw(QPainter* painter, const QwtScaleMap& xMap, const QwtScaleMap& yMap,
const QRectF& /*canvasRect*/) const {
if (dataBBox_.isNull()) return;
@ -253,18 +313,21 @@ void ContourPlotItem::draw(QPainter* painter, const QwtScaleMap& xMap, const Qwt
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setBrush(Qt::NoBrush);
for (const auto& a : anoms_) {
for (int ai = 0; ai < static_cast<int>(anoms_.size()); ++ai) {
const auto& a = anoms_[ai];
if (a.localPts.empty()) continue;
const bool hl = (ai == highlightIdx_); // I12 当前定位高亮
QColor col(QString::fromStdString(a.lineColor));
if (!col.isValid()) col = QColor(0, 0, 0);
QPen pen(col);
pen.setWidthF(a.lineWidth > 0 ? a.lineWidth : 1.0);
pen.setStyle(a.dashed ? Qt::DashLine : Qt::SolidLine);
QPen pen(hl ? QColor(255, 255, 0) : col); // 高亮黄(对照原版 #ffff00
pen.setWidthF(hl ? std::max(3.0, a.lineWidth) : (a.lineWidth > 0 ? a.lineWidth : 1.0));
pen.setStyle(hl ? Qt::SolidLine : (a.dashed ? Qt::DashLine : Qt::SolidLine));
painter->setPen(pen);
if (a.markType == core::AnomalyMarkType::Point) {
const QPointF c = mapPt(a.localPts.front());
painter->drawRect(QRectF(c.x() - 3, c.y() - 3, 6, 6));
const double r = hl ? 5.0 : 3.0;
painter->drawRect(QRectF(c.x() - r, c.y() - r, 2 * r, 2 * r));
} else {
QPolygonF poly;
poly.reserve(static_cast<int>(a.localPts.size()));

View File

@ -33,11 +33,22 @@ public:
void setShowLines(bool on) { showLines_ = on; }
void setShowLabels(bool on) { showLabels_ = on; }
void setShowAnomalies(bool on) { showAnomalies_ = on; }
// I8 简化容差:对等值线做 Douglas-Peucker 抽稀数据坐标系tol>0 生效)。
// 改容差即重算 lines_从原始 linesRaw_调用方随后 replot。
void setSimplifyTolerance(double tol);
// I7 等值线提示:按数据坐标命中最近等值线,返回其 level无命中返回 NaN
// hitDataRadius 为命中半径(数据坐标,由调用方按像素半径换算)。
double contourLevelNear(double dataX, double dataY, double hitDataRadius) const;
// 线形⚙ 配置(色阶编辑器下发):等值线色/线型(虚实)、标注色。默认黑实线。
void setLineColor(const core::Rgba& c) { lineColor_ = c; }
void setLineDashed(bool dashed) { lineDashed_ = dashed; }
void setLabelColor(const core::Rgba& c) { labelColor_ = c; }
// I12 定位:高亮指定下标的异常(黄色加粗描边),-1=清除。调用方随后 replot。
void setHighlightedAnomaly(int index) { highlightIdx_ = index; }
// I12 定位:取某异常的数据坐标包围盒(用于视图缩放);下标越界/无点返回 null。
QRectF anomalyBoundingRect(int index) const;
int rtti() const override { return QwtPlotItem::Rtti_PlotUserItem; }
QRectF boundingRect() const override;
@ -48,10 +59,13 @@ public:
private:
void buildFillImage(const core::Grid& g, ColorMapService* svc);
void resolveLineLevels(const core::Grid& g, const core::ColorScale& cs);
void applySimplify(); // 从 linesRaw_ 按 simplifyTol_ 重算 lines_保留 level
QImage fillImage_; // 预渲染填充热力图ARGB32含透明无数据区
QRectF dataBBox_; // 数据包围盒x[xmin,xmax] y[ymin,ymax]
std::vector<render::ContourLine> lines_; // 矢量等值线(含 level
std::vector<render::ContourLine> linesRaw_; // 原始等值线(简化前,作简化数据源)
std::vector<render::ContourLine> lines_; // 当前绘制等值线(按容差简化后,含 level
double simplifyTol_ = 0.0; // I8 简化容差数据坐标0=不简化)
std::vector<core::Anomaly> anoms_; // 异常叠加
bool showLines_ = true;
@ -60,6 +74,7 @@ private:
core::Rgba lineColor_{0, 0, 0, 255}; // 等值线色(默认黑)
bool lineDashed_ = false; // 等值线虚实(默认实线)
core::Rgba labelColor_{0, 0, 0, 255}; // 标注色(默认黑)
int highlightIdx_ = -1; // I12 当前高亮异常下标(-1=无)
};
} // namespace geopro::app

View File

@ -0,0 +1,51 @@
#include "panels/chart/ContourSimplify.hpp"
#include <cmath>
namespace geopro::app {
namespace {
// 点 p 到线段 ab 的垂距ab 退化为点时取点距)。
double perpDistance(const geopro::core::Vec2& p, const geopro::core::Vec2& a,
const geopro::core::Vec2& b) {
const double dx = b.x - a.x, dy = b.y - a.y;
const double len2 = dx * dx + dy * dy;
if (len2 <= 0.0) return std::hypot(p.x - a.x, p.y - a.y);
// 叉积 / 段长 = 垂距。
const double cross = std::fabs((p.x - a.x) * dy - (p.y - a.y) * dx);
return cross / std::sqrt(len2);
}
// 递归对 [lo,hi] 区间抽稀,保留点索引置 keep[]。
void dpRecurse(const std::vector<geopro::core::Vec2>& pts, int lo, int hi, double tol,
std::vector<bool>& keep) {
if (hi <= lo + 1) return;
double maxD = -1.0;
int idx = lo;
for (int i = lo + 1; i < hi; ++i) {
const double d = perpDistance(pts[i], pts[lo], pts[hi]);
if (d > maxD) { maxD = d; idx = i; }
}
if (maxD > tol) {
keep[idx] = true;
dpRecurse(pts, lo, idx, tol, keep);
dpRecurse(pts, idx, hi, tol, keep);
}
}
} // namespace
std::vector<geopro::core::Vec2> douglasPeucker(const std::vector<geopro::core::Vec2>& pts,
double tol) {
if (tol <= 0.0 || pts.size() <= 2) return pts;
std::vector<bool> keep(pts.size(), false);
keep.front() = true;
keep.back() = true;
dpRecurse(pts, 0, static_cast<int>(pts.size()) - 1, tol, keep);
std::vector<geopro::core::Vec2> out;
out.reserve(pts.size());
for (std::size_t i = 0; i < pts.size(); ++i)
if (keep[i]) out.push_back(pts[i]);
return out;
}
} // namespace geopro::app

View File

@ -0,0 +1,14 @@
#pragma once
#include <vector>
#include "model/Anomaly.hpp" // core::Vec2
namespace geopro::app {
// 折线 Douglas-Peucker 抽稀(纯几何,无 Qt/VTK 依赖,便于单测)。
// tol<=0 或点数<=2 时原样返回(拷贝)。容差单位与点坐标一致(数据坐标)。
// I8「简化容差」滑块据此对等值线做点抽稀容差越大点越少、线越粗略
std::vector<geopro::core::Vec2> douglasPeucker(const std::vector<geopro::core::Vec2>& pts,
double tol);
} // namespace geopro::app

View File

@ -1,10 +1,17 @@
#include "panels/chart/DataTableView.hpp"
#include <utility>
#include <QCursor>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QPainter>
#include <QPushButton>
#include <QTableView>
#include <QToolTip>
#include <QVBoxLayout>
#include "panels/chart/InversionFormDialog.hpp"
#include "panels/chart/TablePager.hpp"
namespace geopro::app {
@ -111,6 +118,14 @@ DataTableView::DataTableView(QWidget* parent) : QWidget(parent) {
lay->setContentsMargins(0, 0, 0, 0);
lay->setSpacing(0);
// 顶部功能按钮行(默认隐藏;仅 dd_grid 载荷带 functionButtons 时显示)。右对齐贴近原版布局。
toolbar_ = new QWidget(this);
toolbarLay_ = new QHBoxLayout(toolbar_);
toolbarLay_->setContentsMargins(0, 0, 0, 8);
toolbarLay_->addStretch(1);
toolbar_->hide();
lay->addWidget(toolbar_);
model_ = new TablePayloadModel(this);
table_ = new QTableView(this);
table_->setModel(model_);
@ -162,6 +177,54 @@ void DataTableView::setPayload(const QVariant& payload) {
} else {
pager_->hide();
}
// 功能按钮行:按载荷 functionButtons 重建(空 → 隐藏;非 dd_grid 始终空)。
rebuildToolbar(t.functionButtons);
}
void DataTableView::setCommandRepo(geopro::data::IDatasetCommandRepository* repo,
std::function<QString()> dsIdGetter,
std::function<QString()> projectIdGetter) {
cmdRepo_ = repo;
dsIdGetter_ = std::move(dsIdGetter);
projectIdGetter_ = std::move(projectIdGetter);
}
void DataTableView::rebuildToolbar(const std::vector<geopro::core::TableFunctionButton>& buttons) {
// 清空旧按钮(保留末尾 addStretch逐项删 QPushButton
for (int i = toolbarLay_->count() - 1; i >= 0; --i) {
if (auto* w = toolbarLay_->itemAt(i)->widget()) {
toolbarLay_->removeWidget(w);
w->deleteLater();
}
}
// 仅渲染 enable 的按钮(原版 v-show="enable");全空/全禁用 → 隐藏整行。
int shown = 0;
for (const auto& b : buttons) {
if (!b.enable) continue;
auto* btn = new QPushButton(b.nameChn, toolbar_);
const QString code = b.code;
connect(btn, &QPushButton::clicked, this, [this, code] { onFunctionButton(code); });
toolbarLay_->addWidget(btn);
++shown;
}
toolbar_->setVisible(shown > 0);
}
void DataTableView::onFunctionButton(const QString& code) {
// 路由:原版 handleFunctionBtn 仅处理 inversion其余 code 无操作)。
if (code != QLatin1String("inversion")) return;
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) {
QToolTip::showText(QCursor::pos(), QStringLiteral("暂未实现"), this);
return;
}
// 反演运算:复用共享对话框 Mode::Inversion模型列表+动态表单+submitInversionTask
// 与原版 DdGrid 完全一致(同 InversionForm.vue + outerInversion 端点)。
InversionFormDialog dlg(InversionFormDialog::Mode::Inversion, cmdRepo_, dsId, projectId, this);
dlg.exec(); // 提交反馈由对话框内部处理;列表无需刷新(原版亦仅 Message.success 提示)。
}
} // namespace geopro::app

View File

@ -1,11 +1,20 @@
#pragma once
#include <functional>
#include <QAbstractTableModel>
#include <QStyledItemDelegate>
#include <QString>
#include <QWidget>
#include "model/detail/DetailPayloads.hpp"
#include "panels/chart/IDetailView.hpp"
class QTableView;
class QWidget;
class QHBoxLayout;
namespace geopro::data {
class IDatasetCommandRepository;
}
namespace geopro::app {
@ -48,6 +57,9 @@ class TablePager;
// 通用数据列表视图IDetailView + QTableView+ 分页型载荷时底部 TablePager 分页器)。
// measurement/grid/trajectory 列表共用。载荷 pageSize>0dd_grid时显示分页器并转发翻页请求
// 否则隐藏分页器(全量列表)。
// 顶部功能按钮行:仅当载荷 functionButtons 非空dd_grid时显示按钮文案/可见来自服务端
// functionList点 code=="inversion" → 弹反演动态表单对话框(复用 InversionFormDialog/Mode::Inversion
// 其余 Table 复用场景measurement 列表/trajectory/grfunctionButtons 为空 → 工具条隐藏,无任何变化。
class DataTableView : public QWidget, public IDetailView {
Q_OBJECT
public:
@ -56,14 +68,29 @@ public:
QWidget* widget() override { return this; }
void setPayload(const QVariant& payload) override; // 坏/空 variant → 保持空态不崩
// 注入反演命令仓储 + dsId/projectId 取值回调dd_grid「反演」功能按钮用
// 未注入时功能按钮仍渲染但点击退化为占位提示(与其它未接入视图一致)。
void setCommandRepo(geopro::data::IDatasetCommandRepository* repo,
std::function<QString()> dsIdGetter,
std::function<QString()> projectIdGetter);
signals:
// 分页器请求加载某页(翻页/跳页/改每页条数)。壳据此触发控制器 loadTabPaged。
void pageRequested(int pageNo, int pageSize);
private:
void rebuildToolbar(const std::vector<geopro::core::TableFunctionButton>& buttons);
void onFunctionButton(const QString& code); // 功能按钮路由(仅 inversion 起效)
QWidget* toolbar_; // 顶部功能按钮行容器functionButtons 空时隐藏)
QHBoxLayout* toolbarLay_; // 功能按钮布局(重建时清空重填)
QTableView* table_;
TablePayloadModel* model_;
TablePager* pager_; // 分页器pageSize>0 时显示,否则隐藏)
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
std::function<QString()> dsIdGetter_;
std::function<QString()> projectIdGetter_;
};
} // namespace geopro::app

View File

@ -14,18 +14,32 @@ namespace geopro::app {
std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget* parent,
geopro::data::IColorTemplateRepository* colorTplRepo,
std::function<QString()> projectIdGetter) {
std::function<QString()> projectIdGetter,
geopro::data::IDatasetCommandRepository* cmdRepo,
std::function<QString()> dsIdGetter) {
switch (kind) {
case controller::ViewKind::Scatter:
return std::unique_ptr<IDetailView>(new RawDataChartView(parent));
case controller::ViewKind::Scatter: {
auto* raw = new RawDataChartView(parent);
// 注入反演命令仓储 + dsId/projectId 取值回调measurement 反演运算/生成视电阻率)。
raw->setCommandRepo(cmdRepo, dsIdGetter, projectIdGetter);
return std::unique_ptr<IDetailView>(raw);
}
case controller::ViewKind::FilledContour: {
auto* grid = new GridDataChartView(parent);
// 注入色阶模板仓储 + projectId 取值回调(网格剖面「色阶配置」编辑器用)。
grid->setColorTemplateRepo(colorTplRepo, std::move(projectIdGetter));
grid->setColorTemplateRepo(colorTplRepo, projectIdGetter);
// 注入反演命令仓储 + dsId/projectId 取值回调(网格化/白化/滤波/另存为按钮)。
grid->setCommandRepo(cmdRepo, std::move(dsIdGetter), std::move(projectIdGetter));
return std::unique_ptr<IDetailView>(grid);
}
case controller::ViewKind::Table:
return std::unique_ptr<IDetailView>(new DataTableView(parent));
case controller::ViewKind::Table: {
auto* table = new DataTableView(parent);
// 注入反演命令仓储 + dsId/projectId 取值回调dd_grid「反演」功能按钮用
// 其余 Table 复用场景measurement 列表/trajectory/gr载荷无 functionButtons
// 工具条隐藏,注入的仓储不会被触达 → 无副作用。
table->setCommandRepo(cmdRepo, std::move(dsIdGetter), std::move(projectIdGetter));
return std::unique_ptr<IDetailView>(table);
}
case controller::ViewKind::Bar:
return std::unique_ptr<IDetailView>(new BarChartView(parent));
case controller::ViewKind::LineProfile:

View File

@ -10,6 +10,7 @@ class QWidget;
namespace geopro::data {
class IColorTemplateRepository;
class IDatasetCommandRepository;
}
namespace geopro::app {
@ -20,9 +21,12 @@ class IDetailView;
// Bar/LineProfile/PolylineMap/Table 留待后续阶段measurement/trajectory/grid
// 现阶段命中会抛 std::runtime_error明确失败而非静默空指针
// colorTplRepo/projectIdGetterFilledContour 视图的色阶模板仓储注入(可空 → 编辑器后端按钮禁用)。
// cmdRepo/dsIdGetterScatter 视图measurement反演运算/生成视电阻率命令仓储注入(可空 → 按钮占位提示)。
std::unique_ptr<IDetailView> makeDetailView(
controller::ViewKind kind, QWidget* parent,
geopro::data::IColorTemplateRepository* colorTplRepo = nullptr,
std::function<QString()> projectIdGetter = {});
std::function<QString()> projectIdGetter = {},
geopro::data::IDatasetCommandRepository* cmdRepo = nullptr,
std::function<QString()> dsIdGetter = {});
} // namespace geopro::app

View File

@ -0,0 +1,140 @@
#include "panels/chart/ExceptionDetailDialog.hpp"
#include <QColorDialog>
#include <QComboBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QJsonObject>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QPointer>
#include <QPushButton>
#include <QTableWidget>
#include <QVBoxLayout>
#include "FormKit.hpp"
#include "Theme.hpp"
#include "repo/IDatasetCommandRepository.hpp"
namespace geopro::app {
ExceptionDetailDialog::ExceptionDetailDialog(geopro::data::IDatasetCommandRepository* repo,
const geopro::core::Anomaly& anomaly, QWidget* parent)
: QDialog(parent), repo_(repo), anomaly_(anomaly) {
setWindowTitle(QStringLiteral("异常详情"));
setModal(true);
resize(420, 460);
lineColor_ = QString::fromStdString(anomaly_.lineColor);
if (lineColor_.isEmpty()) lineColor_ = QStringLiteral("#000000");
auto* root = formkit::dialogRoot(this);
auto* card = formkit::formCard(this);
auto* cardLay = formkit::cardBody(card);
auto* form = formkit::makeEditForm();
nameEdit_ = new QLineEdit(QString::fromStdString(anomaly_.name), this);
formkit::capField(nameEdit_);
form->addRow(formkit::editLabel(QStringLiteral("名称")), nameEdit_);
auto* typeLabel = new QLabel(QString::fromStdString(anomaly_.typeName), this);
form->addRow(formkit::editLabel(QStringLiteral("异常类型")), typeLabel);
// 图例:线色 / 线宽 / 线型(对照原版 legend.polyline*)。
colorBtn_ = new QPushButton(lineColor_, this);
colorBtn_->setStyleSheet(QStringLiteral("background:%1;").arg(lineColor_));
connect(colorBtn_, &QPushButton::clicked, this, [this]() {
const QColor c = QColorDialog::getColor(QColor(lineColor_), this, QStringLiteral("线色"));
if (c.isValid()) {
lineColor_ = c.name();
colorBtn_->setText(lineColor_);
colorBtn_->setStyleSheet(QStringLiteral("background:%1;").arg(lineColor_));
}
});
formkit::capField(colorBtn_);
form->addRow(formkit::editLabel(QStringLiteral("线色")), colorBtn_);
widthSpin_ = new QDoubleSpinBox(this);
widthSpin_->setRange(0.1, 20.0);
widthSpin_->setSingleStep(0.5);
widthSpin_->setValue(anomaly_.lineWidth > 0 ? anomaly_.lineWidth : 1.0);
formkit::capField(widthSpin_);
form->addRow(formkit::editLabel(QStringLiteral("线宽")), widthSpin_);
shapeCombo_ = new QComboBox(this);
shapeCombo_->addItem(QStringLiteral("实线"), QStringLiteral("solid"));
shapeCombo_->addItem(QStringLiteral("虚线"), QStringLiteral("dash"));
shapeCombo_->setCurrentIndex(anomaly_.dashed ? 1 : 0);
formkit::capField(shapeCombo_);
form->addRow(formkit::editLabel(QStringLiteral("线型")), shapeCombo_);
remarkEdit_ = new QPlainTextEdit(QString::fromStdString(anomaly_.remark), this);
remarkEdit_->setFixedHeight(geopro::app::scaledPx(60));
formkit::capField(remarkEdit_);
form->addRow(formkit::editLabel(QStringLiteral("备注")), remarkEdit_);
cardLay->addLayout(form);
root->addWidget(card);
// 坐标(只读展示,对照原版坐标信息 tab
root->addWidget(new QLabel(QStringLiteral("坐标:"), this));
auto* coordTable = new QTableWidget(static_cast<int>(anomaly_.localPts.size()), 2, this);
coordTable->setHorizontalHeaderLabels({QStringLiteral("x"), QStringLiteral("y")});
coordTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
coordTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
for (int r = 0; r < static_cast<int>(anomaly_.localPts.size()); ++r) {
coordTable->setItem(r, 0,
new QTableWidgetItem(QString::number(anomaly_.localPts[r].x, 'f', 7)));
coordTable->setItem(r, 1,
new QTableWidgetItem(QString::number(anomaly_.localPts[r].y, 'f', 7)));
}
root->addWidget(coordTable, 1);
auto* btnLay = new QHBoxLayout();
btnLay->addStretch();
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
okBtn_ = new QPushButton(QStringLiteral("确定"), this);
okBtn_->setDefault(true);
btnLay->addWidget(cancelBtn);
btnLay->addWidget(okBtn_);
root->addLayout(btnLay);
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
connect(okBtn_, &QPushButton::clicked, this, &ExceptionDetailDialog::onConfirm);
}
void ExceptionDetailDialog::onConfirm() {
if (!repo_ || anomaly_.id.empty()) { reject(); return; }
const QString name = nameEdit_->text().trimmed();
if (name.isEmpty()) {
QMessageBox::warning(this, windowTitle(), QStringLiteral("请输入名称"));
return;
}
// 原版详情抽屉「改名称/备注」走 PUT /business/exception 的局部更新,
// 仅发 {id, exceptionName, remark}(线样式是另一条独立 PUT且抽屉里样式控件 disabled
// 对齐原版 contourPage.vue onOk不合并/不重发 legend。
QJsonObject body{
{QStringLiteral("id"), QString::fromStdString(anomaly_.id)},
{QStringLiteral("exceptionName"), name},
{QStringLiteral("remark"), remarkEdit_->toPlainText()},
};
okBtn_->setEnabled(false);
QPointer<ExceptionDetailDialog> self(this);
repo_->updateException(body, [self](bool ok, QString msg) {
if (!self) return;
self->okBtn_->setEnabled(true);
if (ok) {
self->accept();
} else {
QMessageBox::warning(self, self->windowTitle(),
msg.isEmpty() ? QStringLiteral("更新失败") : msg);
}
});
}
} // namespace geopro::app

View File

@ -0,0 +1,44 @@
#pragma once
#include <QDialog>
#include <QString>
#include "model/Anomaly.hpp"
class QLineEdit;
class QPlainTextEdit;
class QDoubleSpinBox;
class QComboBox;
class QPushButton;
namespace geopro::data {
class IDatasetCommandRepository;
}
namespace geopro::app {
// 异常详情/编辑对话框I11复刻原版 drawerExceptionInfo 的可编辑部分):
// 名称(可编辑) / 异常类型(只读) / 图例样式(线色/线宽/线型) / 备注(可编辑) / 坐标(只读展示)。
// 确认 → updateException(PUT body {id, exceptionName, remark, legend:{polylineColor,
// polylineWidth, polylineShape}}),成功 accept(),调用方随后 reloadGrid。
class ExceptionDetailDialog : public QDialog {
Q_OBJECT
public:
ExceptionDetailDialog(geopro::data::IDatasetCommandRepository* repo,
const geopro::core::Anomaly& anomaly, QWidget* parent = nullptr);
private:
void onConfirm();
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
geopro::core::Anomaly anomaly_; // 拷贝(不可变范式:编辑后组装新 body不改原对象
QLineEdit* nameEdit_ = nullptr;
QPlainTextEdit* remarkEdit_ = nullptr;
QPushButton* colorBtn_ = nullptr; // 线色选择(弹 QColorDialog
QString lineColor_; // 当前线色 hex
QDoubleSpinBox* widthSpin_ = nullptr;
QComboBox* shapeCombo_ = nullptr; // solid / dash
QPushButton* okBtn_ = nullptr;
};
} // namespace geopro::app

View File

@ -0,0 +1,214 @@
#include "panels/chart/ExceptionDialog.hpp"
#include <utility>
#include <QComboBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QJsonArray>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QPointer>
#include <QPushButton>
#include <QTableWidget>
#include <QVBoxLayout>
#include "FormKit.hpp"
#include "Theme.hpp"
#include "repo/IDatasetCommandRepository.hpp"
namespace geopro::app {
namespace {
// 标注类型 → 最少坐标点数(点/文字=1线≥2面≥3
int minPoints(const QString& markType) {
if (markType == QStringLiteral("2")) return 2;
if (markType == QStringLiteral("3")) return 3;
return 1; // 点("1")/文字("4")
}
} // namespace
ExceptionDialog::ExceptionDialog(geopro::data::IDatasetCommandRepository* repo, QString projectId,
QString remarkSourceId, QWidget* parent)
: QDialog(parent),
repo_(repo),
projectId_(std::move(projectId)),
remarkSourceId_(std::move(remarkSourceId)) {
setWindowTitle(QStringLiteral("新建异常"));
setModal(true);
resize(440, 480);
auto* root = formkit::dialogRoot(this);
auto* card = formkit::formCard(this);
auto* cardLay = formkit::cardBody(card);
auto* form = formkit::makeEditForm();
// 标注类型remarkSourceType "1".."4",与原版 annotationType 一致)。
markTypeCombo_ = new QComboBox(this);
markTypeCombo_->addItem(QStringLiteral(""), QStringLiteral("1"));
markTypeCombo_->addItem(QStringLiteral("线"), QStringLiteral("2"));
markTypeCombo_->addItem(QStringLiteral(""), QStringLiteral("3"));
markTypeCombo_->addItem(QStringLiteral("文字"), QStringLiteral("4"));
formkit::capField(markTypeCombo_);
form->addRow(formkit::editLabel(QStringLiteral("标注类型")), markTypeCombo_);
exceptionTypeCombo_ = new QComboBox(this);
formkit::capField(exceptionTypeCombo_);
form->addRow(formkit::editLabel(QStringLiteral("异常类型")), exceptionTypeCombo_);
nameEdit_ = new QLineEdit(this);
nameEdit_->setPlaceholderText(QStringLiteral("数据名称+异常类型代号+序号"));
formkit::capField(nameEdit_);
form->addRow(formkit::editLabel(QStringLiteral("名称")), nameEdit_);
remarkEdit_ = new QPlainTextEdit(this);
remarkEdit_->setFixedHeight(geopro::app::scaledPx(60));
formkit::capField(remarkEdit_);
form->addRow(formkit::editLabel(QStringLiteral("备注")), remarkEdit_);
cardLay->addLayout(form);
root->addWidget(card);
// 坐标表x/y 多行),下方加/减行按钮。
root->addWidget(new QLabel(QStringLiteral("坐标xy"), this));
coordTable_ = new QTableWidget(0, 2, this);
coordTable_->setHorizontalHeaderLabels({QStringLiteral("x"), QStringLiteral("y")});
coordTable_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
root->addWidget(coordTable_, 1);
auto* rowBtns = new QHBoxLayout();
auto* addRow = new QPushButton(QStringLiteral("加一行"), this);
auto* delRow = new QPushButton(QStringLiteral("删一行"), this);
rowBtns->addWidget(addRow);
rowBtns->addWidget(delRow);
rowBtns->addStretch();
root->addLayout(rowBtns);
connect(addRow, &QPushButton::clicked, this,
[this]() { coordTable_->insertRow(coordTable_->rowCount()); });
connect(delRow, &QPushButton::clicked, this, [this]() {
if (coordTable_->rowCount() > 0) coordTable_->removeRow(coordTable_->rowCount() - 1);
});
auto* btnLay = new QHBoxLayout();
btnLay->addStretch();
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
okBtn_ = new QPushButton(QStringLiteral("确定"), this);
okBtn_->setDefault(true);
btnLay->addWidget(cancelBtn);
btnLay->addWidget(okBtn_);
root->addLayout(btnLay);
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
connect(okBtn_, &QPushButton::clicked, this, &ExceptionDialog::onConfirm);
connect(markTypeCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this](int) { onTypeChanged(); });
connect(exceptionTypeCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this](int) { suggestName(); });
onTypeChanged(); // 初始:拉首个类型的异常类型列表 + 铺最少坐标行
}
QString ExceptionDialog::markTypeValue() const {
return markTypeCombo_->currentData().toString();
}
void ExceptionDialog::onTypeChanged() {
// 调整坐标表行数到该形态最少点数(不足则补行;已多则保留)。
const int need = minPoints(markTypeValue());
while (coordTable_->rowCount() < need) coordTable_->insertRow(coordTable_->rowCount());
loadExceptionTypes();
}
void ExceptionDialog::loadExceptionTypes() {
if (!repo_) return;
QPointer<ExceptionDialog> self(this);
repo_->listExceptionTypes(
projectId_, markTypeValue(), [self](bool ok, QJsonArray list, QString) {
if (!self || !ok) return;
self->exceptionTypeCombo_->clear();
for (const QJsonValue& v : list) {
const QJsonObject o = v.toObject();
// 兼容 {label,value} 与 {exceptionTypeName,id} 两种返回形态。
QString label = o.value(QStringLiteral("label")).toString();
if (label.isEmpty()) label = o.value(QStringLiteral("exceptionTypeName")).toString();
QString id = o.value(QStringLiteral("value")).toString();
if (id.isEmpty()) id = o.value(QStringLiteral("id")).toString();
if (!id.isEmpty()) self->exceptionTypeCombo_->addItem(label, id);
}
if (self->exceptionTypeCombo_->count() > 0) self->suggestName();
});
}
void ExceptionDialog::suggestName() {
if (!repo_) return;
const QString typeId = exceptionTypeCombo_->currentData().toString();
if (typeId.isEmpty()) return;
QPointer<ExceptionDialog> self(this);
repo_->getExceptionName(typeId, remarkSourceId_, [self](bool ok, QJsonObject data, QString) {
if (!self || !ok) return;
// 仅当用户未手填时回填建议名(避免覆盖)。
if (!self->nameEdit_->text().trimmed().isEmpty()) return;
QString name = data.value(QStringLiteral("exceptionName")).toString();
if (name.isEmpty()) name = data.value(QStringLiteral("name")).toString();
self->nameEdit_->setText(name);
});
}
void ExceptionDialog::onConfirm() {
if (!repo_) { reject(); return; }
const QString name = nameEdit_->text().trimmed();
const QString typeId = exceptionTypeCombo_->currentData().toString();
if (name.isEmpty()) {
QMessageBox::warning(this, windowTitle(), QStringLiteral("请输入名称"));
return;
}
if (typeId.isEmpty()) {
QMessageBox::warning(this, windowTitle(), QStringLiteral("请选择异常类型"));
return;
}
// 收集坐标(跳过空行)。
QJsonArray coords;
for (int r = 0; r < coordTable_->rowCount(); ++r) {
auto* ix = coordTable_->item(r, 0);
auto* iy = coordTable_->item(r, 1);
if (!ix || !iy || ix->text().trimmed().isEmpty() || iy->text().trimmed().isEmpty()) continue;
bool okx = false, oky = false;
const double x = ix->text().toDouble(&okx);
const double y = iy->text().toDouble(&oky);
if (!okx || !oky) continue;
coords.append(QJsonObject{{QStringLiteral("x"), x}, {QStringLiteral("y"), y}});
}
if (coords.size() < minPoints(markTypeValue())) {
QMessageBox::warning(this, windowTitle(),
QStringLiteral("坐标点数不足(点/文字≥1线≥2面≥3"));
return;
}
QJsonObject body{
{QStringLiteral("exceptionName"), name},
{QStringLiteral("exceptionTypeId"), typeId},
{QStringLiteral("remark"), remarkEdit_->toPlainText()},
{QStringLiteral("remarkSourceType"), markTypeValue()}, // 几何形态字符串
{QStringLiteral("remarkSourceId"), remarkSourceId_}, // = dsObjectId
{QStringLiteral("projectId"), projectId_},
{QStringLiteral("location"), QJsonObject{{QStringLiteral("coordinate"), coords}}},
};
okBtn_->setEnabled(false);
QPointer<ExceptionDialog> self(this);
repo_->newException(body, [self](bool ok, QString msg) {
if (!self) return;
self->okBtn_->setEnabled(true);
if (ok) {
self->accept();
} else {
QMessageBox::warning(self, self->windowTitle(),
msg.isEmpty() ? QStringLiteral("创建失败") : msg);
}
});
}
} // namespace geopro::app

View File

@ -0,0 +1,51 @@
#pragma once
#include <functional>
#include <QDialog>
#include <QJsonObject>
#include <QString>
class QComboBox;
class QLineEdit;
class QPlainTextEdit;
class QTableWidget;
class QPushButton;
namespace geopro::data {
class IDatasetCommandRepository;
}
namespace geopro::app {
// 异常创建对话框I9复刻原版 exceptionDialog + contourPage 保存链路):
// 标注类型(点/线/面/文字 → remarkSourceType "1"/"2"/"3"/"4") + 异常类型(listExceptionTypes)
// + 名称(getExceptionName 建议) + 备注 + 坐标表。
// 确认 → newException(body),成功 accept(),调用方随后 reloadGrid。
// 说明:原版「图上交互式绘制几何」在 Qwt 成本高本实现以坐标表x/y 多行)采集 location
// 覆盖点(1 行)/线(≥2)/面(≥3)/文字(1) 全形态打通完整创建链路on-chart 拖拽绘制为后置项。
class ExceptionDialog : public QDialog {
Q_OBJECT
public:
ExceptionDialog(geopro::data::IDatasetCommandRepository* repo, QString projectId,
QString remarkSourceId, QWidget* parent = nullptr);
private:
void onTypeChanged(); // 标注类型变 → 重拉异常类型列表 + 调整坐标表最少行
void loadExceptionTypes(); // listExceptionTypes(projectId, remarkSourceType)
void suggestName(); // getExceptionName(exceptionTypeId, remarkSourceId) → 名称建议
void onConfirm(); // 校验 → newException
QString markTypeValue() const; // 当前标注类型字符串("1".."4")
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
QString projectId_;
QString remarkSourceId_;
QComboBox* markTypeCombo_ = nullptr; // userData = "1".."4"
QComboBox* exceptionTypeCombo_ = nullptr; // userData = 异常类型 id
QLineEdit* nameEdit_ = nullptr;
QPlainTextEdit* remarkEdit_ = nullptr;
QTableWidget* coordTable_ = nullptr;
QPushButton* okBtn_ = nullptr;
};
} // namespace geopro::app

View File

@ -0,0 +1,342 @@
#include "panels/chart/FilterDialog.hpp"
#include <utility>
#include <QComboBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QHash>
#include <QInputDialog>
#include <QJsonArray>
#include <QJsonDocument>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPointer>
#include <QPushButton>
#include <QSpinBox>
#include <QTableWidget>
#include <QTreeWidget>
#include <QVBoxLayout>
#include "FormKit.hpp"
#include "panels/chart/InversionProcessOps.hpp" // buildFilterApplyBody / buildNewFilterBody
#include "repo/IDatasetCommandRepository.hpp"
namespace geopro::app {
namespace {
constexpr double kFillRange = 1e9;
constexpr int kMatrixMin = 1, kMatrixMax = 21; // 矩阵行列范围(对照原版 1~21
constexpr int kDefaultDim = 3;
const char kDefaultCustomKey[] = "default-custom-filter"; // 默认自定义滤波器(不可删)
const char kCustomGroupName[] = "自定义滤波器";
// 单元格读数(空/非法 → 0
double cellValue(const QTableWidgetItem* it) {
if (!it) return 0.0;
bool ok = false;
const double v = it->text().toDouble(&ok);
return ok ? v : 0.0;
}
} // namespace
FilterDialog::FilterDialog(geopro::data::IDatasetCommandRepository* repo, QString dsId,
QString projectId, QWidget* parent)
: QDialog(parent), repo_(repo), dsId_(std::move(dsId)), projectId_(std::move(projectId)) {
setWindowTitle(QStringLiteral("滤波处理"));
setModal(true);
resize(820, 520);
auto* root = formkit::dialogRoot(this);
auto* body = new QHBoxLayout();
root->addLayout(body, 1);
// ── 左:滤波器树 + 增删按钮 ─────────────────────────────────────────
auto* leftLay = new QVBoxLayout();
tree_ = new QTreeWidget(this);
tree_->setHeaderHidden(true);
leftLay->addWidget(tree_, 1);
auto* treeBtnLay = new QHBoxLayout();
auto* addBtn = new QPushButton(QStringLiteral("另存为"), this);
auto* delBtn = new QPushButton(QStringLiteral("删除"), this);
treeBtnLay->addWidget(addBtn);
treeBtnLay->addWidget(delBtn);
leftLay->addLayout(treeBtnLay);
body->addLayout(leftLay, 1);
// ── 右:配置面板 ────────────────────────────────────────────────────
auto* rightLay = new QVBoxLayout();
auto* form = formkit::makeEditForm();
dataEdge_ = new QComboBox(this);
dataEdge_->addItem(QStringLiteral("设为无效点"), QStringLiteral("whitening"));
dataEdge_->addItem(QStringLiteral("忽略"), QStringLiteral("skip"));
dataEdge_->addItem(QStringLiteral("复制边缘点"), QStringLiteral("edgePoint"));
dataEdge_->addItem(QStringLiteral("填充"), QStringLiteral("filling"));
dataEdgeValue_ = new QLineEdit(this);
dataEdgeValue_->setEnabled(false);
noDataPoints_ = new QComboBox(this);
noDataPoints_->addItem(QStringLiteral("扩展"), QStringLiteral("expansion"));
noDataPoints_->addItem(QStringLiteral("保留"), QStringLiteral("retain"));
noDataPoints_->addItem(QStringLiteral("跳过"), QStringLiteral("skip"));
noDataPoints_->addItem(QStringLiteral("填充"), QStringLiteral("filling"));
noDataPoints_->setCurrentIndex(3); // 默认填充(对照原版)
noDataValue_ = new QLineEdit(this);
filterTimes_ = new QSpinBox(this);
filterTimes_->setRange(1, 10);
rows_ = new QSpinBox(this);
rows_->setRange(kMatrixMin, kMatrixMax);
rows_->setValue(kDefaultDim);
cols_ = new QSpinBox(this);
cols_->setRange(kMatrixMin, kMatrixMax);
cols_->setValue(kDefaultDim);
formkit::capField(dataEdge_);
formkit::capField(dataEdgeValue_);
formkit::capField(noDataPoints_);
formkit::capField(noDataValue_);
formkit::capField(filterTimes_);
formkit::capField(rows_);
formkit::capField(cols_);
form->addRow(formkit::editLabel(QStringLiteral("数据边缘:")), dataEdge_);
form->addRow(formkit::editLabel(QStringLiteral("数据边缘值:")), dataEdgeValue_);
form->addRow(formkit::editLabel(QStringLiteral("无数据点:")), noDataPoints_);
form->addRow(formkit::editLabel(QStringLiteral("无数据点值:")), noDataValue_);
form->addRow(formkit::editLabel(QStringLiteral("滤波次数:")), filterTimes_);
form->addRow(formkit::editLabel(QStringLiteral("行:")), rows_);
form->addRow(formkit::editLabel(QStringLiteral("列:")), cols_);
rightLay->addLayout(form);
matrix_ = new QTableWidget(kDefaultDim, kDefaultDim, this);
matrix_->horizontalHeader()->setVisible(false);
matrix_->verticalHeader()->setVisible(false);
rightLay->addWidget(matrix_, 1);
body->addLayout(rightLay, 2);
// 底部按钮。
auto* btnLay = new QHBoxLayout();
btnLay->addStretch();
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
okBtn_ = new QPushButton(QStringLiteral("应用"), this);
okBtn_->setDefault(true);
btnLay->addWidget(cancelBtn);
btnLay->addWidget(okBtn_);
root->addLayout(btnLay);
resizeMatrix(); // 默认 3x3 中心 1
if (auto* c = matrix_->item(1, 1)) c->setText(QStringLiteral("1"));
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
connect(okBtn_, &QPushButton::clicked, this, &FilterDialog::onConfirm);
connect(addBtn, &QPushButton::clicked, this, &FilterDialog::saveCustomFilter);
connect(delBtn, &QPushButton::clicked, this, &FilterDialog::deleteSelectedFilter);
connect(tree_, &QTreeWidget::itemSelectionChanged, this,
&FilterDialog::onTreeSelectionChanged);
connect(rows_, QOverload<int>::of(&QSpinBox::valueChanged), this,
[this](int) { resizeMatrix(); });
connect(cols_, QOverload<int>::of(&QSpinBox::valueChanged), this,
[this](int) { resizeMatrix(); });
// 数据边缘/无数据点:仅「填充」启用对应值输入框(对照原版 v-if=filling
connect(dataEdge_, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int) {
dataEdgeValue_->setEnabled(dataEdge_->currentData().toString() ==
QStringLiteral("filling"));
});
connect(noDataPoints_, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int) {
noDataValue_->setEnabled(noDataPoints_->currentData().toString() ==
QStringLiteral("filling"));
});
noDataValue_->setEnabled(true); // 默认填充
loadFilters();
}
void FilterDialog::loadFilters() {
if (!repo_) return;
QPointer<FilterDialog> self(this);
repo_->listFilters([self](bool ok, QJsonArray list, QString) {
if (!self || !ok) return;
self->flatItems_.clear();
for (const QJsonValue& v : list) self->flatItems_.push_back(v.toObject());
self->buildTree();
});
}
void FilterDialog::buildTree() {
tree_->clear();
// 按 id 索引节点,先建全部节点再挂父子(扁平 → 树,对照原版 buildTreeData
QHash<QString, QTreeWidgetItem*> byId;
QHash<QString, QString> parentOf;
for (const auto& o : flatItems_) {
const QString id = o.value(QStringLiteral("id")).toString();
const QString name = o.value(QStringLiteral("name")).toString();
auto* item = new QTreeWidgetItem(QStringList{name});
item->setData(0, Qt::UserRole, o.value(QStringLiteral("id")).toString());
item->setData(0, Qt::UserRole + 1, QString::fromUtf8(QJsonDocument(o).toJson()));
// 叶节点(含矩阵 rowColumValue才可选中应用。
const bool selectable = !o.value(QStringLiteral("rowColumValue")).isNull() &&
o.contains(QStringLiteral("rowColumValue"));
if (!selectable) item->setFlags(item->flags() & ~Qt::ItemIsSelectable);
byId.insert(id, item);
parentOf.insert(id, o.value(QStringLiteral("parentId")).toString());
}
for (auto it = byId.begin(); it != byId.end(); ++it) {
const QString pid = parentOf.value(it.key());
if (!pid.isEmpty() && byId.contains(pid))
byId.value(pid)->addChild(it.value());
else
tree_->addTopLevelItem(it.value());
}
tree_->expandAll();
}
void FilterDialog::onTreeSelectionChanged() {
auto* item = tree_->currentItem();
if (!item) return;
const QByteArray raw = item->data(0, Qt::UserRole + 1).toString().toUtf8();
const QJsonObject o = QJsonDocument::fromJson(raw).object();
const QJsonObject rc = o.value(QStringLiteral("rowColumValue")).toObject();
const QJsonArray form = rc.value(QStringLiteral("form")).toArray();
if (form.isEmpty()) return;
// 回填矩阵 + 行列。
const int r = form.size();
const int c = form.at(0).toArray().size();
rows_->setValue(r);
cols_->setValue(c); // 触发 resizeMatrix
for (int i = 0; i < r; ++i) {
const QJsonArray row = form.at(i).toArray();
for (int j = 0; j < c && j < row.size(); ++j)
if (auto* cell = matrix_->item(i, j))
cell->setText(QString::number(row.at(j).toDouble()));
}
// 回填配置存在则用缺省保持当前。code 1..N 直接映射到下拉项 0..N-1。
auto setCombo = [](QComboBox* combo, int code) {
if (code >= 1 && code <= combo->count()) combo->setCurrentIndex(code - 1);
};
if (o.contains(QStringLiteral("boundary")))
setCombo(dataEdge_, o.value(QStringLiteral("boundary")).toInt());
if (o.contains(QStringLiteral("noDataPoints")))
setCombo(noDataPoints_, o.value(QStringLiteral("noDataPoints")).toInt());
if (o.contains(QStringLiteral("number")))
filterTimes_->setValue(o.value(QStringLiteral("number")).toInt());
}
void FilterDialog::resizeMatrix() {
const int r = rows_->value();
const int c = cols_->value();
// 保留重叠区旧值(对照原版 updateFilterMatrix 保留 + 补 0
std::vector<std::vector<double>> old = readMatrix();
matrix_->setRowCount(r);
matrix_->setColumnCount(c);
for (int i = 0; i < r; ++i)
for (int j = 0; j < c; ++j) {
auto* it = matrix_->item(i, j);
if (!it) {
it = new QTableWidgetItem();
matrix_->setItem(i, j, it);
}
const double v = (i < static_cast<int>(old.size()) &&
j < static_cast<int>(old[i].size()))
? old[i][j]
: 0.0;
it->setText(QString::number(v));
}
}
std::vector<std::vector<double>> FilterDialog::readMatrix() const {
std::vector<std::vector<double>> out;
for (int i = 0; i < matrix_->rowCount(); ++i) {
std::vector<double> row;
for (int j = 0; j < matrix_->columnCount(); ++j) row.push_back(cellValue(matrix_->item(i, j)));
out.push_back(std::move(row));
}
return out;
}
QString FilterDialog::selectedFilterName() const {
auto* item = tree_->currentItem();
return item ? item->text(0) : QString();
}
QString FilterDialog::customGroupParentId() const {
// 找名为「自定义滤波器」的分组节点 idnewFilter parentId
for (const auto& o : flatItems_)
if (o.value(QStringLiteral("name")).toString() == QLatin1String(kCustomGroupName))
return o.value(QStringLiteral("id")).toString();
return QString();
}
void FilterDialog::saveCustomFilter() {
if (!repo_) return;
const QString parentId = customGroupParentId();
if (parentId.isEmpty()) {
QMessageBox::warning(this, windowTitle(), QStringLiteral("未找到自定义滤波器分组"));
return;
}
bool ok = false;
const QString name = QInputDialog::getText(this, QStringLiteral("另存为新自定义滤波器"),
QStringLiteral("名称:"), QLineEdit::Normal,
QStringLiteral("自定义滤波器1"), &ok);
if (!ok || name.trimmed().isEmpty()) return;
QPointer<FilterDialog> self(this);
repo_->newFilter(buildNewFilterBody(name.trimmed(), projectId_, parentId, readMatrix()),
[self](bool ok2, QString msg) {
if (!self) return;
if (ok2)
self->loadFilters(); // 刷新树
else
QMessageBox::warning(self, self->windowTitle(),
msg.isEmpty() ? QStringLiteral("保存失败") : msg);
});
}
void FilterDialog::deleteSelectedFilter() {
if (!repo_) return;
auto* item = tree_->currentItem();
if (!item) return;
const QString id = item->data(0, Qt::UserRole).toString();
if (id.isEmpty() || id == QLatin1String(kDefaultCustomKey)) {
QMessageBox::warning(this, windowTitle(), QStringLiteral("该滤波器不可删除"));
return;
}
if (QMessageBox::question(this, windowTitle(), QStringLiteral("确认删除该滤波器?")) !=
QMessageBox::Yes)
return;
QPointer<FilterDialog> self(this);
repo_->deleteFilter(id, [self](bool ok, QString msg) {
if (!self) return;
if (ok)
self->loadFilters();
else
QMessageBox::warning(self, self->windowTitle(),
msg.isEmpty() ? QStringLiteral("删除失败") : msg);
});
}
void FilterDialog::onConfirm() {
if (!repo_) { reject(); return; }
FilterApplyParams p;
p.dsObjectId = dsId_;
p.dataEdge = dataEdge_->currentData().toString();
p.dataEdgeValue = dataEdgeValue_->text().toDouble();
p.noDataPoints = noDataPoints_->currentData().toString();
p.noDataValue = noDataValue_->text().toDouble();
p.number = filterTimes_->value();
p.row = rows_->value();
p.column = cols_->value();
p.matrix = readMatrix();
p.filteringMethod = selectedFilterName();
okBtn_->setEnabled(false);
QPointer<FilterDialog> self(this);
repo_->applyFilter(buildFilterApplyBody(p), [self](bool ok, QString msg) {
if (!self) return;
self->okBtn_->setEnabled(true);
if (ok) {
self->accept();
} else {
QMessageBox::warning(self, self->windowTitle(),
msg.isEmpty() ? QStringLiteral("滤波处理失败") : msg);
}
});
}
} // namespace geopro::app

View File

@ -0,0 +1,64 @@
#pragma once
#include <vector>
#include <QDialog>
#include <QJsonObject>
#include <QString>
class QComboBox;
class QDoubleSpinBox;
class QSpinBox;
class QPushButton;
class QTreeWidget;
class QTreeWidgetItem;
class QTableWidget;
class QLineEdit;
namespace geopro::data {
class IDatasetCommandRepository;
}
namespace geopro::app {
// 滤波处理对话框I41:1 复刻原版 FilterDialog
// 左滤波器树listFilters按 parentId 建树,叶节点可选)+ 自定义滤波器增删。
// 右:数据边缘 / 无数据点 / 滤波次数 / 矩阵行列 + 矩阵编辑表。
// 确认 → applyFilter成功 accept(),调用方随后重载网格重绘。
class FilterDialog : public QDialog {
Q_OBJECT
public:
FilterDialog(geopro::data::IDatasetCommandRepository* repo, QString dsId, QString projectId,
QWidget* parent = nullptr);
private:
void loadFilters(); // 拉滤波器树
void buildTree(); // 由 flatItems_ 建树
void onTreeSelectionChanged(); // 选中叶节点 → 右侧回填
void resizeMatrix(); // 行/列变更 → 重建矩阵表(保留重叠值)
std::vector<std::vector<double>> readMatrix() const; // 读当前矩阵表
void saveCustomFilter(); // 另存为新自定义滤波器newFilter
void deleteSelectedFilter(); // 删除选中自定义滤波器deleteFilter
void onConfirm(); // 应用 → applyFilter
QString selectedFilterName() const; // 选中节点名filteringMethod 字段)
QString customGroupParentId() const; // 「自定义滤波器」分组节点 idnewFilter parentId
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
QString dsId_;
QString projectId_;
QTreeWidget* tree_ = nullptr;
std::vector<QJsonObject> flatItems_; // listFilters 原始扁平项(建树用)
QComboBox* dataEdge_ = nullptr;
QLineEdit* dataEdgeValue_ = nullptr;
QComboBox* noDataPoints_ = nullptr;
QLineEdit* noDataValue_ = nullptr;
QSpinBox* filterTimes_ = nullptr;
QSpinBox* rows_ = nullptr;
QSpinBox* cols_ = nullptr;
QTableWidget* matrix_ = nullptr;
QPushButton* okBtn_ = nullptr;
};
} // namespace geopro::app

View File

@ -3,11 +3,18 @@
#include <utility>
#include <QCheckBox>
#include <QCursor>
#include <QHBoxLayout>
#include <QJsonArray>
#include <QJsonObject>
#include <QMessageBox>
#include <QPointer>
#include <QSignalBlocker>
#include <QLabel>
#include <QSlider>
#include <QTimer>
#include <QToolButton>
#include <QToolTip>
#include <QVBoxLayout>
#include <qwt_plot.h>
@ -23,10 +30,19 @@
#include "panels/AnomalyTablePanel.hpp"
#include "panels/DescriptionPanel.hpp"
#include "panels/chart/ChartTheme.hpp"
#include "panels/chart/AutoAnnotationDialog.hpp"
#include "panels/chart/ColorBarWidget.hpp"
#include "panels/chart/ColorMapService.hpp"
#include "panels/chart/ContourHoverTip.hpp"
#include "panels/chart/ContourPlotItem.hpp"
#include "panels/chart/ExceptionDetailDialog.hpp"
#include "panels/chart/ExceptionDialog.hpp"
#include "panels/chart/FilterDialog.hpp"
#include "panels/chart/GridWizardDialog.hpp"
#include "panels/chart/LivePanner.hpp"
#include "panels/chart/SaveAsDialog.hpp"
#include "panels/chart/WhiteningDialog.hpp"
#include "repo/IDatasetCommandRepository.hpp"
namespace geopro::app {
@ -65,16 +81,23 @@ GridDataChartView::GridDataChartView(QWidget* parent) : QWidget(parent) {
auto* lblSimplify = new QLabel(QStringLiteral("简化容差:"), toolbar);
// 简化容差:对照原版 a-slider min0 max2 step0.1,默认 0.5。客户端用整数滑块 0~20 映射 /10。
simplifySlider_ = new QSlider(Qt::Horizontal, toolbar);
simplifySlider_->setRange(0, 100);
simplifySlider_->setValue(50);
simplifySlider_->setRange(0, 20);
simplifySlider_->setValue(5); // 0.5
simplifySlider_->setFixedWidth(80);
simplifyValueLabel_ = new QLabel(QStringLiteral("0.5"), toolbar);
simplifyValueLabel_->setFixedWidth(28);
// I8 防抖(~300ms对照原版 300ms 节流):滑动只改标签,停下后真重算等值线简化。
simplifyDebounce_ = new QTimer(this);
simplifyDebounce_->setSingleShot(true);
simplifyDebounce_->setInterval(300);
connect(simplifyDebounce_, &QTimer::timeout, this, [this]() { applySimplify(); });
connect(simplifySlider_, &QSlider::valueChanged, this, [this](int v) {
simplifyValueLabel_->setText(QString::number(v / 100.0, 'f', 1));
simplifyValueLabel_->setText(QString::number(v / 10.0, 'f', 1));
simplifyDebounce_->start();
});
auto* btnAnomalyLabel = new QToolButton(toolbar);
@ -174,6 +197,33 @@ GridDataChartView::GridDataChartView(QWidget* parent) : QWidget(parent) {
connect(btnColorScale, &QToolButton::clicked, this,
[this]() { openColorScaleEditor(); });
// 处理类按钮(网格化/白化/滤波/另存为):成功后重载网格重绘。
connect(btnGrid, &QToolButton::clicked, this, [this]() { openGridWizard(); });
connect(btnWhiten, &QToolButton::clicked, this, [this]() { openWhitening(); });
connect(btnFilter, &QToolButton::clicked, this, [this]() { openFilter(); });
connect(btnSaveAs, &QToolButton::clicked, this, [this]() { openSaveAs(); });
// 异常标注 / 自动标注I9 / I13
connect(btnAnomalyLabel, &QToolButton::clicked, this, [this]() { openExceptionDialog(); });
connect(btnAutoLabel, &QToolButton::clicked, this, [this]() { openAutoAnnotation(); });
// 异常表操作列 → 删除/详情/定位I10 / I11 / I12
connect(anomalyTable_, &AnomalyTablePanel::deleteRequested, this,
[this](int i) { deleteAnomaly(i); });
connect(anomalyTable_, &AnomalyTablePanel::detailRequested, this,
[this](int i) { showAnomalyDetail(i); });
connect(anomalyTable_, &AnomalyTablePanel::locateRequested, this,
[this](int i) { locateAnomaly(i); });
// 描述保存I14
connect(descriptionPanel_, &DescriptionPanel::saveRequested, this,
[this](const QString& t) { saveDescription(t); });
// I7 显示等值线提示信息hover tooltip 显隐(本地,挂画布事件过滤器)。
contourTip_ = new ContourHoverTip(plot_, QwtPlot::xBottom, QwtPlot::yLeft, this);
connect(chkContourTip, &QCheckBox::toggled, this,
[this](bool on) { if (contourTip_) contourTip_->setEnabled(on); });
// 主题配色:当前主题套一次 + 监听切换热更新。
applyChartPlotTheme(plot_);
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, plot_,
@ -230,6 +280,9 @@ void GridDataChartView::rebuildContour() {
contourItem_->setLineColor(lineCfg_.lineColor); // 线形⚙ 配置
contourItem_->setLineDashed(lineCfg_.dashed);
contourItem_->setLabelColor(lineCfg_.labelColor);
// I8 应用当前简化容差I7 把等值线提示绑定到新建项。
if (simplifySlider_) contourItem_->setSimplifyTolerance(simplifySlider_->value() / 10.0);
if (contourTip_) contourTip_->setItem(contourItem_);
contourItem_->attach(plot_);
// 轴范围 = 数据范围x=距离、y=深度/高程)。
@ -274,4 +327,186 @@ void GridDataChartView::openColorScaleEditor() {
colorBar_->setColorScale(gridScale_);
}
void GridDataChartView::setCommandRepo(geopro::data::IDatasetCommandRepository* repo,
std::function<QString()> dsIdGetter,
std::function<QString()> projectIdGetter) {
cmdRepo_ = repo;
dsIdGetter_ = std::move(dsIdGetter);
// projectId 取值回调与色阶编辑器共用(色阶注入可能先到,二者一致)。
if (projectIdGetter) projectIdGetter_ = std::move(projectIdGetter);
// I14有仓储 + dsId 时启用描述保存并回填一次。
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (descriptionPanel_) descriptionPanel_->setSaveEnabled(cmdRepo_ && !dsId.isEmpty());
loadDescription();
}
void GridDataChartView::showNotImplemented(QWidget* anchor) {
const QPoint pos = anchor ? anchor->mapToGlobal(QPoint(0, anchor->height())) : QCursor::pos();
QToolTip::showText(pos, QStringLiteral("暂未实现"), anchor);
}
void GridDataChartView::reloadGrid() {
// 处理类操作成功后:只读重载网格 → setGridData 重绘(与 measurement V 值重载同范式)。
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) return;
QPointer<GridDataChartView> self(this);
cmdRepo_->loadInversionGrid(
dsId, [self](bool ok, geopro::core::ContourPayload p, QString msg) {
if (!self) return;
if (!ok) {
QMessageBox::warning(self, QStringLiteral("提示"),
msg.isEmpty() ? QStringLiteral("重载失败") : msg);
return;
}
self->setGridData(p.grid, p.scale, p.anomalies);
});
}
void GridDataChartView::openGridWizard() {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
GridWizardDialog dlg(cmdRepo_, dsId, this);
if (dlg.exec() == QDialog::Accepted) reloadGrid();
}
void GridDataChartView::openWhitening() {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
// tmObjectId白化模板列表用客户端视图未透传 structParentId按原版兜底空串。
WhiteningDialog dlg(cmdRepo_, dsId, projectId, QString(), this);
if (dlg.exec() == QDialog::Accepted) reloadGrid();
}
void GridDataChartView::openFilter() {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
FilterDialog dlg(cmdRepo_, dsId, projectId, this);
if (dlg.exec() == QDialog::Accepted) reloadGrid();
}
void GridDataChartView::openSaveAs() {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
SaveAsDialog dlg(SaveAsDialog::Mode::Inversion, cmdRepo_, dsId, this);
dlg.exec(); // 另存为新数据,不改当前视图(与原版一致)。
}
void GridDataChartView::applySimplify() {
if (!contourItem_ || !simplifySlider_) return;
contourItem_->setSimplifyTolerance(simplifySlider_->value() / 10.0);
plot_->replot();
}
// ============================ 异常标注 / 描述I9~I14============================
void GridDataChartView::openExceptionDialog() {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
// remarkSourceId = dsObjectId异常挂当前等值面数据集
ExceptionDialog dlg(cmdRepo_, projectId, dsId, this);
if (dlg.exec() == QDialog::Accepted) reloadGrid();
}
void GridDataChartView::openAutoAnnotation() {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
AutoAnnotationDialog dlg(cmdRepo_, dsId, projectId, this);
if (dlg.exec() == QDialog::Accepted) reloadGrid();
}
void GridDataChartView::deleteAnomaly(int index) {
if (!cmdRepo_ || index < 0 || index >= static_cast<int>(anoms_.size())) return;
const QString id = QString::fromStdString(anoms_[index].id);
if (id.isEmpty()) {
QMessageBox::warning(this, QStringLiteral("提示"), QStringLiteral("该异常无 id无法删除"));
return;
}
QPointer<GridDataChartView> self(this);
cmdRepo_->deleteException(id, [self](bool ok, QString msg) {
if (!self) return;
if (!ok) {
QMessageBox::warning(self, QStringLiteral("提示"),
msg.isEmpty() ? QStringLiteral("删除失败") : msg);
return;
}
self->reloadGrid(); // 成功后重载(列表 + 图层同步)
});
}
void GridDataChartView::showAnomalyDetail(int index) {
if (!cmdRepo_ || index < 0 || index >= static_cast<int>(anoms_.size())) return;
ExceptionDetailDialog dlg(cmdRepo_, anoms_[index], this);
if (dlg.exec() == QDialog::Accepted) reloadGrid();
}
void GridDataChartView::locateAnomaly(int index) {
if (!contourItem_ || index < 0 || index >= static_cast<int>(anoms_.size())) return;
contourItem_->setHighlightedAnomaly(index);
// 缩放到该异常包围盒(含 10% padding对照原版 padding 0.1)。
QRectF box = contourItem_->anomalyBoundingRect(index);
if (!box.isNull()) {
const double padX = box.width() * 0.1 + (box.width() <= 0 ? 1.0 : 0.0);
const double padY = box.height() * 0.1 + (box.height() <= 0 ? 1.0 : 0.0);
plot_->setAxisScale(QwtPlot::xBottom, box.left() - padX, box.right() + padX);
plot_->setAxisScale(QwtPlot::yLeft, box.top() - padY, box.bottom() + padY);
if (rescaler_) rescaler_->rescale();
}
plot_->replot();
// 1s 后清除高亮(对照原版 highlight duration 1000ms
QPointer<GridDataChartView> self(this);
QTimer::singleShot(1000, this, [self]() {
if (!self || !self->contourItem_) return;
self->contourItem_->setHighlightedAnomaly(-1);
self->plot_->replot();
});
}
void GridDataChartView::loadDescription() {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty() || !descriptionPanel_) return;
QPointer<GridDataChartView> self(this);
cmdRepo_->getDsObjectDetail(dsId, [self](bool ok, QJsonObject data, QString) {
if (!self || !ok) return;
// 原版从 attachedParameters.deltaContent 取 Quill DeltaQt 退化为纯文本:
// 优先 description 字段,否则拼接 delta ops 的 insert 文本。
QString text = data.value(QStringLiteral("description")).toString();
if (text.isEmpty()) {
const QJsonArray ops = data.value(QStringLiteral("attachedParameters"))
.toObject()
.value(QStringLiteral("deltaContent"))
.toArray();
for (const QJsonValue& op : ops)
text += op.toObject().value(QStringLiteral("insert")).toString();
}
self->descriptionPanel_->setText(text);
});
}
void GridDataChartView::saveDescription(const QString& text) {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
// attachedParameters.deltaContent以最简单 op 包纯文本reload 时可还原为纯文本)。
QJsonArray ops;
if (!text.isEmpty()) ops.append(QJsonObject{{QStringLiteral("insert"), text}});
QJsonObject body{
{QStringLiteral("dsObjectId"), dsId},
{QStringLiteral("description"), text},
{QStringLiteral("attachedParameters"),
QJsonObject{{QStringLiteral("deltaContent"), ops}}},
};
QPointer<GridDataChartView> self(this);
cmdRepo_->updateDsObject(body, [self](bool ok, QString msg) {
if (!self) return;
QMessageBox::information(
self, QStringLiteral("提示"),
ok ? QStringLiteral("描述已保存")
: (msg.isEmpty() ? QStringLiteral("保存失败") : msg));
});
}
} // namespace geopro::app

View File

@ -15,11 +15,13 @@
class QSlider;
class QLabel;
class QCheckBox;
class QTimer;
class QwtPlot;
class QwtPlotRescaler;
namespace geopro::data {
class IColorTemplateRepository;
class IDatasetCommandRepository;
}
namespace geopro::app {
@ -29,6 +31,7 @@ class DescriptionPanel;
class ColorBarWidget;
class ColorMapService;
class ContourPlotItem;
class ContourHoverTip;
// 网格数据图表视图:工具条 + QwtPlot白底 + 真实比尺 + 实时平移/滚轮缩放x 轴在底部)
// + 独立色阶条 + 底部双页签(异常列表/描述)。
@ -52,9 +55,30 @@ public:
void setColorTemplateRepo(geopro::data::IColorTemplateRepository* repo,
std::function<QString()> projectIdGetter);
// 注入反演命令仓储 + dsId/projectId 取值回调(网格化/白化/滤波/另存为按钮)。
// 可传空仓储 → 这些按钮退化为「暂未实现」占位提示。
void setCommandRepo(geopro::data::IDatasetCommandRepository* repo,
std::function<QString()> dsIdGetter,
std::function<QString()> projectIdGetter);
private:
void rebuildContour(); // 按当前显隐开关重建并重绘 ContourPlotItem
void openColorScaleEditor(); // 「色阶配置」→ 共享色阶编辑器(色阶 + 层级⚙ + 线形⚙)
void openGridWizard(); // I1「网格」→ 网格化向导
void openWhitening(); // I3「白化」→ 白化弹窗
void openFilter(); // I4「滤波处理」→ 滤波弹窗
void openSaveAs(); // I15「另存为」→ 复用 SaveAsDialog(Inversion)
void reloadGrid(); // 处理类成功后loadInversionGrid → setGridData 重绘
void applySimplify(); // I8把当前滑块容差透传给 ContourPlotItem 并重绘
void showNotImplemented(QWidget* anchor); // 占位提示(无仓储/无 dsId
void openExceptionDialog(); // I9 异常创建
void openAutoAnnotation(); // I13 自动标注
void deleteAnomaly(int index); // I10 异常删除
void showAnomalyDetail(int index); // I11 异常详情/编辑
void locateAnomaly(int index); // I12 异常定位(高亮 + 缩放)
void loadDescription(); // I14 进入时回填描述
void saveDescription(const QString& text); // I14 保存描述
QwtPlot* plot_ = nullptr;
QwtPlotRescaler* rescaler_ = nullptr;
@ -64,6 +88,8 @@ private:
QSlider* simplifySlider_ = nullptr;
QLabel* simplifyValueLabel_ = nullptr;
QCheckBox* chkShowLabels_ = nullptr; // 工具条「显示等值线标注」(线形⚙ 改标注显隐后同步)
QTimer* simplifyDebounce_ = nullptr; // I8 简化容差防抖(~300ms
ContourHoverTip* contourTip_ = nullptr; // I7 等值线提示hover
// 渲染状态
ColorMapService* colorSvc_ = nullptr; // heapsetGridData 重建
@ -81,6 +107,10 @@ private:
// 色阶模板仓储 + projectId 取值回调(注入;空则编辑器后端按钮禁用)。
geopro::data::IColorTemplateRepository* tplRepo_ = nullptr;
std::function<QString()> projectIdGetter_;
// 反演命令仓储 + dsId 取值回调(注入;空则处理类按钮占位提示)。
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
std::function<QString()> dsIdGetter_;
};
} // namespace geopro::app

View File

@ -0,0 +1,217 @@
#include "panels/chart/GridWizardDialog.hpp"
#include <utility>
#include <QComboBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QMessageBox>
#include <QPointer>
#include <QPushButton>
#include <QSpinBox>
#include <QStackedWidget>
#include <QVBoxLayout>
#include "FormKit.hpp"
#include "panels/chart/InversionProcessOps.hpp" // buildGridToBody
#include "repo/IDatasetCommandRepository.hpp"
namespace geopro::app {
namespace {
constexpr double kCoordRange = 1e9; // 坐标范围(足够宽)
constexpr int kSizeMin = 1, kSizeMax = 300; // 点数范围(对照原版 1~300
constexpr int kDefaultXSize = 100; // 默认点数(原版 xPoints 默认 100
// 配一个坐标 spinbox6 位小数,宽范围)。
QDoubleSpinBox* makeCoordSpin(QWidget* parent) {
auto* sp = new QDoubleSpinBox(parent);
sp->setRange(-kCoordRange, kCoordRange);
sp->setDecimals(6);
return sp;
}
} // namespace
GridWizardDialog::GridWizardDialog(geopro::data::IDatasetCommandRepository* repo, QString dsId,
QWidget* parent)
: QDialog(parent), repo_(repo), dsId_(std::move(dsId)) {
setWindowTitle(QStringLiteral("网格化"));
setModal(true);
resize(560, 420);
auto* root = formkit::dialogRoot(this);
stack_ = new QStackedWidget(this);
root->addWidget(stack_, 1);
// ── 步骤 1算法选择单选列表──────────────────────────────────────
auto* page1 = new QWidget(this);
auto* p1Lay = new QVBoxLayout(page1);
p1Lay->addWidget(new QLabel(QStringLiteral("请选择网格方法:"), page1));
algoList_ = new QListWidget(page1);
p1Lay->addWidget(algoList_, 1);
stack_->addWidget(page1);
// ── 步骤 2网格参数 + 数据值设置 ────────────────────────────────────
auto* page2 = new QWidget(this);
auto* p2Lay = new QVBoxLayout(page2);
xMin_ = makeCoordSpin(page2); xMax_ = makeCoordSpin(page2);
yMin_ = makeCoordSpin(page2); yMax_ = makeCoordSpin(page2);
vMin_ = makeCoordSpin(page2); vMax_ = makeCoordSpin(page2);
xSize_ = new QSpinBox(page2); xSize_->setRange(kSizeMin, kSizeMax); xSize_->setValue(kDefaultXSize);
ySize_ = new QSpinBox(page2); ySize_->setRange(kSizeMin, kSizeMax); ySize_->setValue(kDefaultXSize);
xSpacing_ = makeCoordSpin(page2); xSpacing_->setRange(0.0, kCoordRange);
ySpacing_ = makeCoordSpin(page2); ySpacing_->setRange(0.0, kCoordRange);
saveFormat_ = new QComboBox(page2);
saveFormat_->addItem(QStringLiteral("线性"), QStringLiteral("linear"));
saveFormat_->addItem(QStringLiteral("对数"), QStringLiteral("log"));
formkit::capField(xMin_);
formkit::capField(xMax_);
formkit::capField(xSpacing_);
formkit::capField(yMin_);
formkit::capField(yMax_);
formkit::capField(ySpacing_);
formkit::capField(vMin_);
formkit::capField(vMax_);
formkit::capField(xSize_);
formkit::capField(ySize_);
formkit::capField(saveFormat_);
auto* form = formkit::makeEditForm();
form->addRow(formkit::editLabel(QStringLiteral("Xmin")), xMin_);
form->addRow(formkit::editLabel(QStringLiteral("Xmax")), xMax_);
form->addRow(formkit::editLabel(QStringLiteral("X点数")), xSize_);
form->addRow(formkit::editLabel(QStringLiteral("X间距")), xSpacing_);
form->addRow(formkit::editLabel(QStringLiteral("Ymin")), yMin_);
form->addRow(formkit::editLabel(QStringLiteral("Ymax")), yMax_);
form->addRow(formkit::editLabel(QStringLiteral("Y点数")), ySize_);
form->addRow(formkit::editLabel(QStringLiteral("Y间距")), ySpacing_);
form->addRow(formkit::editLabel(QStringLiteral("数据值min")), vMin_);
form->addRow(formkit::editLabel(QStringLiteral("数据值max")), vMax_);
form->addRow(formkit::editLabel(QStringLiteral("保存格式:")), saveFormat_);
p2Lay->addLayout(form);
stack_->addWidget(page2);
// ── 底部按钮(上一步 / 下一步 / 确认 / 取消)────────────────────────
auto* btnLay = new QHBoxLayout();
btnLay->addStretch();
prevBtn_ = new QPushButton(QStringLiteral("上一步"), this);
nextBtn_ = new QPushButton(QStringLiteral("下一步"), this);
okBtn_ = new QPushButton(QStringLiteral("确认"), this);
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
btnLay->addWidget(prevBtn_);
btnLay->addWidget(nextBtn_);
btnLay->addWidget(okBtn_);
btnLay->addWidget(cancelBtn);
root->addLayout(btnLay);
prevBtn_->setVisible(false);
okBtn_->setVisible(false);
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
connect(nextBtn_, &QPushButton::clicked, this, &GridWizardDialog::goToStep2);
connect(prevBtn_, &QPushButton::clicked, this, [this]() {
stack_->setCurrentIndex(0);
prevBtn_->setVisible(false); okBtn_->setVisible(false); nextBtn_->setVisible(true);
});
connect(okBtn_, &QPushButton::clicked, this, &GridWizardDialog::onConfirm);
// 点数变化 → 重算间距(原版 calculateXInterval/calculateYInterval
connect(xSize_, QOverload<int>::of(&QSpinBox::valueChanged), this,
[this](int) { recalcXSpacing(); });
connect(ySize_, QOverload<int>::of(&QSpinBox::valueChanged), this,
[this](int) { recalcYSpacing(); });
loadAlgorithms();
}
void GridWizardDialog::loadAlgorithms() {
if (!repo_) return;
QPointer<GridWizardDialog> self(this);
repo_->listGridAlgorithm(dsId_, [self](bool ok, QJsonArray list, QString) {
if (!self || !ok) return;
self->algoList_->clear();
for (const QJsonValue& v : list) {
const QJsonObject o = v.toObject();
const QString name = o.value(QStringLiteral("scriptName")).toString();
const QString code = o.value(QStringLiteral("scriptCode")).toString();
auto* item = new QListWidgetItem(name, self->algoList_);
item->setData(Qt::UserRole, code);
}
if (self->algoList_->count() > 0) self->algoList_->setCurrentRow(0); // 默认首项
});
}
void GridWizardDialog::goToStep2() {
if (!algoList_->currentItem()) {
QMessageBox::warning(this, windowTitle(), QStringLiteral("请选择网格方法"));
return;
}
stack_->setCurrentIndex(1);
nextBtn_->setVisible(false);
prevBtn_->setVisible(true);
okBtn_->setVisible(true);
loadParams();
}
void GridWizardDialog::loadParams() {
if (!repo_) return;
QPointer<GridWizardDialog> self(this);
repo_->getGridRawDataParams(dsId_, [self](bool ok, QJsonObject d, QString) {
if (!self || !ok) return;
self->xMin_->setValue(d.value(QStringLiteral("xmin")).toDouble());
self->xMax_->setValue(d.value(QStringLiteral("xmax")).toDouble());
self->yMin_->setValue(d.value(QStringLiteral("ymin")).toDouble());
self->yMax_->setValue(d.value(QStringLiteral("ymax")).toDouble());
self->vMin_->setValue(d.value(QStringLiteral("vmin")).toDouble());
self->vMax_->setValue(d.value(QStringLiteral("vmax")).toDouble());
// 初始间距 = (xmax-xmin)/xSizey 间距同 x原版 onMounted 逻辑)。
self->recalcXSpacing();
const double dx = self->xSpacing_->value();
if (dx > 0) self->ySpacing_->setValue(dx);
});
}
void GridWizardDialog::recalcXSpacing() {
const int n = xSize_->value();
if (n > 0) xSpacing_->setValue((xMax_->value() - xMin_->value()) / n);
}
void GridWizardDialog::recalcYSpacing() {
const int n = ySize_->value();
if (n > 0) ySpacing_->setValue((yMax_->value() - yMin_->value()) / n);
}
void GridWizardDialog::onConfirm() {
if (!repo_ || !algoList_->currentItem()) { reject(); return; }
if (xMax_->value() < xMin_->value() || yMax_->value() < yMin_->value() ||
vMax_->value() < vMin_->value()) {
QMessageBox::warning(this, windowTitle(), QStringLiteral("最大值不能小于最小值"));
return;
}
GridToParams p;
p.dsObjectId = dsId_;
p.actionCode = algoList_->currentItem()->data(Qt::UserRole).toString();
p.xMin = xMin_->value(); p.xMax = xMax_->value();
p.yMin = yMin_->value(); p.yMax = yMax_->value();
p.vMin = vMin_->value(); p.vMax = vMax_->value();
p.xSize = xSize_->value(); p.ySize = ySize_->value();
p.xSpacing = xSpacing_->value(); p.ySpacing = ySpacing_->value();
p.logFormat = (saveFormat_->currentData().toString() == QStringLiteral("log"));
okBtn_->setEnabled(false);
QPointer<GridWizardDialog> self(this);
repo_->toGrid(buildGridToBody(p), [self](bool ok, QString msg) {
if (!self) return;
self->okBtn_->setEnabled(true);
if (ok) {
self->accept();
} else {
QMessageBox::warning(self, self->windowTitle(),
msg.isEmpty() ? QStringLiteral("网格化失败") : msg);
}
});
}
} // namespace geopro::app

View File

@ -0,0 +1,60 @@
#pragma once
#include <QDialog>
#include <QString>
class QComboBox;
class QDoubleSpinBox;
class QSpinBox;
class QPushButton;
class QStackedWidget;
class QListWidget;
namespace geopro::data {
class IDatasetCommandRepository;
}
namespace geopro::app {
// 网格化向导2 步1:1 复刻原版 GridDialog
// 步骤 1listGridAlgorithm 选算法单选列表scriptName 显示 / scriptCode 提交)。
// 步骤 2getGridRawDataParams 取 x/y/v min/max 默认 + 点数/间距/保存格式(线性|对数)
// 确认 → toGrid。成功 accept(),调用方(视图)随后重载网格重绘。
// I1网格视图工具条「网格」与 O1原数据工具条「网格」共用本向导。
class GridWizardDialog : public QDialog {
Q_OBJECT
public:
GridWizardDialog(geopro::data::IDatasetCommandRepository* repo, QString dsId,
QWidget* parent = nullptr);
private:
void loadAlgorithms(); // 步骤 1拉算法列表
void loadParams(); // 步骤 2拉 x/y/v 默认参数
void goToStep2(); // 下一步(校验算法已选)
void recalcXSpacing(); // (xMax-xMin)/xSize → xSpacing间距已有值时
void recalcYSpacing(); // (yMax-yMin)/ySize → ySpacing
void onConfirm(); // 确认 → toGrid
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
QString dsId_;
QStackedWidget* stack_ = nullptr;
QListWidget* algoList_ = nullptr; // 算法单选列表userData=scriptCode
QDoubleSpinBox* xMin_ = nullptr;
QDoubleSpinBox* xMax_ = nullptr;
QDoubleSpinBox* yMin_ = nullptr;
QDoubleSpinBox* yMax_ = nullptr;
QDoubleSpinBox* vMin_ = nullptr;
QDoubleSpinBox* vMax_ = nullptr;
QSpinBox* xSize_ = nullptr;
QSpinBox* ySize_ = nullptr;
QDoubleSpinBox* xSpacing_ = nullptr;
QDoubleSpinBox* ySpacing_ = nullptr;
QComboBox* saveFormat_ = nullptr; // linear / log
QPushButton* nextBtn_ = nullptr;
QPushButton* prevBtn_ = nullptr;
QPushButton* okBtn_ = nullptr;
};
} // namespace geopro::app

View File

@ -0,0 +1,200 @@
#include "panels/chart/InversionFormDialog.hpp"
#include <utility>
#include <QComboBox>
#include <QFrame>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QPointer>
#include <QPushButton>
#include <QSignalBlocker>
#include <QVBoxLayout>
#include "FormKit.hpp"
#include "repo/IDatasetCommandRepository.hpp"
namespace geopro::app {
// 纯解析/组装函数定义在 InversionFormParse.cppQt-Core-only便于单测
namespace {
constexpr char kVisualResistivityCode[] = "script_visual_resistivity_data";
} // namespace
InversionFormDialog::InversionFormDialog(Mode mode, geopro::data::IDatasetCommandRepository* repo,
QString dsId, QString projectId, QWidget* parent)
: QDialog(parent),
mode_(mode),
repo_(repo),
dsId_(std::move(dsId)),
projectId_(std::move(projectId)) {
setWindowTitle(mode_ == Mode::Inversion ? QStringLiteral("反演运算")
: QStringLiteral("反演参数设置"));
setModal(true);
resize(480, 360);
auto* root = formkit::dialogRoot(this);
// 模型选择行label + 下拉)。生成视电阻率下拉禁用(复刻原版 disabled
auto* modelLay = new QVBoxLayout();
modelLay->addWidget(formkit::editLabel(QStringLiteral("反演模型"), this));
modelCombo_ = new QComboBox(this);
if (mode_ == Mode::ApparentResistivity) modelCombo_->setEnabled(false);
modelLay->addWidget(modelCombo_);
root->addLayout(modelLay);
// 动态字段容器(按 groups_ 重建)。
formHost_ = new QWidget(this);
formHostLay_ = new QVBoxLayout(formHost_);
formHostLay_->setContentsMargins(0, 0, 0, 0);
root->addWidget(formHost_, 1);
// 底部按钮(取消 / 确定)。
auto* btnLay = new QHBoxLayout();
btnLay->addStretch();
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
okBtn_ = new QPushButton(QStringLiteral("确定"), this);
okBtn_->setDefault(true);
btnLay->addWidget(cancelBtn);
btnLay->addWidget(okBtn_);
root->addLayout(btnLay);
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
connect(okBtn_, &QPushButton::clicked, this, &InversionFormDialog::onConfirm);
// 反演运算下拉可切换模型allow-clear 在 Qt 以可空首项体现);生成视电阻率禁用故不触发。
connect(modelCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this](int) { onModelChanged(); });
loadScripts();
}
void InversionFormDialog::loadScripts() {
if (!repo_) return;
QPointer<InversionFormDialog> self(this);
const Mode mode = mode_;
repo_->listInversionScripts(dsId_, [self, mode](bool ok, QJsonArray list, QString) {
if (!self) return;
if (!ok) return;
QSignalBlocker block(self->modelCombo_); // 填充期不触发 onModelChanged
self->modelCombo_->clear();
// 反演运算:首项为空(对应原版 allow-clear无默认选中
if (mode == Mode::Inversion) self->modelCombo_->addItem(QString(), QString());
int defaultIdx = -1;
for (const QJsonValue& v : list) {
const QJsonObject o = v.toObject();
const QString label = o.value(QStringLiteral("label")).toString();
const QString value = o.value(QStringLiteral("value")).toString();
const QString code = o.value(QStringLiteral("code")).toString();
self->modelCombo_->addItem(label, value);
if (mode == Mode::ApparentResistivity &&
code == QLatin1String(kVisualResistivityCode)) {
defaultIdx = self->modelCombo_->count() - 1; // 默认选中视电阻率(复刻原版)
}
}
if (mode == Mode::ApparentResistivity) {
// 默认选中视电阻率项未命中则退首项setCurrentIndex 触发 onModelChanged 拉表单。
self->modelCombo_->setCurrentIndex(defaultIdx >= 0 ? defaultIdx : 0);
self->onModelChanged();
}
});
}
void InversionFormDialog::onModelChanged() {
const QString typeId = modelCombo_->currentData().toString();
groups_.clear();
if (typeId.isEmpty()) {
rebuildFormArea(); // 清空表单(复刻 changeModel: 清空 dynamicForms
return;
}
loadDynamicForm(typeId);
}
void InversionFormDialog::loadDynamicForm(const QString& typeId) {
if (!repo_) return;
QPointer<InversionFormDialog> self(this);
repo_->getDynamicForm(projectId_, typeId, [self](bool ok, QJsonObject data, QString) {
if (!self) return;
if (!ok) return;
self->groups_ = parseDynamicForm(data);
self->rebuildFormArea();
});
}
void InversionFormDialog::rebuildFormArea() {
// 清空旧字段控件(含布局子项),重置取值索引。
fieldCombos_.clear();
fieldCodes_.clear();
QLayoutItem* item = nullptr;
while ((item = formHostLay_->takeAt(0)) != nullptr) {
if (item->widget()) item->widget()->deleteLater();
delete item;
}
const bool fillDefaults = (mode_ == Mode::ApparentResistivity);
for (const auto& g : groups_) {
// 分组卡片:标题 + 字段两列网格(复刻原版 a-card 分组 + 半宽字段)。
auto* card = new QFrame(formHost_);
card->setObjectName(QStringLiteral("inversionGroupCard"));
auto* cardLay = new QVBoxLayout(card);
if (!g.groupName.isEmpty())
formkit::addSection(cardLay, g.groupName, card, /*topGap=*/false);
auto* grid = new QGridLayout();
cardLay->addLayout(grid);
int col = 0, gridRow = 0;
for (const auto& f : g.fields) {
auto* fieldBox = new QVBoxLayout();
fieldBox->addWidget(formkit::editLabel(f.fieldName, card));
auto* combo = new QComboBox(card);
for (const auto& o : f.options) combo->addItem(o.label, o.value);
// 生成视电阻率:默认选首项(复刻 initDynamicFieldsDefaultValues
if (fillDefaults && combo->count() > 0) combo->setCurrentIndex(0);
fieldBox->addWidget(combo);
grid->addLayout(fieldBox, gridRow, col);
fieldCombos_.push_back(combo);
fieldCodes_.push_back(f.fieldCode);
if (++col >= 2) { col = 0; ++gridRow; }
}
formHostLay_->addWidget(card);
}
formHostLay_->addStretch();
}
void InversionFormDialog::onConfirm() {
if (!repo_) { reject(); return; }
const QString scriptId = modelCombo_->currentData().toString();
if (scriptId.isEmpty()) {
QMessageBox::warning(this, windowTitle(), QStringLiteral("请选择反演模型"));
return;
}
// 由当前各字段下拉选值装配 {fieldCode: value}。
QJsonObject selected;
for (size_t i = 0; i < fieldCombos_.size(); ++i) {
selected.insert(fieldCodes_[static_cast<int>(i)],
fieldCombos_[i]->currentData().toString());
}
const bool fillDefaults = (mode_ == Mode::ApparentResistivity);
const QJsonObject fields = assembleFieldMap(groups_, selected, fillDefaults);
okBtn_->setEnabled(false);
QPointer<InversionFormDialog> self(this);
auto onDone = [self](bool ok, QString msg) {
if (!self) return;
self->okBtn_->setEnabled(true);
if (ok) {
self->accept();
} else {
QMessageBox::warning(self, self->windowTitle(),
msg.isEmpty() ? QStringLiteral("反演任务失败") : msg);
}
};
if (mode_ == Mode::Inversion) {
repo_->submitInversionTask(dsId_, scriptId, fields, onDone);
} else {
repo_->createVisualResistivityData(dsId_, scriptId, fields, onDone);
}
}
} // namespace geopro::app

View File

@ -0,0 +1,63 @@
#pragma once
#include <vector>
#include <QDialog>
#include <QJsonObject>
#include <QString>
#include "panels/chart/InversionFormParse.hpp" // InversionGroup + 纯解析/组装函数
class QComboBox;
class QVBoxLayout;
class QPushButton;
class QWidget;
namespace geopro::data {
class IDatasetCommandRepository;
}
namespace geopro::app {
// 反演动态表单对话框1:1 复刻原版 web。一套对话框服务两个入口用 Mode 区分:
// - Inversion → measurement「反演运算」原版 InversionForm.vue + postInversionTask
// - ApparentResistivity → measurement「生成视电阻率」原版 InversionDialog.vue + createVisualResistivityData
// 共同流程:① 拉模型列表 → ② 选模型 → ③ 按 typeId 拉动态表单 → ④ 分组卡片渲染字段 → ⑤ 提交。
// 差异(严格对照原版):
// Inversion模型下拉可选(allow-clear)、无默认选中、无字段默认值;提交体 {dsId,scriptId,properties}。
// ApparentResistivity模型下拉禁用、默认选中 code=='script_visual_resistivity_data'、
// 字段默认取首个选项;提交体 {dsObjectId,scriptId,scriptParamListJsonStr}。
// 回调用 QPointer 守卫(对话框 modal exec但异步回调仍可能在关闭后到达
class InversionFormDialog : public QDialog {
Q_OBJECT
public:
enum class Mode {
Inversion, // 反演运算
ApparentResistivity, // 生成视电阻率
};
InversionFormDialog(Mode mode, geopro::data::IDatasetCommandRepository* repo,
QString dsId, QString projectId, QWidget* parent = nullptr);
private:
void loadScripts(); // 拉模型列表填下拉
void onModelChanged(); // 模型变更 → 拉动态表单
void loadDynamicForm(const QString& typeId);
void rebuildFormArea(); // 按 groups_ 重建分组卡片
void onConfirm(); // 提交(按 mode 走不同端点)
Mode mode_;
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
QString dsId_;
QString projectId_;
QComboBox* modelCombo_ = nullptr;
QWidget* formHost_ = nullptr; // 动态字段容器(重建时清空重填)
QVBoxLayout* formHostLay_ = nullptr;
QPushButton* okBtn_ = nullptr;
std::vector<InversionGroup> groups_; // 当前模型的动态表单(已解析)
std::vector<QComboBox*> fieldCombos_; // 与 groups_ 展平后的字段同序(取值用)
std::vector<QString> fieldCodes_; // 与 fieldCombos_ 同序的 fieldCode
};
} // namespace geopro::app

View File

@ -0,0 +1,52 @@
#include "panels/chart/InversionFormParse.hpp"
#include <utility>
#include <QJsonArray>
#include <QJsonValue>
namespace geopro::app {
std::vector<InversionGroup> parseDynamicForm(const QJsonObject& data) {
std::vector<InversionGroup> groups;
const QJsonArray formList = data.value(QStringLiteral("formList")).toArray();
for (const QJsonValue& gv : formList) {
const QJsonObject g = gv.toObject();
InversionGroup group;
group.groupName = g.value(QStringLiteral("groupName")).toString();
const QJsonArray values = g.value(QStringLiteral("values")).toArray();
for (const QJsonValue& fv : values) {
const QJsonObject f = fv.toObject();
InversionField field;
field.fieldCode = f.value(QStringLiteral("fieldCode")).toString();
field.fieldName = f.value(QStringLiteral("fieldName")).toString();
const QJsonArray opts = f.value(QStringLiteral("optionsObject")).toArray();
for (const QJsonValue& ov : opts) {
const QJsonObject o = ov.toObject();
field.options.push_back({o.value(QStringLiteral("label")).toString(),
o.value(QStringLiteral("value")).toString()});
}
group.fields.push_back(std::move(field));
}
groups.push_back(std::move(group));
}
return groups;
}
QJsonObject assembleFieldMap(const std::vector<InversionGroup>& groups, const QJsonObject& selected,
bool fillDefaults) {
QJsonObject out;
for (const auto& g : groups) {
for (const auto& f : g.fields) {
QString value;
if (selected.contains(f.fieldCode)) value = selected.value(f.fieldCode).toString();
if (value.isEmpty() && fillDefaults && !f.options.empty()) {
value = f.options.front().value; // 复刻 initDynamicFieldsDefaultValues
}
if (!value.isEmpty()) out.insert(f.fieldCode, value); // 复刻 handleConfirm空值不进体
}
}
return out;
}
} // namespace geopro::app

View File

@ -0,0 +1,35 @@
#pragma once
#include <vector>
#include <QJsonObject>
#include <QString>
namespace geopro::app {
// 反演动态表单的数据模型 + 纯解析/组装函数(仅依赖 QtCore JSON无 Widgets/MOC
// 拆出独立 TU 以便单测tests 链 geopro_data/Qt6::Core 即可,不必拖入对话框)。
// 一个动态表单字段(仅取渲染/提交所需,复刻原版 optionsObject + fieldCode/fieldName
struct InversionFieldOption {
QString label;
QString value;
};
struct InversionField {
QString fieldCode;
QString fieldName;
std::vector<InversionFieldOption> options; // optionsObjectSelect 选项)
};
struct InversionGroup {
QString groupName;
std::vector<InversionField> fields;
};
// 解析动态表单响应 data.formList → 分组/字段模型。
std::vector<InversionGroup> parseDynamicForm(const QJsonObject& data);
// 组装提交字段表 {fieldCode: value}。fillDefaults=true 时空选值回退首个选项(生成视电阻率用)。
// 复刻原版 handleConfirm仅写入有值的字段空值不进体
QJsonObject assembleFieldMap(const std::vector<InversionGroup>& groups, const QJsonObject& selected,
bool fillDefaults);
} // namespace geopro::app

View File

@ -0,0 +1,98 @@
#include "panels/chart/InversionProcessOps.hpp"
namespace geopro::app {
QJsonObject buildGridToBody(const GridToParams& p) {
// 字段名/casing 严格对照原版 toGridTheDataxsize/ysize 小写xSpacing/ySpacing 驼峰。
return QJsonObject{
{QStringLiteral("dsObjectId"), p.dsObjectId},
{QStringLiteral("actionCode"), p.actionCode},
{QStringLiteral("xminValue"), p.xMin},
{QStringLiteral("xmaxValue"), p.xMax},
{QStringLiteral("yminValue"), p.yMin},
{QStringLiteral("ymaxValue"), p.yMax},
{QStringLiteral("xsize"), p.xSize},
{QStringLiteral("xSpacing"), p.xSpacing},
{QStringLiteral("ysize"), p.ySize},
{QStringLiteral("ySpacing"), p.ySpacing},
{QStringLiteral("vminValue"), p.vMin},
{QStringLiteral("vmaxValue"), p.vMax},
// saveDataValueType1=线性 2=对数(原版 saveFormat==='linear'?1:2
{QStringLiteral("saveDataValueType"), p.logFormat ? 2 : 1},
};
}
QJsonObject buildWhitenBody(const WhitenParams& p) {
QJsonObject body{
{QStringLiteral("dsObjectId"), p.dsObjectId},
{QStringLiteral("whiteningMethod"), p.whiteningMethod},
};
if (p.whiteningMethod == 1) {
body.insert(QStringLiteral("boundaryExtension"), p.boundaryExtension);
// 原版 whitenedWay 为布尔whiteningType===0 → true外部白化
body.insert(QStringLiteral("whitenedWay"), p.whiteningType == 0);
} else if (p.whiteningMethod == 2) {
body.insert(QStringLiteral("whitenedDataId"), p.whitenedDataId);
} else if (p.whiteningMethod == 3) {
body.insert(QStringLiteral("modelWhiteningSubType"), p.modelWhiteningSubType);
}
return body;
}
int filterBoundaryCode(const QString& dataEdge) {
if (dataEdge == QStringLiteral("whitening")) return 1;
if (dataEdge == QStringLiteral("skip")) return 2;
if (dataEdge == QStringLiteral("edgePoint")) return 3;
if (dataEdge == QStringLiteral("filling")) return 4;
return 1; // 默认设为无效点
}
int filterNoDataCode(const QString& noDataPoints) {
if (noDataPoints == QStringLiteral("expansion")) return 1;
if (noDataPoints == QStringLiteral("retain")) return 2;
if (noDataPoints == QStringLiteral("skip")) return 3;
if (noDataPoints == QStringLiteral("filling")) return 4;
return 4; // 默认填充
}
QJsonArray matrixToJson(const std::vector<std::vector<double>>& matrix) {
QJsonArray form;
for (const auto& r : matrix) {
QJsonArray row;
for (double v : r) row.append(v);
form.append(row);
}
return form;
}
QJsonObject buildFilterApplyBody(const FilterApplyParams& p) {
// 全字段对照原版 useFilterToProcessData。
return QJsonObject{
{QStringLiteral("boundary"), filterBoundaryCode(p.dataEdge)},
{QStringLiteral("boundaryValue"), p.dataEdgeValue},
{QStringLiteral("column"), p.column},
{QStringLiteral("dsObjectId"), p.dsObjectId},
{QStringLiteral("noDataPoints"), filterNoDataCode(p.noDataPoints)},
{QStringLiteral("noDataPointsValue"), p.noDataValue},
{QStringLiteral("number"), p.number},
{QStringLiteral("row"), p.row},
{QStringLiteral("rowColumValue"),
QJsonObject{{QStringLiteral("form"), matrixToJson(p.matrix)}}},
{QStringLiteral("filteringMethod"), p.filteringMethod},
};
}
QJsonObject buildNewFilterBody(const QString& name, const QString& projectId,
const QString& parentId,
const std::vector<std::vector<double>>& matrix) {
return QJsonObject{
{QStringLiteral("type"), 1},
{QStringLiteral("rowColumValue"),
QJsonObject{{QStringLiteral("form"), matrixToJson(matrix)}}},
{QStringLiteral("name"), name},
{QStringLiteral("projectId"), projectId},
{QStringLiteral("parentId"), parentId},
};
}
} // namespace geopro::app

View File

@ -0,0 +1,77 @@
#pragma once
#include <vector>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
namespace geopro::app {
// 反演网格视图「处理类」操作的纯逻辑(仅依赖 QtCore JSON无 Widgets/MOC
// 拆出独立 TU 以便单测(与 ScatterDataOps / InversionFormParse 同范式)。
// 字段名/取值一律对照原版 webGridDialog/WhiteningDialog/FilterDialog
// ── I1/O1 网格化toGrid对照原版 toGridTheData─────────────────────────────
// 网格化向导第二步参数集合。spacing/size 取整由调用方按 UI 输入填好。
struct GridToParams {
QString dsObjectId;
QString actionCode; // = 算法项 scriptCode
double xMin = 0.0, xMax = 0.0, yMin = 0.0, yMax = 0.0;
double vMin = 0.0, vMax = 0.0;
int xSize = 0, ySize = 0; // 点数
double xSpacing = 0.0, ySpacing = 0.0; // 间距
bool logFormat = false; // 保存格式false=线性(1) true=对数(2)
};
// 组装 toGrid 请求体(字段名/casing 对照原版xsize/ysize 小写xSpacing/ySpacing 驼峰)。
QJsonObject buildGridToBody(const GridToParams& p);
// ── I3 白化whitenData对照原版 whitenTheData────────────────────────────
// 三种白化方式(数值对照原版 whiteningMethod 1/2/3
struct WhitenParams {
QString dsObjectId;
int whiteningMethod = 1; // 1 数据边界自动 / 2 白化模板 / 3 模型白化
// method 1边界扩展 + 内/外白化whiteningType0 外部 / 1 内部)。
double boundaryExtension = 0.0;
int whiteningType = 0;
// method 2选中的白化文件 id。
QString whitenedDataId;
// method 3模型白化子类型2 梯形 / 1 矩形)。
int modelWhiteningSubType = 2;
};
// 组装 whitenData 请求体(按 method 分支组装差异字段,对照原版)。
// method 1 的 whitenedWay 是布尔whiteningType==0 → true 外部)。
QJsonObject buildWhitenBody(const WhitenParams& p);
// ── I4 滤波applyFilter对照原版 useFilterToProcessData────────────────────
// 数据边缘处理方式 codewhitening→1 / skip→2 / edgePoint→3 / filling→4
int filterBoundaryCode(const QString& dataEdge);
// 无数据点处理方式 codeexpansion→1 / retain→2 / skip→3 / filling→4
int filterNoDataCode(const QString& noDataPoints);
struct FilterApplyParams {
QString dsObjectId;
QString dataEdge = QStringLiteral("whitening"); // 数据边缘下拉值
double dataEdgeValue = 0.0; // 仅 filling 生效
QString noDataPoints = QStringLiteral("filling"); // 无数据点下拉值
double noDataValue = 0.0; // 仅 filling 生效
int number = 1; // 滤波次数
int row = 3, column = 3; // 矩阵行/列
std::vector<std::vector<double>> matrix; // 滤波矩阵
QString filteringMethod; // 选中滤波器节点名(字符串)
};
// 把二维矩阵转为 rowColumValue.formQJsonArray of QJsonArray
QJsonArray matrixToJson(const std::vector<std::vector<double>>& matrix);
// 组装 applyFilter 请求体(全字段对照原版 useFilterToProcessData
QJsonObject buildFilterApplyBody(const FilterApplyParams& p);
// 组装 newFilter 请求体(自定义滤波器另存,对照原版 newTheFilter
// {type:1, rowColumValue:{form:matrix}, name, projectId, parentId}。
QJsonObject buildNewFilterBody(const QString& name, const QString& projectId,
const QString& parentId,
const std::vector<std::vector<double>>& matrix);
} // namespace geopro::app

View File

@ -1,23 +1,45 @@
#include "panels/chart/RawDataChartView.hpp"
#include "ColorScaleConfigDialog.hpp"
#include "panels/chart/ChartTheme.hpp"
#include "panels/chart/ColorBarWidget.hpp"
#include "panels/chart/GridWizardDialog.hpp"
#include "panels/chart/InversionFormDialog.hpp"
#include "panels/chart/SaveAsDialog.hpp"
#include "panels/chart/ScatterDataOps.hpp"
#include "panels/chart/ScatterFilterDialog.hpp"
#include "panels/chart/ScatterHoverTip.hpp"
#include "panels/chart/ScatterPlotItem.hpp"
#include <utility>
#include "repo/IDatasetCommandRepository.hpp"
#include <QByteArray>
#include <QComboBox>
#include <QCursor>
#include <QEvent>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QIcon>
#include <QJsonArray>
#include <QJsonObject>
#include <QLabel>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPen>
#include <QPixmap>
#include <QPointer>
#include <QPushButton>
#include <QSaveFile>
#include <QSignalBlocker>
#include <QToolButton>
#include <QToolTip>
#include <QVBoxLayout>
#include <qwt_scale_map.h>
#include <qwt_plot.h>
#include <qwt_plot_canvas.h>
#include <qwt_plot_marker.h>
@ -67,6 +89,13 @@ RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) {
tbLay->addWidget(btnSaveAs);
tbLay->addStretch();
// 反演原数据默认工具条交互O1 网格 / O2 色阶配置 / O3 另存为)。
connect(btnGrid, &QToolButton::clicked, this, [this, btnGrid]() { openGridWizard(btnGrid); });
connect(btnColorScale, &QToolButton::clicked, this,
[this, btnColorScale]() { openInversionColorScale(btnColorScale); });
connect(btnSaveAs, &QToolButton::clicked, this,
[this, btnSaveAs]() { openInversionSaveAs(btnSaveAs); });
lay->addWidget(toolbar);
// ---- QwtPlotstretch 填满剩余空间)----
@ -257,6 +286,41 @@ void styleToolIconButton(QToolButton* btn, const QIcon& icon) {
btn->setCursor(Qt::PointingHandCursor);
}
// core::Rgba → colorBar 颜色串(与 ColorScaleConfigDialog::rgbaToCss 同格式:不透明 #RRGGBB
// 半透明 rgba(r,g,b,a∈0..1)),与后端 colorBar 互通。
QString rgbaToColorBarCss(const geopro::core::Rgba& c) {
if (c.a >= 255)
return QStringLiteral("#%1%2%3")
.arg(c.r, 2, 16, QLatin1Char('0'))
.arg(c.g, 2, 16, QLatin1Char('0'))
.arg(c.b, 2, 16, QLatin1Char('0'))
.toUpper();
return QStringLiteral("rgba(%1, %2, %3, %4)")
.arg(c.r)
.arg(c.g)
.arg(c.b)
.arg(QString::number(c.a / 255.0, 'g', 3));
}
// 组装色阶 propertiescolorBar + lineConfig + labelConfig与原版散点路径
// newLvlColorLevel 一致battery/scatters 仅发这三块,不含 lvlSchemeType 等等值面专属字段)。
QJsonObject buildColorScaleProperties(const geopro::core::ColorScale& scale,
const ContourLineConfig& lineCfg) {
QJsonArray colorBar;
for (const auto& [value, color] : scale.stops())
colorBar.append(QJsonArray{QString::number(value, 'f', 2), rgbaToColorBarCss(color)});
QJsonObject lineConfig{
{QStringLiteral("showLines"), lineCfg.lineShow},
{QStringLiteral("color"), rgbaToColorBarCss(lineCfg.lineColor)},
{QStringLiteral("lineType"),
lineCfg.dashed ? QStringLiteral("dashed") : QStringLiteral("solid")}};
QJsonObject labelConfig{{QStringLiteral("showLabels"), lineCfg.labelShow},
{QStringLiteral("color"), rgbaToColorBarCss(lineCfg.labelColor)}};
return QJsonObject{{QStringLiteral("colorBar"), colorBar},
{QStringLiteral("lineConfig"), lineConfig},
{QStringLiteral("labelConfig"), labelConfig}};
}
} // namespace
void RawDataChartView::showNotImplemented(QWidget* anchor) {
@ -266,6 +330,333 @@ void RawDataChartView::showNotImplemented(QWidget* anchor) {
QToolTip::showText(pos, QStringLiteral("暂未实现"), anchor);
}
void RawDataChartView::setCommandRepo(geopro::data::IDatasetCommandRepository* repo,
std::function<QString()> dsIdGetter,
std::function<QString()> projectIdGetter) {
cmdRepo_ = repo;
dsIdGetter_ = std::move(dsIdGetter);
projectIdGetter_ = std::move(projectIdGetter);
}
void RawDataChartView::openInversionDialog(bool apparentResistivity, QWidget* anchor) {
// 无仓储/无 dsId 取值回调 → 退化占位(与未注入时一致)。
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) {
showNotImplemented(anchor);
return;
}
const auto mode = apparentResistivity ? InversionFormDialog::Mode::ApparentResistivity
: InversionFormDialog::Mode::Inversion;
InversionFormDialog dlg(mode, cmdRepo_, dsId, projectId, this);
dlg.exec(); // 提交成功/失败由对话框内部反馈;本视图无需后续刷新(原版亦仅提示)。
}
void RawDataChartView::openGridWizard(QWidget* anchor) {
// O1网格化向导与网格视图 I1 共用 GridWizardDialog。成功后散点视图无法渲染网格
// 故仅提示成功(用户切到网格页签查看,与原版「生成新网格数据后刷新」语义一致)。
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(anchor); return; }
GridWizardDialog dlg(cmdRepo_, dsId, this);
if (dlg.exec() == QDialog::Accepted)
QMessageBox::information(this, QStringLiteral("网格化"), QStringLiteral("网格化成功!"));
}
void RawDataChartView::openInversionColorScale(QWidget* anchor) {
// O2反演原数据散点色阶type1businessCode 空串,对照原版 originPage
if (data_.scale.empty()) { showNotImplemented(anchor); return; }
double vMin = std::numeric_limits<double>::max();
double vMax = std::numeric_limits<double>::lowest();
for (double v : data_.scatter.v) {
if (!std::isfinite(v)) continue;
if (v < vMin) vMin = v;
if (v > vMax) vMax = v;
}
if (vMin > vMax) { vMin = 0.0; vMax = 1.0; }
std::vector<double> samples = data_.scatter.v;
ColorScaleConfigDialog dlg(data_.scale, vMin, vMax, std::move(samples), {}, nullptr, {}, this);
if (dlg.exec() != QDialog::Accepted) return;
// 本地重建上色重绘。
data_.scale = dlg.colorScale();
delete colorSvc_;
colorSvc_ = new ColorMapService(data_.scale);
redrawScatter();
colorBar_->setColorScale(data_.scale);
// 持久化businessCode 空,对照原版 originPage newLvlColorLevel businessCode:'')。
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) return;
QJsonObject body{
{QStringLiteral("dsObjectId"), dsId},
{QStringLiteral("businessCode"), QString()},
{QStringLiteral("projectId"), projectId},
{QStringLiteral("properties"), buildColorScaleProperties(data_.scale, dlg.lineConfig())},
};
QPointer<RawDataChartView> self(this);
cmdRepo_->saveColorGradation(body, [self](bool ok, QString msg) {
if (!self || ok) return;
QMessageBox::warning(self, QStringLiteral("色阶配置"),
msg.isEmpty() ? QStringLiteral("色阶保存失败") : msg);
});
}
void RawDataChartView::openInversionSaveAs(QWidget* anchor) {
// O3另存为复用 SaveAsDialog::Inversion → saveInversionAsData
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(anchor); return; }
SaveAsDialog dlg(SaveAsDialog::Mode::Inversion, cmdRepo_, dsId, this);
dlg.exec();
}
QString RawDataChartView::currentVFieldCode() const {
if (vCombo_ && vCombo_->currentIndex() >= 0) {
const QString code = vCombo_->currentData().toString();
if (!code.isEmpty()) return code;
}
return QStringLiteral("R0"); // 默认视电阻率(与初始加载一致)
}
void RawDataChartView::redrawScatter() {
// 用当前 data_.scatter + colorSvc_ 重绘M7/M8 本地变换/色阶变更后复用)。
if (!scatterItem_ || !colorSvc_) return;
// 数据范围跟随当前 v 有限值 min/maxcauto与初始上色一致
double vMin = std::numeric_limits<double>::max();
double vMax = std::numeric_limits<double>::lowest();
for (double v : data_.scatter.v) {
if (!std::isfinite(v)) continue;
if (v < vMin) vMin = v;
if (v > vMax) vMax = v;
}
if (vMin <= vMax) colorSvc_->setDataRange(vMin, vMax);
scatterItem_->setData(data_.scatter, colorSvc_);
if (hoverTip_) hoverTip_->setField(&data_.scatter);
plot_->replot();
}
void RawDataChartView::onShowHide(bool hide) {
// popconfirm 确认(原版 a-popconfirm复刻确认文案。
const QString text = hide ? QStringLiteral("该操作将会把已选择的散点进行隐藏?")
: QStringLiteral("该操作将会显示所有已经隐藏的散点?");
const auto ans = QMessageBox::question(this, QStringLiteral("提示"), text,
QMessageBox::Ok | QMessageBox::Cancel);
if (ans != QMessageBox::Ok) return;
// 本地切换:显示/隐藏全部数据方块(电极保留)。
auto localToggle = [this, hide]() {
if (!scatterItem_) return;
scatterItem_->setScatterVisible(!hide);
plot_->replot();
};
// 无仓储/无 dsId → 仅本地切换(退化,不持久化)。
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { localToggle(); return; }
// 收集要持久化的点 id隐藏取可见点 / 显示取隐藏点status0=显示 1=隐藏。
const QJsonArray ids = collectScatterIds(data_.scatter, hide);
const int status = hide ? 1 : 0;
QPointer<RawDataChartView> self(this);
cmdRepo_->saveDisplayStatus(dsId, ids, status, [self, hide, localToggle](bool ok, QString msg) {
if (!self) return;
if (!ok) {
QMessageBox::warning(self, QStringLiteral("提示"),
msg.isEmpty() ? QStringLiteral("操作失败") : msg);
return;
}
// 持久化成功后同步本地 displayStatus 与方块可见性。
const int newStatus = hide ? 1 : 0;
for (int& s : self->data_.scatter.displayStatus) s = newStatus;
localToggle();
});
}
void RawDataChartView::openFilterDialog(QWidget* anchor) {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(anchor); return; }
ScatterFilterDialog dlg(cmdRepo_, dsId, currentVFieldCode(), this);
dlg.exec(); // 成功/失败由对话框内部反馈(生成过滤数据集为后端动作)。
}
void RawDataChartView::reloadForVValue() {
// V 值切换:重新请求散点 + 色阶(原版 vValueType change
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(vCombo_); return; }
QPointer<RawDataChartView> self(this);
const QString vCode = currentVFieldCode();
cmdRepo_->loadMeasurementScatter(
dsId, vCode, [self](bool ok, geopro::core::ScatterPayload payload, QString msg) {
if (!self) return;
if (!ok) {
QMessageBox::warning(self, QStringLiteral("提示"),
msg.isEmpty() ? QStringLiteral("加载失败") : msg);
return;
}
// 保留已建 measurement 工具条(不重建):只换数据 + 色阶。setData 会重置 baseV_。
self->setData(payload);
// 切 V 后值类型回到线性(与原版重新请求后默认线性一致)。
if (self->valueTypeCombo_) {
const QSignalBlocker block(self->valueTypeCombo_);
self->valueTypeCombo_->setCurrentIndex(0);
}
});
}
void RawDataChartView::applyValueType() {
// 本地值类型变换(线性/倒数/对数):从 baseV_ 算,重新上色重绘(无后端)。
if (!valueTypeCombo_ || baseV_.empty()) return;
const ScatterValueType type = scatterValueTypeFromCode(valueTypeCombo_->currentData().toString());
data_.scatter.v = applyScatterValueType(baseV_, type);
redrawScatter();
}
void RawDataChartView::openScatterColorScale(QWidget* anchor) {
if (data_.scale.empty()) { showNotImplemented(anchor); return; }
// 数据范围取当前 v 有限值 min/max散点上色 cauto 区间)。
double vMin = std::numeric_limits<double>::max();
double vMax = std::numeric_limits<double>::lowest();
for (double v : data_.scatter.v) {
if (!std::isfinite(v)) continue;
if (v < vMin) vMin = v;
if (v > vMax) vMax = v;
}
if (vMin > vMax) { vMin = 0.0; vMax = 1.0; }
std::vector<double> samples = data_.scatter.v; // 直方图/等积分层用原始标量
// 散点无独立 lvl 模板仓储(视图只持命令仓储)→ tplRepo 传空(另存为/打开 禁用)。
ColorScaleConfigDialog dlg(data_.scale, vMin, vMax, std::move(samples), {}, nullptr, {}, this);
if (dlg.exec() != QDialog::Accepted) return;
// 本地重建 colorSvc_ 重绘散点M8 即时生效)。
data_.scale = dlg.colorScale();
delete colorSvc_;
colorSvc_ = new ColorMapService(data_.scale);
redrawScatter();
// 同步右侧竖条/底部横条色阶图例。
if (data_.verticalLegend) colorBarV_->setColorScale(data_.scale);
else colorBar_->setColorScale(data_.scale);
// 持久化到后端saveColorGradationbusinessCode=当前 V 值type=3 散点路径)。
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) return; // 无仓储 → 仅本地生效(不阻塞)
QJsonObject body{
{QStringLiteral("dsObjectId"), dsId},
{QStringLiteral("businessCode"), currentVFieldCode()},
{QStringLiteral("projectId"), projectId},
{QStringLiteral("properties"), buildColorScaleProperties(data_.scale, dlg.lineConfig())},
};
QPointer<RawDataChartView> self(this);
cmdRepo_->saveColorGradation(body, [self](bool ok, QString msg) {
if (!self || ok) return;
QMessageBox::warning(self, QStringLiteral("色阶配置"),
msg.isEmpty() ? QStringLiteral("色阶保存失败") : msg);
});
}
void RawDataChartView::openSaveAs(QWidget* anchor) {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(anchor); return; }
SaveAsDialog dlg(SaveAsDialog::Mode::RawData, cmdRepo_, dsId, this);
dlg.exec(); // 成功/失败由对话框内部反馈。
}
void RawDataChartView::exportDat() {
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
// 参数与原版 exportScatterData2Dat 一致electrodePosition=2, ipDataMark=0, typeMeasurement=0。
QPointer<RawDataChartView> self(this);
cmdRepo_->exportMeasurementDat(
dsId, /*electrodePosition*/ 2, /*ipDataMark*/ 0, /*typeMeasurement*/ 0,
[self](bool ok, QString fileName, QString fileData, QString msg) {
if (!self) return;
if (!ok) {
QMessageBox::warning(self, QStringLiteral("导出"),
msg.isEmpty() ? QStringLiteral("导出失败!") : msg);
return;
}
const QString suggested = fileName.isEmpty() ? QStringLiteral("export.dat") : fileName;
const QString path = QFileDialog::getSaveFileName(
self, QStringLiteral("导出 DAT"), suggested, QStringLiteral("DAT 文件 (*.dat)"));
if (path.isEmpty()) return;
const QByteArray bytes = QByteArray::fromBase64(fileData.toUtf8());
QSaveFile f(path);
if (!f.open(QIODevice::WriteOnly) || f.write(bytes) != bytes.size() || !f.commit()) {
QMessageBox::warning(self, QStringLiteral("导出"), QStringLiteral("写入文件失败。"));
}
});
}
void RawDataChartView::toggleInfoMode(bool on) {
infoMode_ = on;
if (on && !infoPanel_) {
// 首次开启:建覆盖在图区右上角的属性面板(复刻原版 .scatterInfos 浮层)。
infoPanel_ = new QWidget(plot_->canvas());
infoPanel_->setObjectName(QStringLiteral("scatterInfoPanel"));
auto* il = new QVBoxLayout(infoPanel_);
il->setContentsMargins(8, 6, 8, 6);
infoLabel_ = new QLabel(QStringLiteral("点选散点查看属性"), infoPanel_);
il->addWidget(infoLabel_);
applyTokenizedStyleSheet(
infoPanel_,
QStringLiteral("QWidget#scatterInfoPanel { background: {{bg/panel}};"
" border: 1px solid {{border/default}}; border-radius: 6px; }"
"QLabel { color: {{text/primary}}; }"));
// 画布事件过滤器:信息模式下点击找最近点显示属性。
plot_->canvas()->installEventFilter(this);
}
if (infoPanel_) {
infoPanel_->setVisible(on);
if (on) {
infoPanel_->adjustSize();
infoPanel_->move(plot_->canvas()->width() - infoPanel_->width() - 10, 10);
infoPanel_->raise();
}
}
}
void RawDataChartView::showPointInfoAt(const QPoint& canvasPos) {
if (!infoLabel_ || data_.scatter.x.empty()) return;
const QwtScaleMap xMap = plot_->canvasMap(QwtPlot::xTop);
const QwtScaleMap yMap = plot_->canvasMap(QwtPlot::yLeft);
const auto& s = data_.scatter;
const std::size_t n = std::min(s.x.size(), s.y.size());
double bestD2 = std::numeric_limits<double>::max();
std::size_t bestI = 0;
for (std::size_t i = 0; i < n; ++i) {
const double dx = xMap.transform(s.x[i]) - canvasPos.x();
const double dy = yMap.transform(s.y[i]) - canvasPos.y();
const double d2 = dx * dx + dy * dy;
if (d2 < bestD2) { bestD2 = d2; bestI = i; }
}
constexpr double kHit = 8.0; // 命中半径(像素)
if (bestD2 > kHit * kHit) return; // 未命中任何点 → 保持当前显示
auto at = [](const std::vector<double>& v, std::size_t i) {
return i < v.size() ? v[i] : 0.0;
};
// 复刻原版 scatterInfosA / B / M / N / DataRow / Pseu_Resis。
infoLabel_->setText(QStringLiteral("A= %1\nB= %2\nM= %3\nN= %4\nDataRow= %5\nPseu_Resis= %6")
.arg(QString::number(at(s.a, bestI), 'g', 6),
QString::number(at(s.b, bestI), 'g', 6),
QString::number(at(s.m, bestI), 'g', 6),
QString::number(at(s.n, bestI), 'g', 6),
QString::number(at(s.row, bestI), 'g', 6),
QString::number(at(s.pseu, bestI), 'g', 6)));
infoPanel_->adjustSize();
infoPanel_->move(plot_->canvas()->width() - infoPanel_->width() - 10, 10);
infoPanel_->raise();
}
bool RawDataChartView::eventFilter(QObject* obj, QEvent* ev) {
// 仅信息模式 + 画布左键点击:找最近散点显示属性,不消费事件(保留平移链路)。
if (infoMode_ && plot_ && obj == plot_->canvas() && ev->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(ev);
if (me->button() == Qt::LeftButton) showPointInfoAt(me->position().toPoint());
}
return QWidget::eventFilter(obj, ev);
}
void RawDataChartView::replotForAxis() {
// 本地换 x/y无网络按下拉 fieldCode 从备选列取数据,重设 scatter.x/.y 并重绘。
if (!xCombo_ || !yCombo_ || !scatterItem_) return;
@ -305,73 +696,91 @@ void RawDataChartView::buildMeasurementToolbar(const geopro::core::ScatterToolba
auto* btnInfo = new QToolButton(toolbar);
btnInfo->setToolTip(QStringLiteral("信息"));
styleToolIconButton(btnInfo, makeInfoIcon());
connect(btnInfo, &QToolButton::clicked, this, [this, btnInfo]() { showNotImplemented(btnInfo); });
auto* btnMarquee = new QToolButton(toolbar);
btnMarquee->setToolTip(QStringLiteral("框选"));
styleToolIconButton(btnMarquee, makeMarqueeIcon());
connect(btnMarquee, &QToolButton::clicked, this, [this, btnMarquee]() { showNotImplemented(btnMarquee); });
// 主题热切重绘图标info 锚定品牌蓝marquee 描边随次要文本色)。
connect(&ThemeManager::instance(), &ThemeManager::changed, btnInfo,
[btnInfo]() { btnInfo->setIcon(makeInfoIcon()); });
connect(&ThemeManager::instance(), &ThemeManager::changed, btnMarquee,
[btnMarquee]() { btnMarquee->setIcon(makeMarqueeIcon()); });
// 显示 / 隐藏:功能性——切换全部数据方块可见性(电极保留)。
// [i] 信息:切换信息模式(点选散点看 A/B/M/N/DataRow/Pseu_Resis
btnInfo->setCheckable(true);
connect(btnInfo, &QToolButton::toggled, this, [this](bool on) { toggleInfoMode(on); });
// [▣] 框选本轮后置Qwt 橡皮筋框选 + 选区联动隐藏成本较高),保持占位提示。
connect(btnMarquee, &QToolButton::clicked, this,
[this, btnMarquee]() { showNotImplemented(btnMarquee); });
// 显示 / 隐藏popconfirm 确认 → saveDisplayStatus 持久化 → 本地切换M1
auto* btnShow = new QPushButton(QStringLiteral("显示"), toolbar);
auto* btnHide = new QPushButton(QStringLiteral("隐藏"), toolbar);
connect(btnShow, &QPushButton::clicked, this, [this]() {
if (scatterItem_) { scatterItem_->setScatterVisible(true); plot_->replot(); }
});
connect(btnHide, &QPushButton::clicked, this, [this]() {
if (scatterItem_) { scatterItem_->setScatterVisible(false); plot_->replot(); }
});
connect(btnShow, &QPushButton::clicked, this, [this]() { onShowHide(/*hide*/ false); });
connect(btnHide, &QPushButton::clicked, this, [this]() { onShowHide(/*hide*/ true); });
// 数据过滤:占位(暂未实现)。
// 数据过滤范围过滤弹窗M3
auto* btnFilter = new QPushButton(QStringLiteral("数据过滤"), toolbar);
connect(btnFilter, &QPushButton::clicked, this, [this, btnFilter]() { showNotImplemented(btnFilter); });
connect(btnFilter, &QPushButton::clicked, this, [this, btnFilter]() { openFilterDialog(btnFilter); });
// x / y 下拉功能性本地换列重绘v / method 下拉:视觉占位(选不同 v/method 提示暂未实现)。
// 导出 DAT原版在页头「导出」客户端页头为占位且跨 dd 共用,故 measurement 专属导出
// 收纳进本工具条M12。点击 → exportMeasurementDat → 选路径写盘。
auto* btnExport = new QPushButton(QStringLiteral("导出"), toolbar);
connect(btnExport, &QPushButton::clicked, this, [this]() { exportDat(); });
// x / y 下拉本地换列重绘v 下拉:重新请求散点+色阶M6值类型下拉本地变换M7
xCombo_ = new QComboBox(toolbar);
fillCombo(xCombo_, conf.x, conf.defaultX, QString());
yCombo_ = new QComboBox(toolbar);
fillCombo(yCombo_, conf.y, conf.defaultY, QString());
auto* vCombo = new QComboBox(toolbar);
fillCombo(vCombo, conf.v, conf.defaultV, QString());
auto* methodCombo = new QComboBox(toolbar);
fillCombo(methodCombo, conf.method, QString(), conf.defaultMethod);
vCombo_ = new QComboBox(toolbar);
fillCombo(vCombo_, conf.v, conf.defaultV, QString());
valueTypeCombo_ = new QComboBox(toolbar);
// 值类型固定三项(原版 linearity/inverse/logarithm本地变换无后端。
valueTypeCombo_->addItem(QStringLiteral("线性"), QStringLiteral("linearity"));
valueTypeCombo_->addItem(QStringLiteral("倒数"), QStringLiteral("inverse"));
valueTypeCombo_->addItem(QStringLiteral("对数"), QStringLiteral("logarithm"));
connect(xCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this](int) { replotForAxis(); });
connect(yCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this](int) { replotForAxis(); });
// v / method换选项 → 暂未实现(需重新请求散点/色阶,属重交互,本轮不做)。
connect(vCombo, QOverload<int>::of(&QComboBox::activated), this,
[this, vCombo](int) { showNotImplemented(vCombo); });
connect(methodCombo, QOverload<int>::of(&QComboBox::activated), this,
[this, methodCombo](int) { showNotImplemented(methodCombo); });
// V 值切换:重新请求散点+色阶(用 activated 仅用户操作触发,填充期不误触)。
connect(vCombo_, QOverload<int>::of(&QComboBox::activated), this,
[this](int) { reloadForVValue(); });
connect(valueTypeCombo_, QOverload<int>::of(&QComboBox::activated), this,
[this](int) { applyValueType(); });
// 色阶配置:占位(暂未实现)。
// 色阶配置:复用 ColorScaleConfigDialog散点上色路径保存 + 本地重绘M8)。
auto* btnColorScale = new QPushButton(QStringLiteral("色阶配置"), toolbar);
connect(btnColorScale, &QPushButton::clicked, this, [this, btnColorScale]() { showNotImplemented(btnColorScale); });
connect(btnColorScale, &QPushButton::clicked, this,
[this, btnColorScale]() { openScatterColorScale(btnColorScale); });
// 右侧主操作(蓝色):生成视电阻率数据 / 反演运算 / 另存为 —— 占位(暂未实现)
// 右侧主操作(蓝色):生成视电阻率数据 / 反演运算 / 另存为
auto* btnGen = new QPushButton(QStringLiteral("生成视电阻率数据"), toolbar);
auto* btnInvert = new QPushButton(QStringLiteral("反演运算"), toolbar);
auto* btnSaveAs = new QPushButton(QStringLiteral("另存为"), toolbar);
for (auto* b : {btnGen, btnInvert, btnSaveAs}) {
b->setObjectName(QStringLiteral("primaryBtn")); // 蓝色主按钮(下方 QSS
connect(b, &QPushButton::clicked, this, [this, b]() { showNotImplemented(b); });
}
// 反演运算 → submitInversionTask生成视电阻率 → createVisualResistivityData共享反演对话框
connect(btnInvert, &QPushButton::clicked, this,
[this, btnInvert]() { openInversionDialog(/*apparentResistivity*/ false, btnInvert); });
connect(btnGen, &QPushButton::clicked, this,
[this, btnGen]() { openInversionDialog(/*apparentResistivity*/ true, btnGen); });
// 另存为:新增/覆盖弹窗 → saveRawDataM11
connect(btnSaveAs, &QPushButton::clicked, this,
[this, btnSaveAs]() { openSaveAs(btnSaveAs); });
tbLay->addWidget(btnInfo);
tbLay->addWidget(btnMarquee);
tbLay->addWidget(btnShow);
tbLay->addWidget(btnHide);
tbLay->addWidget(btnFilter);
tbLay->addWidget(btnExport);
tbLay->addWidget(xCombo_);
tbLay->addWidget(yCombo_);
tbLay->addWidget(vCombo);
tbLay->addWidget(methodCombo);
tbLay->addWidget(vCombo_);
tbLay->addWidget(valueTypeCombo_);
tbLay->addWidget(btnColorScale);
tbLay->addStretch(); // 把主操作推到右侧
tbLay->addWidget(btnGen);
@ -405,6 +814,7 @@ void RawDataChartView::setPayload(const QVariant& payload) {
void RawDataChartView::setData(const geopro::core::ScatterPayload& p) {
data_ = p;
baseV_ = data_.scatter.v; // 缓存原始 v线性M7 值类型变换从原值算,不累积误差
if (hoverTip_) hoverTip_->setField(&data_.scatter); // 显式重绑(地址稳定,消除隐式依赖)
// measurement 载荷toolbar 非空):首次到来时建并替换工具条(视觉 1:1。反演留空 → 不动。

View File

@ -1,4 +1,7 @@
#pragma once
#include <functional>
#include <QString>
#include <QWidget>
#include "model/detail/DetailPayloads.hpp"
#include "panels/chart/ColorMapService.hpp"
@ -6,9 +9,14 @@
class QComboBox;
class QVBoxLayout;
class QLabel;
class QwtPlot;
class QwtPlotRescaler;
namespace geopro::data {
class IDatasetCommandRepository;
}
namespace geopro::app {
class ColorBarWidget;
@ -32,6 +40,16 @@ public:
// 供外部访问(已不再是占位,保留兼容接口返回 plot_
QWidget* plotArea() const;
// 注入反演命令仓储 + dsId/projectId 取值回调measurement 反演运算/生成视电阻率按钮)。
// 可传空仓储 → 两按钮退化为「暂未实现」占位提示。
void setCommandRepo(geopro::data::IDatasetCommandRepository* repo,
std::function<QString()> dsIdGetter,
std::function<QString()> projectIdGetter);
protected:
// 信息模式M13下捕获画布点击找最近散点显示属性。其余事件不消费。
bool eventFilter(QObject* obj, QEvent* ev) override;
private:
// 工具条按载荷类型二选一:反演原数据 = ctor 默认建的 inversion 工具条measurement =
// 首个非空 ScatterToolbarConf 到来时建一次并替换(视觉 1:1。建好后缓存后续 setData 复用。
@ -40,6 +58,27 @@ private:
void replotForAxis();
// “暂未实现”轻提示(占位按钮/下拉点击)。
void showNotImplemented(QWidget* anchor);
// 打开反演动态表单对话框(反演运算/生成视电阻率共享)。无仓储/无 dsId → 占位提示。
void openInversionDialog(bool apparentResistivity, QWidget* anchor);
// 反演原数据默认工具条交互O1/O2/O3
void openGridWizard(QWidget* anchor); // O1 网格化向导(复用 GridWizardDialog
void openInversionColorScale(QWidget* anchor); // O2 原数据散点色阶type1businessCode=''
void openInversionSaveAs(QWidget* anchor); // O3 另存为(复用 SaveAsDialog::Inversion
// measurement 交互:
void onShowHide(bool hide); // M1 显示/隐藏popconfirm→持久化→本地切
void openFilterDialog(QWidget* anchor); // M3 数据过滤
void reloadForVValue(); // M6 V 值切换重新请求散点+色阶
void applyValueType(); // M7 值类型本地变换重新上色
void openScatterColorScale(QWidget* anchor); // M8 色阶配置(编辑+保存+重绘)
void openSaveAs(QWidget* anchor); // M11 另存为
void exportDat(); // M12 导出 DAT
void toggleInfoMode(bool on); // M13 [i] 信息模式开关
void showPointInfoAt(const QPoint& canvasPos); // M13 点选显示属性
// 用 colorSvc_ 重绘当前散点M7/M8 本地变换/色阶变更后复用)。
void redrawScatter();
QString currentVFieldCode() const; // 当前 V 值下拉 fieldCode
geopro::core::ScatterPayload data_;
QwtPlot* plot_;
@ -53,11 +92,25 @@ private:
bool measurementToolbar_ = false; // 已建 measurement 工具条
QComboBox* xCombo_ = nullptr; // measurement x 下拉
QComboBox* yCombo_ = nullptr; // measurement y 下拉
QComboBox* vCombo_ = nullptr; // measurement V 值下拉M6 重载)
QComboBox* valueTypeCombo_ = nullptr; // measurement 值类型下拉M7 本地变换)
// M7 值类型:保留原始 v线性以便倒数/对数从原值变换,不累积误差。
std::vector<double> baseV_;
// M13 [i]信息:信息模式开关 + 覆盖在图区右上的属性面板。
bool infoMode_ = false;
QWidget* infoPanel_ = nullptr; // 属性覆盖面板A/B/M/N/DataRow/Pseu_Resis
QLabel* infoLabel_ = nullptr;
// 使用 unique_ptr 管理生命周期attach 后 QwtPlot 接管绘制,但我们仍持有指针
ColorMapService* colorSvc_ = nullptr; // heap由 setData 重建
ScatterPlotItem* scatterItem_ = nullptr;
ScatterHoverTip* hoverTip_ = nullptr; // 散点 hover 提示QObjectthis 持有)
// 反演命令仓储 + dsId/projectId 取值回调(注入;空则反演按钮占位)。
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
std::function<QString()> dsIdGetter_;
std::function<QString()> projectIdGetter_;
};
} // namespace geopro::app

View File

@ -0,0 +1,110 @@
#include "panels/chart/SaveAsDialog.hpp"
#include <utility>
#include <QButtonGroup>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPointer>
#include <QPushButton>
#include <QRadioButton>
#include <QVBoxLayout>
#include "FormKit.hpp"
#include "panels/chart/ScatterDataOps.hpp" // buildSaveRawDataBody纯组装便于单测
#include "repo/IDatasetCommandRepository.hpp"
namespace geopro::app {
SaveAsDialog::SaveAsDialog(Mode mode, geopro::data::IDatasetCommandRepository* repo, QString dsId,
QWidget* parent)
: QDialog(parent), mode_(mode), repo_(repo), dsId_(std::move(dsId)) {
setWindowTitle(QStringLiteral("数据另存为"));
setModal(true);
auto* root = formkit::dialogRoot(this);
auto* card = formkit::formCard(this);
auto* cardLay = formkit::cardBody(card);
if (mode_ == Mode::RawData) {
// 新增/覆盖单选(复刻原版 a-radio-group1=新增 0=覆盖)。
auto* opLay = new QHBoxLayout();
auto* rbNew = new QRadioButton(QStringLiteral("新增"), this);
auto* rbOverwrite = new QRadioButton(QStringLiteral("覆盖"), this);
opGroup_ = new QButtonGroup(this);
opGroup_->addButton(rbNew, 1);
opGroup_->addButton(rbOverwrite, 0);
rbNew->setChecked(true); // 默认新增(与原版 dataStored 初值一致)
opLay->addWidget(rbNew);
opLay->addWidget(rbOverwrite);
opLay->addStretch();
cardLay->addLayout(opLay);
}
// 名称行RawData 仅新增可见Inversion 始终可见。
auto* nameForm = formkit::makeEditForm();
nameLabel_ = formkit::editLabel(QStringLiteral("数据名称"), this);
nameEdit_ = new QLineEdit(this);
formkit::capField(nameEdit_);
nameForm->addRow(nameLabel_, nameEdit_);
cardLay->addLayout(nameForm);
root->addWidget(card);
if (mode_ == Mode::RawData && opGroup_) {
// 切到覆盖隐藏名称框,切回新增显示(复刻原版 v-show=dataStored===1
connect(opGroup_, QOverload<int>::of(&QButtonGroup::idClicked), this, [this](int id) {
const bool isNew = (id == 1);
nameLabel_->setVisible(isNew);
nameEdit_->setVisible(isNew);
});
}
auto* btnLay = new QHBoxLayout();
btnLay->addStretch();
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
okBtn_ = new QPushButton(QStringLiteral("确定"), this);
okBtn_->setDefault(true);
btnLay->addWidget(cancelBtn);
btnLay->addWidget(okBtn_);
root->addLayout(btnLay);
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
connect(okBtn_, &QPushButton::clicked, this, &SaveAsDialog::onConfirm);
}
void SaveAsDialog::onConfirm() {
if (!repo_) { reject(); return; }
// RawData 新增 / Inversion 均需名称(覆盖不需)。
const int operationType = opGroup_ ? opGroup_->checkedId() : 1;
const bool needName = (mode_ == Mode::Inversion) || (operationType == 1);
const QString name = nameEdit_->text().trimmed();
if (needName && name.isEmpty()) {
QMessageBox::warning(this, windowTitle(), QStringLiteral("请输入数据名称"));
return;
}
okBtn_->setEnabled(false);
QPointer<SaveAsDialog> self(this);
auto onDone = [self](bool ok, QString msg) {
if (!self) return;
self->okBtn_->setEnabled(true);
if (ok) {
self->accept();
} else {
QMessageBox::warning(self, self->windowTitle(),
msg.isEmpty() ? QStringLiteral("保存失败") : msg);
}
};
if (mode_ == Mode::Inversion) {
repo_->saveInversionAsData(dsId_, name, onDone);
} else {
repo_->saveRawData(buildSaveRawDataBody(dsId_, operationType, name), onDone);
}
}
} // namespace geopro::app

View File

@ -0,0 +1,50 @@
#pragma once
#include <functional>
#include <QDialog>
#include <QJsonObject>
#include <QString>
class QLineEdit;
class QButtonGroup;
class QLabel;
class QPushButton;
namespace geopro::data {
class IDatasetCommandRepository;
}
namespace geopro::app {
// 「另存为」对话框1:1 复刻原版 web两形态可复用
// - RawDatameasurement「另存为」原版 saveRawDataValue新增/覆盖单选 +(新增时)名称框。
// 提交体 {dsId, operationType(1新增/0覆盖), name?}(覆盖不带 name
// - Inversioninversion「另存为」原版 saveVisualResistivityData仅名称框。
// 提交体 {dsObjectId, name}。
// 设计成可复用Mode 区分两形态;提交统一经 IDatasetCommandRepository::saveRawData /
// saveInversionAsData。回调用 QPointer 守卫(虽 modal exec仍异步回调
class SaveAsDialog : public QDialog {
Q_OBJECT
public:
enum class Mode {
RawData, // measurement 另存为(新增/覆盖 + 名称)
Inversion, // inversion 另存为(仅名称)
};
SaveAsDialog(Mode mode, geopro::data::IDatasetCommandRepository* repo, QString dsId,
QWidget* parent = nullptr);
private:
void onConfirm();
Mode mode_;
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
QString dsId_;
QButtonGroup* opGroup_ = nullptr; // RawData新增(1)/覆盖(0)
QLabel* nameLabel_ = nullptr; // RawData仅新增可见
QLineEdit* nameEdit_ = nullptr;
QPushButton* okBtn_ = nullptr;
};
} // namespace geopro::app

View File

@ -0,0 +1,68 @@
#include "panels/chart/ScatterDataOps.hpp"
#include <cmath>
namespace geopro::app {
ScatterValueType scatterValueTypeFromCode(const QString& code) {
if (code == QStringLiteral("inverse")) return ScatterValueType::Inverse;
if (code == QStringLiteral("logarithm")) return ScatterValueType::Logarithm;
return ScatterValueType::Linearity; // 'linearity' 及未知
}
std::vector<double> applyScatterValueType(const std::vector<double>& v, ScatterValueType type) {
std::vector<double> out;
out.reserve(v.size());
for (double x : v) {
switch (type) {
case ScatterValueType::Inverse:
out.push_back(x == 0.0 ? 0.0 : 1.0 / x);
break;
case ScatterValueType::Logarithm:
// v<=0 无定义:保持原值(避免 NaN/-inf 污染数据范围→全图取 NaN 色)。
out.push_back(x > 0.0 ? std::log10(x) : x);
break;
case ScatterValueType::Linearity:
default:
out.push_back(x);
break;
}
}
return out;
}
QJsonArray collectScatterIds(const geopro::core::ScatterField& field, bool hide) {
QJsonArray ids;
const std::size_t n = field.id.size();
for (std::size_t i = 0; i < n; ++i) {
const std::string& id = field.id[i];
if (id.empty()) continue;
// displayStatus 缺省视为可见(0)。
const int status = i < field.displayStatus.size() ? field.displayStatus[i] : 0;
const bool visible = (status == 0);
// 隐藏:取可见点;显示:取隐藏点。
if (hide == visible) ids.append(QString::fromStdString(id));
}
return ids;
}
QJsonObject buildScatterFilterBody(const QString& dsObjectId, const QString& vFieldCode, double min,
double max) {
return QJsonObject{
{QStringLiteral("sourceDsObjectId"), dsObjectId},
{QStringLiteral("sourceVFieldCode"), vFieldCode},
{QStringLiteral("min"), min},
{QStringLiteral("max"), max},
};
}
QJsonObject buildSaveRawDataBody(const QString& dsId, int operationType, const QString& name) {
QJsonObject body{
{QStringLiteral("dsId"), dsId},
{QStringLiteral("operationType"), operationType},
};
if (operationType == 1) body.insert(QStringLiteral("name"), name); // 新增才带名称
return body;
}
} // namespace geopro::app

View File

@ -0,0 +1,43 @@
#pragma once
#include <vector>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include "model/Field.hpp"
namespace geopro::app {
// measurement 散点的纯逻辑(仅依赖 QtCore JSON + core model无 Widgets/MOC
// 拆出独立 TU 以便单测(与 InversionFormParse 同范式)。
// 值类型变换M7 值类型下拉:线性 / 倒数 / 对数)。原版本地变换显示,无后端。
enum class ScatterValueType {
Linearity, // 线性:原值 v
Inverse, // 倒数1/vv==0 → 保持 0避免 inf
Logarithm, // 对数log10(v)v<=0 → 保持原值,避免 NaN/-inf 污染色阶范围)
};
// fieldCode'linearity'/'inverse'/'logarithm')→ 枚举;未知回退线性。
ScatterValueType scatterValueTypeFromCode(const QString& code);
// 对一组 v 值应用变换,返回新数组(不可变:不改入参)。
std::vector<double> applyScatterValueType(const std::vector<double>& v, ScatterValueType type);
// 收集「显示/隐藏」要持久化的点 idM1
// hide=true → 收集当前可见(displayStatus==0)的点 id原版隐藏全部已选/可见点)。
// hide=false → 收集当前隐藏(displayStatus!=0)的点 id原版显示全部已隐藏点
// id 为空串的点跳过(无效)。
QJsonArray collectScatterIds(const geopro::core::ScatterField& field, bool hide);
// 组装散点过滤请求体M3applyScatterFilter
// {sourceDsObjectId, sourceVFieldCode, min, max}(字段名对照原版 applyScatterFilterInfo
QJsonObject buildScatterFilterBody(const QString& dsObjectId, const QString& vFieldCode,
double min, double max);
// 组装 measurement「另存为」请求体M11saveRawData对照原版 saveRawDataValue
// {dsId, operationType(1新增/0覆盖)},仅新增(operationType==1)才带 name。
QJsonObject buildSaveRawDataBody(const QString& dsId, int operationType, const QString& name);
} // namespace geopro::app

View File

@ -0,0 +1,113 @@
#include "panels/chart/ScatterFilterDialog.hpp"
#include <utility>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QPointer>
#include <QPushButton>
#include <QVBoxLayout>
#include "FormKit.hpp"
#include "panels/chart/ScatterDataOps.hpp" // buildScatterFilterBody
#include "repo/IDatasetCommandRepository.hpp"
namespace geopro::app {
namespace {
constexpr double kSpinRange = 1e12; // 数值范围足够宽,覆盖电阻率/电位等量纲
} // namespace
ScatterFilterDialog::ScatterFilterDialog(geopro::data::IDatasetCommandRepository* repo,
QString dsObjectId, QString vFieldCode, QWidget* parent)
: QDialog(parent),
repo_(repo),
dsObjectId_(std::move(dsObjectId)),
vFieldCode_(std::move(vFieldCode)) {
setWindowTitle(QStringLiteral("数据过滤"));
setModal(true);
resize(360, 200);
auto* root = formkit::dialogRoot(this);
rangeLabel_ = new QLabel(QStringLiteral("数值范围:—"), this);
root->addWidget(rangeLabel_);
auto* card = formkit::formCard(this);
auto* cardLay = formkit::cardBody(card);
auto* form = formkit::makeEditForm();
minSpin_ = new QDoubleSpinBox(this);
minSpin_->setRange(-kSpinRange, kSpinRange);
minSpin_->setDecimals(2);
formkit::capField(minSpin_);
maxSpin_ = new QDoubleSpinBox(this);
maxSpin_->setRange(-kSpinRange, kSpinRange);
maxSpin_->setDecimals(2);
formkit::capField(maxSpin_);
form->addRow(formkit::editLabel(QStringLiteral("最小值")), minSpin_);
form->addRow(formkit::editLabel(QStringLiteral("最大值")), maxSpin_);
cardLay->addLayout(form);
root->addWidget(card);
auto* btnLay = new QHBoxLayout();
btnLay->addStretch();
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
applyBtn_ = new QPushButton(QStringLiteral("应用过滤"), this);
applyBtn_->setDefault(true);
btnLay->addWidget(cancelBtn);
btnLay->addWidget(applyBtn_);
root->addLayout(btnLay);
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
connect(applyBtn_, &QPushButton::clicked, this, &ScatterFilterDialog::onApply);
loadConfig();
}
void ScatterFilterDialog::loadConfig() {
if (!repo_) return;
QPointer<ScatterFilterDialog> self(this);
repo_->getScatterFilterConfig(
dsObjectId_, vFieldCode_, [self](bool ok, QJsonObject cfg, QString) {
if (!self || !ok) return;
const double mn = cfg.value(QStringLiteral("min")).toDouble();
const double mx = cfg.value(QStringLiteral("max")).toDouble();
self->minSpin_->setValue(mn);
self->maxSpin_->setValue(mx);
self->rangeLabel_->setText(QStringLiteral("数值范围:%1 — %2")
.arg(QString::number(mn, 'g', 6),
QString::number(mx, 'g', 6)));
});
}
void ScatterFilterDialog::onApply() {
if (!repo_) { reject(); return; }
const double mn = minSpin_->value();
const double mx = maxSpin_->value();
if (mn > mx) {
QMessageBox::warning(this, windowTitle(), QStringLiteral("最小值不能大于最大值"));
return;
}
applyBtn_->setEnabled(false);
QPointer<ScatterFilterDialog> self(this);
repo_->applyScatterFilter(
buildScatterFilterBody(dsObjectId_, vFieldCode_, mn, mx), [self](bool ok, QString msg) {
if (!self) return;
self->applyBtn_->setEnabled(true);
if (ok) {
QMessageBox::information(self, self->windowTitle(),
QStringLiteral("应用过滤成功!"));
self->accept();
} else {
QMessageBox::warning(self, self->windowTitle(),
msg.isEmpty() ? QStringLiteral("应用过滤失败") : msg);
}
});
}
} // namespace geopro::app

View File

@ -0,0 +1,40 @@
#pragma once
#include <QDialog>
#include <QString>
class QDoubleSpinBox;
class QLabel;
class QPushButton;
namespace geopro::data {
class IDatasetCommandRepository;
}
namespace geopro::app {
// 「数据过滤」对话框(复刻原版 web dataFilter.vue 的范围过滤部分):
// 打开时经 getScatterFilterConfig 取 min/max 初值填入「最小值/最大值」输入框;
// 「应用过滤」经 applyScatterFilter 生成过滤后数据集({sourceDsObjectId, sourceVFieldCode, min, max})。
// 直方图(原版左侧 D3 分布图)本轮后置:范围过滤为核心,直方图仅可视化辅助。
// 回调用 QPointer 守卫(虽 modal exec仍异步回调
class ScatterFilterDialog : public QDialog {
Q_OBJECT
public:
ScatterFilterDialog(geopro::data::IDatasetCommandRepository* repo, QString dsObjectId,
QString vFieldCode, QWidget* parent = nullptr);
private:
void loadConfig(); // 取 min/max 初值
void onApply(); // 应用过滤
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
QString dsObjectId_;
QString vFieldCode_;
QLabel* rangeLabel_ = nullptr; // 「数值范围min — max」
QDoubleSpinBox* minSpin_ = nullptr;
QDoubleSpinBox* maxSpin_ = nullptr;
QPushButton* applyBtn_ = nullptr;
};
} // namespace geopro::app

View File

@ -0,0 +1,173 @@
#include "panels/chart/WhiteningDialog.hpp"
#include <utility>
#include <QButtonGroup>
#include <QComboBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QPointer>
#include <QPushButton>
#include <QRadioButton>
#include <QStackedWidget>
#include <QVBoxLayout>
#include "FormKit.hpp"
#include "panels/chart/InversionProcessOps.hpp" // buildWhitenBody
#include "repo/IDatasetCommandRepository.hpp"
namespace geopro::app {
namespace {
constexpr double kExtRange = 1e6; // 边界扩展范围
} // namespace
WhiteningDialog::WhiteningDialog(geopro::data::IDatasetCommandRepository* repo, QString dsId,
QString projectId, QString tmObjectId, QWidget* parent)
: QDialog(parent),
repo_(repo),
dsId_(std::move(dsId)),
projectId_(std::move(projectId)),
tmObjectId_(std::move(tmObjectId)) {
setWindowTitle(QStringLiteral("白化"));
setModal(true);
resize(460, 280);
auto* root = formkit::dialogRoot(this);
// 白化方式下拉(数值对照原版 whiteningMethod 1/2/3
auto* methodLay = formkit::makeEditForm();
methodCombo_ = new QComboBox(this);
methodCombo_->addItem(QStringLiteral("数据边界自动白化"), 1);
methodCombo_->addItem(QStringLiteral("白化模板"), 2);
methodCombo_->addItem(QStringLiteral("模型白化"), 3);
formkit::capField(methodCombo_);
methodLay->addRow(formkit::editLabel(QStringLiteral("白化方式")), methodCombo_);
root->addLayout(methodLay);
stack_ = new QStackedWidget(this);
root->addWidget(stack_, 1);
// ── 方式 1数据边界自动白化 ────────────────────────────────────────
auto* page1 = new QWidget(this);
auto* p1 = formkit::makeEditForm();
page1->setLayout(p1);
extension_ = new QDoubleSpinBox(page1);
extension_->setRange(-kExtRange, kExtRange);
extension_->setDecimals(3);
formkit::capField(extension_);
p1->addRow(formkit::editLabel(QStringLiteral("白化边界扩展")), extension_);
auto* typeRow = new QHBoxLayout();
auto* rbOuter = new QRadioButton(QStringLiteral("外部白化"), page1);
auto* rbInner = new QRadioButton(QStringLiteral("内部白化"), page1);
rbOuter->setChecked(true);
whiteningType_ = new QButtonGroup(this);
whiteningType_->addButton(rbOuter, 0);
whiteningType_->addButton(rbInner, 1);
typeRow->addWidget(rbOuter);
typeRow->addWidget(rbInner);
typeRow->addStretch();
p1->addRow(formkit::editLabel(QStringLiteral("白化")), typeRow);
stack_->addWidget(page1);
// ── 方式 2白化模板选文件──────────────────────────────────────
auto* page2 = new QWidget(this);
auto* p2 = formkit::makeEditForm();
page2->setLayout(p2);
fileCombo_ = new QComboBox(page2);
formkit::capField(fileCombo_);
p2->addRow(formkit::editLabel(QStringLiteral("选择白化文件")), fileCombo_);
stack_->addWidget(page2);
// ── 方式 3模型白化梯形/矩形)───────────────────────────────────
auto* page3 = new QWidget(this);
auto* p3 = formkit::makeEditForm();
page3->setLayout(p3);
auto* subRow = new QHBoxLayout();
auto* rbTrap = new QRadioButton(QStringLiteral("梯形白化"), page3);
auto* rbRect = new QRadioButton(QStringLiteral("矩形白化"), page3);
rbTrap->setChecked(true);
modelSubType_ = new QButtonGroup(this);
modelSubType_->addButton(rbTrap, 2); // 2 梯形
modelSubType_->addButton(rbRect, 1); // 1 矩形
subRow->addWidget(rbTrap);
subRow->addWidget(rbRect);
subRow->addStretch();
p3->addRow(formkit::editLabel(QStringLiteral("白化")), subRow);
stack_->addWidget(page3);
// 底部按钮。
auto* btnLay = new QHBoxLayout();
btnLay->addStretch();
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
okBtn_ = new QPushButton(QStringLiteral("确定"), this);
okBtn_->setDefault(true);
btnLay->addWidget(cancelBtn);
btnLay->addWidget(okBtn_);
root->addLayout(btnLay);
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
connect(okBtn_, &QPushButton::clicked, this, &WhiteningDialog::onConfirm);
connect(methodCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this](int) { onMethodChanged(methodCombo_->currentData().toInt()); });
loadWhitenedFiles(); // 预拉文件列表(方式 2 用)
}
void WhiteningDialog::onMethodChanged(int method) {
stack_->setCurrentIndex(method - 1); // 1/2/3 → 页 0/1/2
}
void WhiteningDialog::loadWhitenedFiles() {
if (!repo_) return;
QPointer<WhiteningDialog> self(this);
repo_->listWhitenedData(projectId_, tmObjectId_, [self](bool ok, QJsonArray list, QString) {
if (!self || !ok) return;
self->fileCombo_->clear();
for (const QJsonValue& v : list) {
const QJsonObject o = v.toObject();
QString label = o.value(QStringLiteral("dsName")).toString();
if (label.isEmpty()) label = o.value(QStringLiteral("name")).toString();
const QString id = o.value(QStringLiteral("id")).toString();
if (label.isEmpty()) label = QStringLiteral("未命名文件_%1").arg(id);
self->fileCombo_->addItem(label, id);
}
});
}
void WhiteningDialog::onConfirm() {
if (!repo_) { reject(); return; }
WhitenParams p;
p.dsObjectId = dsId_;
p.whiteningMethod = methodCombo_->currentData().toInt();
if (p.whiteningMethod == 1) {
p.boundaryExtension = extension_->value();
p.whiteningType = whiteningType_->checkedId();
} else if (p.whiteningMethod == 2) {
if (fileCombo_->currentIndex() < 0) {
QMessageBox::warning(this, windowTitle(), QStringLiteral("请选择白化文件"));
return;
}
p.whitenedDataId = fileCombo_->currentData().toString();
} else {
p.modelWhiteningSubType = modelSubType_->checkedId();
}
okBtn_->setEnabled(false);
QPointer<WhiteningDialog> self(this);
repo_->whitenData(buildWhitenBody(p), [self](bool ok, QString msg) {
if (!self) return;
self->okBtn_->setEnabled(true);
if (ok) {
self->accept();
} else {
QMessageBox::warning(self, self->windowTitle(),
msg.isEmpty() ? QStringLiteral("白化失败") : msg);
}
});
}
} // namespace geopro::app

View File

@ -0,0 +1,51 @@
#pragma once
#include <QDialog>
#include <QString>
class QComboBox;
class QDoubleSpinBox;
class QButtonGroup;
class QStackedWidget;
class QPushButton;
namespace geopro::data {
class IDatasetCommandRepository;
}
namespace geopro::app {
// 白化对话框I31:1 复刻原版 WhiteningDialog。三种白化方式
// 1 数据边界自动:边界扩展 + 内/外白化单选。
// 2 白化模板listWhitenedData(projectId, tmObjectId) 选白化文件。
// 3 模型白化:梯形/矩形单选。
// 确认 → whitenData成功 accept(),调用方随后重载网格重绘。
class WhiteningDialog : public QDialog {
Q_OBJECT
public:
WhiteningDialog(geopro::data::IDatasetCommandRepository* repo, QString dsId, QString projectId,
QString tmObjectId, QWidget* parent = nullptr);
private:
void onMethodChanged(int method); // 切换方式 → 切到对应配置页
void loadWhitenedFiles(); // 方式 2拉白化文件列表
void onConfirm(); // 确认 → whitenData
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
QString dsId_;
QString projectId_;
QString tmObjectId_;
QComboBox* methodCombo_ = nullptr;
QStackedWidget* stack_ = nullptr;
// 方式 1
QDoubleSpinBox* extension_ = nullptr;
QButtonGroup* whiteningType_ = nullptr; // 0 外部 / 1 内部
// 方式 2
QComboBox* fileCombo_ = nullptr;
// 方式 3
QButtonGroup* modelSubType_ = nullptr; // 2 梯形 / 1 矩形
QPushButton* okBtn_ = nullptr;
};
} // namespace geopro::app

View File

@ -16,6 +16,7 @@ struct Anomaly {
std::string typeName; // exceptionTypeName
std::string exceptionTypeId; // 异常类型 id保存请求 exceptionTypeId
std::string remark; // 备注
std::string createTime; // 创建时间(异常列表展示用,只读)
AnomalyMarkType markType = AnomalyMarkType::Polyline;
std::vector<Vec2> localPts; // 2D 局部坐标剖面详情x=距离, y=深度)
// VTK 三维:异常多边形/折线/点的世界 3D 坐标(落在所在切片平面上)+ 平面(法向/一点)

View File

@ -2,6 +2,7 @@
#include <vector>
#include <cstddef>
#include <cmath>
#include <string>
namespace geopro::core {
// 规则三维标量场IInterpolator 输出render 层转 vtkImageData
@ -55,6 +56,14 @@ struct ScatterField {
std::vector<double> a, b, m, n;
std::vector<double> electrodeX;
std::vector<double> electrodeNo;
// measurement 散点 [i]信息 / 显隐 / 持久化用(反演留空)。与 x/y/v 同序、一一对应:
// id = 点 idsaveDisplayStatus ids[],原版 rows[i][8]core 保持 Qt-free 用 std::string
// displayStatus = 可见性0=显示 1=隐藏,原版 rows[i][7]
// row = DataRow[i]信息面板,原版 rows[i][15]
// pseu = Pseu_Resis 视电阻率([i]信息面板,原版 rows[i][16]
std::vector<std::string> id;
std::vector<int> displayStatus;
std::vector<double> row, pseu;
};
} // namespace geopro::core

View File

@ -60,15 +60,26 @@ struct TableColumn {
TableColumnKind kind = TableColumnKind::Text;
};
// 表格功能按钮dd_grid服务端 functionList = DDGridFunctionVO[],驱动列表上方功能按钮行)。
// 仅 dd_grid 列表携带(其余 Table 复用场景该列表为空 → 不渲染工具条。enable=false 的按钮不显示
// (原版 v-show="enable")。点击按 code 路由(原版仅处理 code=="inversion")。
struct TableFunctionButton {
QString code; // functionCode如 inversion
QString nameChn; // functionNameChn按钮中文文案
bool enable = true;
};
// 通用表格载荷:列定义 + 预格式化的行(每格 QString+ 总数(分页用)。
// 分页dd_grid 列表,服务端分页 vxe-pagerpageSize>0 时视图渲染分页器pageNo 为当前页(1 基)
// pageSize=0默认= 不分页measurement/trajectory 全量列表,一次性返回所有行)。
// functionButtons仅 dd_grid 列表非空(来自服务端 functionList驱动列表上方功能按钮行。
struct TablePayload {
std::vector<TableColumn> columns;
std::vector<std::vector<QString>> rows;
int total = 0;
int pageNo = 1; // 当前页(1 基);分页用
int pageSize = 0; // 每页条数;>0 才渲染分页器(vxe-pager)0=不分页
std::vector<TableFunctionButton> functionButtons; // dd_grid 功能按钮(其余场景空)
};
// 柱状图系列:名称(图例/legend+ 各类目的 y 值 + 填充色hex如 #5470c6数据色两主题一致

View File

@ -13,6 +13,7 @@ add_library(geopro_data STATIC
api/ApiProjectRepository.cpp
api/ApiDatasetRepository.cpp
api/ApiColorTemplateRepository.cpp
api/ApiDatasetCommandRepository.cpp
api/Api3dRepository.cpp
api/DatasetLoadHandles.cpp
api/NavRequest.cpp)

View File

@ -0,0 +1,425 @@
#include "api/ApiDatasetCommandRepository.hpp"
#include <utility>
#include <QUrl>
#include "ApiBatch.hpp"
#include "ApiClient.hpp"
#include "IApiCall.hpp"
#include "dto/MeasurementDto.hpp" // parseMeasurementScatterV 值重载复用初始加载解析)
#include "dto/DatasetChartDto.hpp" // parseInversionGrid/parseColorBar/parseDatasetAnomalies网格重载
namespace geopro::data {
namespace {
// 失败判定与色阶/详情仓储一致:业务码 != 200 或传输错误rawError 非空)。
bool isOk(const net::ApiResponse& r) { return r.code == 200 && r.rawError.isEmpty(); }
// URL 路径段编码(动态 id与现有 enc 用法一致。
QString enc(const QString& s) { return QString::fromUtf8(QUrl::toPercentEncoding(s)); }
// 三类回调骨架:统一「句柄判空 → connect → finished 取值」,消除样板重复。
// 仅状态:(bool ok, QString msg)。
void wireStatus(net::IApiCall* call, std::function<void(bool, QString)> cb) {
if (call == nullptr) {
if (cb) cb(false, QStringLiteral("请求创建失败"));
return;
}
QObject::connect(call, &net::IApiCall::finished, call,
[cb = std::move(cb)](const net::ApiResponse& resp) {
if (cb) cb(isOk(resp), resp.msg);
});
}
// 返回数组:(bool ok, QJsonArray list, QString msg)。顶层数组被 buildResponse 包成 data.value。
void wireArray(net::IApiCall* call, std::function<void(bool, QJsonArray, QString)> cb) {
if (call == nullptr) {
if (cb) cb(false, {}, QStringLiteral("请求创建失败"));
return;
}
QObject::connect(call, &net::IApiCall::finished, call,
[cb = std::move(cb)](const net::ApiResponse& resp) {
if (!cb) return;
if (!isOk(resp)) {
cb(false, {}, resp.msg);
return;
}
cb(true, resp.data.value(QStringLiteral("value")).toArray(), resp.msg);
});
}
// 返回对象:(bool ok, QJsonObject data, QString msg)。
void wireObject(net::IApiCall* call, std::function<void(bool, QJsonObject, QString)> cb) {
if (call == nullptr) {
if (cb) cb(false, {}, QStringLiteral("请求创建失败"));
return;
}
QObject::connect(call, &net::IApiCall::finished, call,
[cb = std::move(cb)](const net::ApiResponse& resp) {
if (!cb) return;
if (!isOk(resp)) {
cb(false, {}, resp.msg);
return;
}
cb(true, resp.data, resp.msg);
});
}
} // namespace
ApiDatasetCommandRepository::ApiDatasetCommandRepository(net::ApiClient& api) : api_(api) {}
void ApiDatasetCommandRepository::listInversionScripts(
const QString& dsObjectId, std::function<void(bool, QJsonArray, QString)> cb) {
auto* call = api_.getAsync(
QStringLiteral("/business/outerInversion/query/script?dsObjectId=%1")
.arg(QString::fromUtf8(QUrl::toPercentEncoding(dsObjectId))));
if (call == nullptr) {
if (cb) cb(false, {}, QStringLiteral("请求创建失败"));
return;
}
QObject::connect(call, &net::IApiCall::finished, call,
[cb = std::move(cb)](const net::ApiResponse& resp) {
if (!cb) return;
if (!isOk(resp)) {
cb(false, {}, resp.msg);
return;
}
// 模型列表data 顶层为数组buildResponse 包成 {"value": [...]}。
cb(true, resp.data.value(QStringLiteral("value")).toArray(), resp.msg);
});
}
void ApiDatasetCommandRepository::getDynamicForm(
const QString& projectId, const QString& typeId,
std::function<void(bool, QJsonObject, QString)> cb) {
QJsonObject body{{QStringLiteral("projectId"), projectId},
{QStringLiteral("type"), 6},
{QStringLiteral("typeId"), typeId}};
auto* call = api_.postJsonAsync(QStringLiteral("/business/project/getDynamicForm"), body);
if (call == nullptr) {
if (cb) cb(false, {}, QStringLiteral("请求创建失败"));
return;
}
QObject::connect(call, &net::IApiCall::finished, call,
[cb = std::move(cb)](const net::ApiResponse& resp) {
if (!cb) return;
if (!isOk(resp)) {
cb(false, {}, resp.msg);
return;
}
// 动态表单data 为对象(含 formList 数组)。
cb(true, resp.data, resp.msg);
});
}
void ApiDatasetCommandRepository::submitInversionTask(const QString& dsId, const QString& scriptId,
const QJsonObject& properties,
std::function<void(bool, QString)> cb) {
QJsonObject body{{QStringLiteral("dsId"), dsId},
{QStringLiteral("scriptId"), scriptId},
{QStringLiteral("properties"), properties}};
auto* call =
api_.postJsonAsync(QStringLiteral("/business/outerInversion/submitInversionTask"), body);
if (call == nullptr) {
if (cb) cb(false, QStringLiteral("请求创建失败"));
return;
}
QObject::connect(call, &net::IApiCall::finished, call,
[cb = std::move(cb)](const net::ApiResponse& resp) {
if (cb) cb(isOk(resp), resp.msg);
});
}
void ApiDatasetCommandRepository::createVisualResistivityData(
const QString& dsObjectId, const QString& scriptId, const QJsonObject& scriptParamListJsonStr,
std::function<void(bool, QString)> cb) {
QJsonObject body{{QStringLiteral("dsObjectId"), dsObjectId},
{QStringLiteral("scriptId"), scriptId},
{QStringLiteral("scriptParamListJsonStr"), scriptParamListJsonStr}};
auto* call = api_.postJsonAsync(
QStringLiteral("/business/dd/ert/measurement/createVisualResistivityData"), body);
if (call == nullptr) {
if (cb) cb(false, QStringLiteral("请求创建失败"));
return;
}
QObject::connect(call, &net::IApiCall::finished, call,
[cb = std::move(cb)](const net::ApiResponse& resp) {
if (cb) cb(isOk(resp), resp.msg);
});
}
// ============================ measurement 散点相关 ============================
void ApiDatasetCommandRepository::saveDisplayStatus(const QString& dsObjectId,
const QJsonArray& ids, int status,
std::function<void(bool, QString)> cb) {
QJsonObject body{{QStringLiteral("dsObjectId"), dsObjectId},
{QStringLiteral("ids"), ids},
{QStringLiteral("status"), status}};
wireStatus(api_.postJsonAsync(QStringLiteral("/business/dd/ert/measurement/saveDisplayStatus"),
body),
std::move(cb));
}
void ApiDatasetCommandRepository::getScatterFilterConfig(
const QString& dsObjectId, const QString& vFieldCode,
std::function<void(bool, QJsonObject, QString)> cb) {
wireObject(
api_.getAsync(QStringLiteral(
"/business/scatterPlotDataFilter/getDataFilterConfig?dsObjectId=%1&vFieldCode=%2")
.arg(enc(dsObjectId), enc(vFieldCode))),
std::move(cb));
}
void ApiDatasetCommandRepository::applyScatterFilter(const QJsonObject& body,
std::function<void(bool, QString)> cb) {
wireStatus(api_.postJsonAsync(QStringLiteral("/business/scatterPlotDataFilter"), body),
std::move(cb));
}
void ApiDatasetCommandRepository::saveRawData(const QJsonObject& body,
std::function<void(bool, QString)> cb) {
wireStatus(api_.postJsonAsync(QStringLiteral("/business/dd/ert/measurement/saveRawData"), body),
std::move(cb));
}
void ApiDatasetCommandRepository::exportMeasurementDat(
const QString& dsId, int electrodePosition, int ipDataMark, int typeMeasurement,
std::function<void(bool, QString, QString, QString)> cb) {
auto* call = api_.getAsync(
QStringLiteral("/business/dd/ert/measurement/rs2d/"
"export?dsId=%1&electrodePosition=%2&ipDataMark=%3&typeMeasurement=%4")
.arg(enc(dsId))
.arg(electrodePosition)
.arg(ipDataMark)
.arg(typeMeasurement));
if (call == nullptr) {
if (cb) cb(false, {}, {}, QStringLiteral("请求创建失败"));
return;
}
QObject::connect(call, &net::IApiCall::finished, call,
[cb = std::move(cb)](const net::ApiResponse& resp) {
if (!cb) return;
if (!isOk(resp)) {
cb(false, {}, {}, resp.msg);
return;
}
// 导出data 含 base64 fileData 与 fileName落盘交由 UI 层。
cb(true, resp.data.value(QStringLiteral("fileName")).toString(),
resp.data.value(QStringLiteral("fileData")).toString(), resp.msg);
});
}
void ApiDatasetCommandRepository::loadMeasurementScatter(
const QString& dsObjectId, const QString& vFieldCode,
std::function<void(bool, geopro::core::ScatterPayload, QString)> cb) {
// 并发拉 scatter/graph(带 vFieldCode) + 色阶 getDetail(type3, businessCode=vFieldCode)。
// 与 ApiDatasetRepository::measurementScatterBatch 同端点/同解析(仅 vFieldCode 可变)。
QList<net::IApiCall*> calls{
api_.getAsync(
QStringLiteral("/business/dd/ert/measurement/scatter/graph?dsObjectId=%1&vFieldCode=%2")
.arg(enc(dsObjectId), enc(vFieldCode))),
api_.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"),
QJsonObject{{QStringLiteral("dsObjectId"), dsObjectId},
{QStringLiteral("businessCode"), vFieldCode},
{QStringLiteral("type"), 3}}),
};
auto* batch = new net::ApiBatch(calls, [](const net::ApiResponse& r) {
return r.code != 200 || !r.rawError.isEmpty();
});
QObject::connect(batch, &net::ApiBatch::succeeded, batch,
[cb](const QList<net::ApiResponse>& r) {
if (cb) cb(true, dto::parseMeasurementScatter(r[0].data, r[1].data), {});
});
QObject::connect(batch, &net::ApiBatch::failed, batch,
[cb](int, const net::ApiResponse& resp) {
if (cb) cb(false, {}, resp.msg);
});
// ApiBatch 完成/失败后各 call 自行 deleteLaterbatch 本身随末次信号后 deleteLater。
QObject::connect(batch, &net::ApiBatch::succeeded, batch, &QObject::deleteLater);
QObject::connect(batch, &net::ApiBatch::failed, batch, &QObject::deleteLater);
}
// ============================ 色阶lvl相关 ============================
void ApiDatasetCommandRepository::saveColorGradation(const QJsonObject& body,
std::function<void(bool, QString)> cb) {
wireStatus(api_.postJsonAsync(QStringLiteral("/business/lvl/colorGradation"), body),
std::move(cb));
}
// ============================ inversion 相关 ============================
void ApiDatasetCommandRepository::saveInversionAsData(const QString& dsObjectId,
const QString& name,
std::function<void(bool, QString)> cb) {
QJsonObject body{{QStringLiteral("dsObjectId"), dsObjectId}, {QStringLiteral("name"), name}};
wireStatus(api_.postJsonAsync(QStringLiteral("/business/dd/ert/inversion/saveAsData"), body),
std::move(cb));
}
void ApiDatasetCommandRepository::loadInversionGrid(
const QString& dsObjectId,
std::function<void(bool, geopro::core::ContourPayload, QString)> cb) {
// 与 ApiDatasetRepository::inversionGridBatch 同端点/同解析rows(慢) + 色阶 type2 + 异常。
QList<net::IApiCall*> calls{
api_.getAsync(QStringLiteral("/business/dd/ert/inversion/rows/%1").arg(enc(dsObjectId))),
api_.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"),
QJsonObject{{QStringLiteral("dsObjectId"), dsObjectId},
{QStringLiteral("businessCode"), QString()},
{QStringLiteral("type"), 2}}),
api_.getAsync(
QStringLiteral("/business/exception/queryException/%1").arg(enc(dsObjectId))),
};
auto* batch = new net::ApiBatch(calls, [](const net::ApiResponse& r) {
return r.code != 200 || !r.rawError.isEmpty();
});
QObject::connect(batch, &net::ApiBatch::succeeded, batch,
[cb](const QList<net::ApiResponse>& r) {
if (!cb) return;
geopro::core::ContourPayload p{
dto::parseInversionGrid(r[0].data),
dto::parseColorBar(r[1].data),
dto::parseDatasetAnomalies(
r[2].data.value(QStringLiteral("value")).toArray())};
cb(true, p, {});
});
QObject::connect(batch, &net::ApiBatch::failed, batch,
[cb](int, const net::ApiResponse& resp) {
if (cb) cb(false, {}, resp.msg);
});
QObject::connect(batch, &net::ApiBatch::succeeded, batch, &QObject::deleteLater);
QObject::connect(batch, &net::ApiBatch::failed, batch, &QObject::deleteLater);
}
void ApiDatasetCommandRepository::listGridAlgorithm(
const QString& dsObjectId, std::function<void(bool, QJsonArray, QString)> cb) {
wireArray(api_.getAsync(QStringLiteral("/business/dd/ert/inversion/queryAlgorithmModel/%1")
.arg(enc(dsObjectId))),
std::move(cb));
}
void ApiDatasetCommandRepository::getGridRawDataParams(
const QString& dsObjectId, std::function<void(bool, QJsonObject, QString)> cb) {
wireObject(api_.getAsync(QStringLiteral("/business/dd/ert/inversion/getRawData/%1")
.arg(enc(dsObjectId))),
std::move(cb));
}
void ApiDatasetCommandRepository::toGrid(const QJsonObject& body,
std::function<void(bool, QString)> cb) {
wireStatus(api_.postJsonAsync(QStringLiteral("/business/dd/ert/inversion/grid"), body),
std::move(cb));
}
// ============================ 白化相关 ============================
void ApiDatasetCommandRepository::listWhitenedData(
const QString& projectId, const QString& tmObjectId,
std::function<void(bool, QJsonArray, QString)> cb) {
QJsonObject body{{QStringLiteral("projectId"), projectId},
{QStringLiteral("tmObjectId"), tmObjectId}};
wireArray(api_.postJsonAsync(QStringLiteral("/business/dsObject/queryWhitenedDataList"), body),
std::move(cb));
}
void ApiDatasetCommandRepository::whitenData(const QJsonObject& body,
std::function<void(bool, QString)> cb) {
wireStatus(api_.postJsonAsync(QStringLiteral("/business/dd/ert/inversion/whitenedData"), body),
std::move(cb));
}
// ============================ 滤波相关 ============================
void ApiDatasetCommandRepository::listFilters(std::function<void(bool, QJsonArray, QString)> cb) {
wireArray(api_.getAsync(QStringLiteral("/business/filter/queryFilter")), std::move(cb));
}
void ApiDatasetCommandRepository::newFilter(const QJsonObject& body,
std::function<void(bool, QString)> cb) {
wireStatus(api_.postJsonAsync(QStringLiteral("/business/filter"), body), std::move(cb));
}
void ApiDatasetCommandRepository::deleteFilter(const QString& id,
std::function<void(bool, QString)> cb) {
wireStatus(api_.deleteAsync(QStringLiteral("/business/filter/delete/%1").arg(enc(id))),
std::move(cb));
}
void ApiDatasetCommandRepository::applyFilter(const QJsonObject& body,
std::function<void(bool, QString)> cb) {
wireStatus(api_.postJsonAsync(QStringLiteral("/business/dd/ert/inversion/filterData"), body),
std::move(cb));
}
// ============================ 异常 CRUD ============================
void ApiDatasetCommandRepository::listExceptionTypes(
const QString& projectId, const QString& remarkSourceType,
std::function<void(bool, QJsonArray, QString)> cb) {
wireArray(api_.getAsync(
QStringLiteral(
"/business/exceptionType/queryExceptionTypeByProjectIdAndType/%1/%2")
.arg(enc(projectId), enc(remarkSourceType))),
std::move(cb));
}
void ApiDatasetCommandRepository::getExceptionName(
const QString& exceptionTypeId, const QString& remarkSourceId,
std::function<void(bool, QJsonObject, QString)> cb) {
QJsonObject body{{QStringLiteral("exceptionTypeId"), exceptionTypeId},
{QStringLiteral("remarkSourceId"), remarkSourceId}};
wireObject(api_.postJsonAsync(QStringLiteral("/business/exception/getExceptionName"), body),
std::move(cb));
}
void ApiDatasetCommandRepository::newException(const QJsonObject& body,
std::function<void(bool, QString)> cb) {
wireStatus(api_.postJsonAsync(QStringLiteral("/business/exception"), body), std::move(cb));
}
void ApiDatasetCommandRepository::deleteException(const QString& id,
std::function<void(bool, QString)> cb) {
wireStatus(api_.deleteAsync(QStringLiteral("/business/exception/%1").arg(enc(id))),
std::move(cb));
}
void ApiDatasetCommandRepository::updateException(const QJsonObject& body,
std::function<void(bool, QString)> cb) {
wireStatus(api_.putJsonAsync(QStringLiteral("/business/exception"), body), std::move(cb));
}
// ============================ 自动标注 ============================
void ApiDatasetCommandRepository::executeExceptionMark(
const QJsonObject& body, std::function<void(bool, QJsonObject, QString)> cb) {
wireObject(
api_.postJsonAsync(QStringLiteral("/business/exception/exception-mark/execute"), body),
std::move(cb));
}
void ApiDatasetCommandRepository::batchCreateException(const QJsonObject& body,
std::function<void(bool, QString)> cb) {
wireStatus(api_.postJsonAsync(QStringLiteral("/business/exception/batch/create"), body),
std::move(cb));
}
// ============================ 描述 / dsObject ============================
void ApiDatasetCommandRepository::getDsObjectDetail(
const QString& dsObjectId, std::function<void(bool, QJsonObject, QString)> cb) {
wireObject(
api_.getAsync(QStringLiteral("/business/dsObject/getDetail/%1").arg(enc(dsObjectId))),
std::move(cb));
}
void ApiDatasetCommandRepository::updateDsObject(const QJsonObject& body,
std::function<void(bool, QString)> cb) {
// 原版 URL 末尾带斜杠updateProfileInversionDescription
wireStatus(api_.putJsonAsync(QStringLiteral("/business/dsObject/updateDsObject/"), body),
std::move(cb));
}
} // namespace geopro::data

View File

@ -0,0 +1,112 @@
#pragma once
#include "repo/IDatasetCommandRepository.hpp"
namespace geopro::net { class ApiClient; }
namespace geopro::data {
// IDatasetCommandRepository 的真实 API 实现(反演相关写操作)。
// 持 ApiClient&(共享会话),每个方法组装 body → 发请求 → 连 IApiCall::finished 回调;
// 句柄自管理(完成后 deleteLater不手动 delete。与 ApiColorTemplateRepository 同模式。
class ApiDatasetCommandRepository : public IDatasetCommandRepository {
public:
explicit ApiDatasetCommandRepository(net::ApiClient& api);
void listInversionScripts(
const QString& dsObjectId,
std::function<void(bool ok, QJsonArray list, QString msg)> cb) override;
void getDynamicForm(
const QString& projectId, const QString& typeId,
std::function<void(bool ok, QJsonObject data, QString msg)> cb) override;
void submitInversionTask(const QString& dsId, const QString& scriptId,
const QJsonObject& properties,
std::function<void(bool ok, QString msg)> cb) override;
void createVisualResistivityData(
const QString& dsObjectId, const QString& scriptId,
const QJsonObject& scriptParamListJsonStr,
std::function<void(bool ok, QString msg)> cb) override;
// measurement 散点
void saveDisplayStatus(const QString& dsObjectId, const QJsonArray& ids, int status,
std::function<void(bool ok, QString msg)> cb) override;
void getScatterFilterConfig(
const QString& dsObjectId, const QString& vFieldCode,
std::function<void(bool ok, QJsonObject data, QString msg)> cb) override;
void applyScatterFilter(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) override;
void saveRawData(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) override;
void exportMeasurementDat(
const QString& dsId, int electrodePosition, int ipDataMark, int typeMeasurement,
std::function<void(bool ok, QString fileName, QString fileData, QString msg)> cb) override;
void loadMeasurementScatter(
const QString& dsObjectId, const QString& vFieldCode,
std::function<void(bool ok, geopro::core::ScatterPayload payload, QString msg)> cb) override;
// 色阶
void saveColorGradation(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) override;
// inversion
void saveInversionAsData(const QString& dsObjectId, const QString& name,
std::function<void(bool ok, QString msg)> cb) override;
void loadInversionGrid(
const QString& dsObjectId,
std::function<void(bool ok, geopro::core::ContourPayload payload, QString msg)> cb) override;
void listGridAlgorithm(
const QString& dsObjectId,
std::function<void(bool ok, QJsonArray list, QString msg)> cb) override;
void getGridRawDataParams(
const QString& dsObjectId,
std::function<void(bool ok, QJsonObject data, QString msg)> cb) override;
void toGrid(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) override;
// 白化
void listWhitenedData(const QString& projectId, const QString& tmObjectId,
std::function<void(bool ok, QJsonArray list, QString msg)> cb) override;
void whitenData(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) override;
// 滤波
void listFilters(std::function<void(bool ok, QJsonArray list, QString msg)> cb) override;
void newFilter(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) override;
void deleteFilter(const QString& id,
std::function<void(bool ok, QString msg)> cb) override;
void applyFilter(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) override;
// 异常 CRUD
void listExceptionTypes(
const QString& projectId, const QString& remarkSourceType,
std::function<void(bool ok, QJsonArray list, QString msg)> cb) override;
void getExceptionName(
const QString& exceptionTypeId, const QString& remarkSourceId,
std::function<void(bool ok, QJsonObject data, QString msg)> cb) override;
void newException(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) override;
void deleteException(const QString& id,
std::function<void(bool ok, QString msg)> cb) override;
void updateException(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) override;
// 自动标注
void executeExceptionMark(
const QJsonObject& body,
std::function<void(bool ok, QJsonObject data, QString msg)> cb) override;
void batchCreateException(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) override;
// 描述 / dsObject
void getDsObjectDetail(
const QString& dsObjectId,
std::function<void(bool ok, QJsonObject data, QString msg)> cb) override;
void updateDsObject(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) override;
private:
net::ApiClient& api_;
};
} // namespace geopro::data

View File

@ -65,8 +65,12 @@ std::vector<Anomaly> parseDatasetAnomalies(const QJsonArray& arr) {
for (auto e : arr) {
const QJsonObject o = e.toObject();
Anomaly a;
a.id = o.value("id").toString().toStdString(); // 删除/更新/定位需要持久化 id
a.name = o.value("exceptionName").toString().toStdString();
a.typeName = o.value("exceptionTypeName").toString().toStdString();
a.exceptionTypeId = o.value("exceptionTypeId").toString().toStdString();
a.remark = o.value("remark").toString().toStdString();
a.createTime = o.value("createTime").toString().toStdString();
const int mt = o.value("exceptionMarkType").toInt(2); // 1=点 2=线 3=面
a.markType = (mt >= 1 && mt <= 3) ? static_cast<AnomalyMarkType>(mt)
: AnomalyMarkType::Polyline; // 越界值兜底为线

View File

@ -1,5 +1,6 @@
#include "dto/GridDto.hpp"
#include <QJsonArray>
#include <QJsonValue>
#include "dto/TrajectoryDto.hpp" // parseGridHeaderTable通用 gridHeaderDisplay+rowList 解析器)复用
@ -28,6 +29,18 @@ TablePayload parseGridTable(const QJsonObject& data, int pageNo, int pageSize) {
t.total = data.value(QStringLiteral("total")).toInt(t.total);
t.pageNo = pn;
t.pageSize = ps;
// 功能按钮data.functionListDDGridFunctionVO[])→ 列表上方功能按钮行(原版 functionButtons
// 仅 dd_grid 携带;缺省/空数组 → 不渲染工具条。enable 透传(视图按 v-show 语义过滤)。
const QJsonArray fns = data.value(QStringLiteral("functionList")).toArray();
for (const auto& v : fns) {
const QJsonObject o = v.toObject();
TableFunctionButton b;
b.code = o.value(QStringLiteral("functionCode")).toString();
b.nameChn = o.value(QStringLiteral("functionNameChn")).toString();
b.enable = o.value(QStringLiteral("enable")).toBool(true);
t.functionButtons.push_back(std::move(b));
}
return t;
}

View File

@ -13,6 +13,7 @@ namespace geopro::data::dto {
// 复用通用 parseGridHeaderTablegridHeaderDisplay→x/y 列 + rowList→行再前插「序号」列
// vxe seq 列:按页偏移自增 = (pageNo-1)*pageSize + 行内序号total 取 data.total非 __rowTotal
// 回填 pageNo/pageSize 供视图渲染分页器。pageNo/pageSize 为本次请求参数(仓储已解析默认值后传入)。
// 另解析 data.functionListDDGridFunctionVO[])→ functionButtons驱动列表上方功能按钮行含「反演」
geopro::core::TablePayload parseGridTable(const QJsonObject& data, int pageNo, int pageSize);
} // namespace geopro::data::dto

View File

@ -13,12 +13,15 @@ namespace {
constexpr int kColHorizontalDistance = 0; // x 备选:平距
constexpr int kColSlopeDistance = 1; // x斜距默认
constexpr int kColPseudoDepth = 3; // y伪深度向下默认
constexpr int kColVisible = 7; // 可见性0=显示 1=隐藏displayStatus
constexpr int kColId = 8; // 点 idsaveDisplayStatus ids[]
constexpr int kColElevationPseudoDepth = 9; // y 备选:伪深度+高程
constexpr int kColA = 10;
constexpr int kColB = 11;
constexpr int kColM = 12;
constexpr int kColN = 13;
constexpr int kColValue = 16; // 色值:选中 v默认视电阻率 R0
constexpr int kColRow = 15; // DataRow[i]信息面板)
constexpr int kColValue = 16; // 色值/Pseu_Resis选中 v默认视电阻率 R0
double numAt(const QJsonArray& arr, int i) {
if (i < 0 || i >= arr.size()) return 0.0;
@ -28,6 +31,15 @@ double numAt(const QJsonArray& arr, int i) {
return 0.0;
}
// 取字符串列id 为字符串形态,如 "1453611521843200";数字形态退回字符串表示)。
QString strAt(const QJsonArray& arr, int i) {
if (i < 0 || i >= arr.size()) return QString();
const QJsonValue v = arr.at(i);
if (v.isString()) return v.toString();
if (v.isDouble()) return QString::number(v.toDouble(), 'f', 0);
return QString();
}
// 把 JSON 值预格式化为单元格 QString数字按原值null/缺省→空串)。
QString cellText(const QJsonValue& v) {
if (v.isDouble()) {
@ -75,6 +87,11 @@ ScatterPayload parseMeasurementScatter(const QJsonObject& scatterData, const QJs
s.b.push_back(numAt(row, kColB));
s.m.push_back(numAt(row, kColM));
s.n.push_back(numAt(row, kColN));
// [i]信息 / 显隐持久化用元数据(与上面 push 同序、一一对应)。
s.id.push_back(strAt(row, kColId).toStdString());
s.displayStatus.push_back(static_cast<int>(numAt(row, kColVisible)));
s.row.push_back(numAt(row, kColRow));
s.pseu.push_back(numAt(row, kColValue));
// x/y 下拉本地重绘备选列(与上面 push 同序、一一对应 → 视图换 x/y 无需再请求)。
p.altXHorizontal.push_back(numAt(row, kColHorizontalDistance));
p.altXSlope.push_back(numAt(row, kColSlopeDistance));

View File

@ -0,0 +1,217 @@
#pragma once
#include <functional>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include "model/detail/DetailPayloads.hpp" // core::ScatterPayloadV 值重载返回完整载荷)
namespace geopro::data {
// 数据集详情视图「写操作 / 命令」仓储抽象(与只读 IAsyncDatasetRepository 平行)。
// 本切片仅纳入「反演」相关的 4 个端点measurement 反演运算 / 生成视电阻率 + 共享的
// 模型列表 / 动态表单查询)。回调式异步:仓储只做「组装请求 + 取数组/状态」的传输职责;
// 领域解析(动态表单分组/字段控件)留在对话框(见 InversionFormDialog 的纯函数)。
// 回调在 Qt 主线程经 IApiCall::finished 触发;调用方须用 QPointer 守卫可能已关闭的对话框/视图。
class IDatasetCommandRepository {
public:
virtual ~IDatasetCommandRepository() = default;
// 反演模型列表GET /business/outerInversion/query/script?dsObjectId={ds}。
// measurement「反演运算」与「生成视电阻率」共用此端点原版 getInversionOptions /
// getProcessScriptList 同 URL。回调 list = 响应 data 数组(每项含 label/value/code
virtual void listInversionScripts(
const QString& dsObjectId,
std::function<void(bool ok, QJsonArray list, QString msg)> cb) = 0;
// 动态表单POST /business/project/getDynamicForm body {projectId, type:6, typeId}。
// 回调 data = 响应 data 对象(含 formList 数组:分组 → values 字段)。
virtual void getDynamicForm(
const QString& projectId, const QString& typeId,
std::function<void(bool ok, QJsonObject data, QString msg)> cb) = 0;
// 提交反演运算POST /business/outerInversion/submitInversionTask
// body {dsId, scriptId, properties:{fieldCode:value,...}}(对应原版 postInversionTask
virtual void submitInversionTask(const QString& dsId, const QString& scriptId,
const QJsonObject& properties,
std::function<void(bool ok, QString msg)> cb) = 0;
// 生成视电阻率数据POST /business/dd/ert/measurement/createVisualResistivityData
// body {dsObjectId, scriptId, scriptParamListJsonStr:{fieldCode:value,...}}
// (对应原版 createVisualResistivityData散点图模块版本
virtual void createVisualResistivityData(
const QString& dsObjectId, const QString& scriptId,
const QJsonObject& scriptParamListJsonStr,
std::function<void(bool ok, QString msg)> cb) = 0;
// ============================ measurement 散点相关 ============================
// 散点可见性持久化POST /business/dd/ert/measurement/saveDisplayStatus
// body {dsObjectId, ids:[...], status}(对应原版 updateScatterDataVisible
// 注:原版字段名为 dsObjectId非 dsObjectId 之外的 dsIdstatus 为 int0/1
virtual void saveDisplayStatus(const QString& dsObjectId, const QJsonArray& ids, int status,
std::function<void(bool ok, QString msg)> cb) = 0;
// 散点过滤配置查询GET /business/scatterPlotDataFilter/getDataFilterConfig
// ?dsObjectId={ds}&vFieldCode={vf}(对应原版 queryScatterFilterInfo
// 回调 data = 响应 data 对象(含 min/max 等配置)。
virtual void getScatterFilterConfig(
const QString& dsObjectId, const QString& vFieldCode,
std::function<void(bool ok, QJsonObject data, QString msg)> cb) = 0;
// 应用散点过滤POST /business/scatterPlotDataFilter
// body {sourceDsObjectId, sourceVFieldCode, min, max}(对应原版 applyScatterFilterInfo
// min/max 为字符串/数值由调用方组装,故收已组装 body。
virtual void applyScatterFilter(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) = 0;
// 另存原始数据POST /business/dd/ert/measurement/saveRawData
// body {dsObjectId, ...}(对应原版 saveRawDataValueprojectSpace 模块)。
// 原版仅约束 dsObjectId 必填operationType/name 由调用方按场景组装,故收 body。
virtual void saveRawData(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) = 0;
// 导出 DATGET /business/dd/ert/measurement/rs2d/export
// ?dsId={ds}&electrodePosition={ep}&ipDataMark={ip}&typeMeasurement={tm}
// (对应原版 exportScatterData2Dat。回调回传 base64 fileData 与 fileName
// 实际落盘/下载交由 UI 层处理。
virtual void exportMeasurementDat(
const QString& dsId, int electrodePosition, int ipDataMark, int typeMeasurement,
std::function<void(bool ok, QString fileName, QString fileData, QString msg)> cb) = 0;
// V 值切换重载散点M6并发拉 scatter/graph(带 vFieldCode) + 色阶 getDetail(type3,
// businessCode=vFieldCode),解析为完整 ScatterPayload对应原版 V 值 change 重新请求散点+色阶)。
// 与 ApiDatasetRepository 的初始 measurement 加载同端点/同解析,仅 vFieldCode 可变。
virtual void loadMeasurementScatter(
const QString& dsObjectId, const QString& vFieldCode,
std::function<void(bool ok, geopro::core::ScatterPayload payload, QString msg)> cb) = 0;
// ============================ 色阶lvl相关 ============================
// 保存 .lvl 色阶POST /business/lvl/colorGradation
// body {dsObjectId, templateId, businessCode, projectId, properties}
// (对应原版 newLvlColorLevel。measurement 散点与 inversion 原数据色阶配置共用。
virtual void saveColorGradation(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) = 0;
// ============================ inversion 相关 ============================
// inversion 另存为数据POST /business/dd/ert/inversion/saveAsData
// body {dsObjectId, name}(对应原版 saveVisualResistivityData
virtual void saveInversionAsData(const QString& dsObjectId, const QString& name,
std::function<void(bool ok, QString msg)> cb) = 0;
// 网格视图重载(处理类操作=网格化/白化/滤波 成功后重绘):并发拉 rows + 色阶 getDetail(type2)
// + 异常,解析为 ContourPayload与 ApiDatasetRepository::makeInversionGrid 同端点/同解析)。
// 与 loadMeasurementScatter 同范式仓储只读重载方法UI 在成功回调 setGridData 重绘。
virtual void loadInversionGrid(
const QString& dsObjectId,
std::function<void(bool ok, geopro::core::ContourPayload payload, QString msg)> cb) = 0;
// 网格化:查询算法模型 GET /business/dd/ert/inversion/queryAlgorithmModel/{ds}
// (对应原版 getGridModel。回调 list = data.value 数组。
virtual void listGridAlgorithm(
const QString& dsObjectId,
std::function<void(bool ok, QJsonArray list, QString msg)> cb) = 0;
// 网格化:获取原始数据参数 GET /business/dd/ert/inversion/getRawData/{ds}
// (对应原版 getGridParams。回调 data = 响应 data 对象。
virtual void getGridRawDataParams(
const QString& dsObjectId,
std::function<void(bool ok, QJsonObject data, QString msg)> cb) = 0;
// 网格化:执行 POST /business/dd/ert/inversion/grid
// body 含 {dsObjectId, actionCode, saveDataValueType, ...}(对应原版 toGridTheData 全字段)。
virtual void toGrid(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) = 0;
// ============================ 白化相关 ============================
// 白化:查询可白化数据列表 POST /business/dsObject/queryWhitenedDataList
// body {projectId, tmObjectId}(对应原版 getWhitenedDataList
// 回调 list = data.value 数组。
virtual void listWhitenedData(const QString& projectId, const QString& tmObjectId,
std::function<void(bool ok, QJsonArray list, QString msg)> cb) = 0;
// 白化:执行 POST /business/dd/ert/inversion/whitenedData
// body 含 {dsObjectId, whiteningMethod, ...}(对应原版 whitenTheData3 种 method 字段差异由调用方组装)。
virtual void whitenData(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) = 0;
// ============================ 滤波相关 ============================
// 滤波:查询滤波器列表 GET /business/filter/queryFilter对应原版 getFilters无 query
// 回调 list = data.value 数组。
virtual void listFilters(std::function<void(bool ok, QJsonArray list, QString msg)> cb) = 0;
// 滤波:新增滤波器 POST /business/filter
// body 含 {projectId, parentId, rowColumValue, type, ...}(对应原版 newTheFilter
virtual void newFilter(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) = 0;
// 滤波:删除滤波器 DELETE /business/filter/delete/{id}(对应原版 deleteTheFilter
virtual void deleteFilter(const QString& id,
std::function<void(bool ok, QString msg)> cb) = 0;
// 滤波:应用滤波处理数据 POST /business/dd/ert/inversion/filterData
// body 含 {dsObjectId, rowColumValue, ...}(对应原版 useFilterToProcessData 全字段)。
virtual void applyFilter(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) = 0;
// ============================ 异常 CRUD ============================
// 异常类型列表GET /business/exceptionType/queryExceptionTypeByProjectIdAndType/{pid}/{type}
// (对应原版 queryExceptionTypeDatatype = remarkSourceType。回调 list = data.value 数组。
virtual void listExceptionTypes(
const QString& projectId, const QString& remarkSourceType,
std::function<void(bool ok, QJsonArray list, QString msg)> cb) = 0;
// 获取异常名称POST /business/exception/getExceptionName
// body {exceptionTypeId, remarkSourceId}(对应原版 queryExceptionNameInProfileInversion
// 回调 data = 响应 data 对象(含建议名称)。
virtual void getExceptionName(
const QString& exceptionTypeId, const QString& remarkSourceId,
std::function<void(bool ok, QJsonObject data, QString msg)> cb) = 0;
// 新增异常POST /business/exception
// body 含 {exceptionName, exceptionTypeId, location, projectId, remarkSourceId, remarkSourceType, remark}
// (对应原版 newExceptionInProfileInversion 全字段)。
virtual void newException(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) = 0;
// 删除异常DELETE /business/exception/{id}(对应原版 deleteExceptionDataInProfileInversion
virtual void deleteException(const QString& id,
std::function<void(bool ok, QString msg)> cb) = 0;
// 更新异常PUT /business/exception
// body 含 {id, exceptionName, remark, ...}(对应原版 updateExceptionDataInProfileInversion
virtual void updateException(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) = 0;
// ============================ 自动标注 ============================
// 执行自动标注POST /business/exception/exception-mark/execute对应原版 executeExceptionMark
virtual void executeExceptionMark(const QJsonObject& body,
std::function<void(bool ok, QJsonObject data, QString msg)> cb) = 0;
// 批量创建异常POST /business/exception/batch/create对应原版 batchCreateException
virtual void batchCreateException(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) = 0;
// ============================ 描述 / dsObject ============================
// 获取 dsObject 详情GET /business/dsObject/getDetail/{ds}
// (对应原版 getProfileInversionDescription。回调 data = 响应 data 对象(含 description 等)。
virtual void getDsObjectDetail(
const QString& dsObjectId,
std::function<void(bool ok, QJsonObject data, QString msg)> cb) = 0;
// 更新 dsObjectPUT /business/dsObject/updateDsObject/
// body 含 {dsObjectId, description, attachedParameters:{deltaContent}, ...}
// (对应原版 updateProfileInversionDescription注意原版 URL 末尾带斜杠)。
virtual void updateDsObject(const QJsonObject& body,
std::function<void(bool ok, QString msg)> cb) = 0;
};
} // namespace geopro::data

View File

@ -139,6 +139,26 @@ target_sources(geopro_tests PRIVATE
app/test_dataset_dimension.cpp
${CMAKE_SOURCE_DIR}/src/app/DatasetDimension.cpp
)
# /parseDynamicForm/assembleFieldMap Qt6::Core JSON
target_sources(geopro_tests PRIVATE
app/test_inversion_form_parse.cpp
${CMAKE_SOURCE_DIR}/src/app/panels/chart/InversionFormParse.cpp
)
# measurement / id / / Qt6::Core JSON + core model
target_sources(geopro_tests PRIVATE
app/test_scatter_data_ops.cpp
${CMAKE_SOURCE_DIR}/src/app/panels/chart/ScatterDataOps.cpp
)
# // + code Qt6::Core JSON
target_sources(geopro_tests PRIVATE
app/test_inversion_process_ops.cpp
${CMAKE_SOURCE_DIR}/src/app/panels/chart/InversionProcessOps.cpp
)
# 线 Douglas-Peucker I8 Qt/VTK
target_sources(geopro_tests PRIVATE
app/test_contour_simplify.cpp
${CMAKE_SOURCE_DIR}/src/app/panels/chart/ContourSimplify.cpp
)
# controller DatasetDetailController QSignalSpy datasetOpened/tabReady/loadFailed
find_package(Qt6 COMPONENTS Test REQUIRED)

View File

@ -0,0 +1,42 @@
#include <gtest/gtest.h>
#include "panels/chart/ContourSimplify.hpp"
using geopro::app::douglasPeucker;
using geopro::core::Vec2;
TEST(ContourSimplify, TolZeroReturnsOriginal) {
std::vector<Vec2> pts{{0, 0}, {1, 0.01}, {2, 0}};
auto out = douglasPeucker(pts, 0.0);
EXPECT_EQ(out.size(), 3u);
}
TEST(ContourSimplify, ShortPolylineUnchanged) {
std::vector<Vec2> pts{{0, 0}, {5, 5}};
auto out = douglasPeucker(pts, 1.0);
EXPECT_EQ(out.size(), 2u); // 点数<=2 原样
}
TEST(ContourSimplify, CollinearMiddleDropped) {
// 共线点:中点在容差内 → 被抽掉,只保留首尾。
std::vector<Vec2> pts{{0, 0}, {1, 0}, {2, 0}, {3, 0}};
auto out = douglasPeucker(pts, 0.1);
ASSERT_EQ(out.size(), 2u);
EXPECT_DOUBLE_EQ(out.front().x, 0.0);
EXPECT_DOUBLE_EQ(out.back().x, 3.0);
}
TEST(ContourSimplify, SignificantPointKept) {
// 中间有明显偏移的点(偏 1.0 > tol 0.5)→ 保留。
std::vector<Vec2> pts{{0, 0}, {1, 1.0}, {2, 0}};
auto out = douglasPeucker(pts, 0.5);
EXPECT_EQ(out.size(), 3u);
}
TEST(ContourSimplify, LargerTolDropsMorePoints) {
std::vector<Vec2> pts{{0, 0}, {1, 0.2}, {2, 0}, {3, 0.2}, {4, 0}};
auto coarse = douglasPeucker(pts, 0.5); // 偏移 0.2 < 0.5 → 全抽成首尾
auto fine = douglasPeucker(pts, 0.05); // 0.2 > 0.05 → 保留中间峰
EXPECT_LT(coarse.size(), fine.size());
EXPECT_EQ(coarse.size(), 2u);
}

View File

@ -0,0 +1,87 @@
#include <gtest/gtest.h>
#include <QJsonDocument>
#include <QJsonObject>
#include "panels/chart/InversionFormParse.hpp"
using namespace geopro::app;
namespace {
// 取自原版动态表单响应 data 结构POST /business/project/getDynamicForm
// formList → [{groupName, values:[{fieldCode, fieldName, optionsObject:[{label,value}]}]}]。
const char* kFormData = R"({
"formList": [
{
"groupName": "基础参数",
"values": [
{ "fieldCode": "elevation", "fieldName": "是否含高程",
"optionsObject": [ {"label": "", "value": "1"}, {"label": "", "value": "0"} ] },
{ "fieldCode": "method", "fieldName": "反演方法",
"optionsObject": [ {"label": "最小二乘", "value": "ls"} ] }
]
},
{
"groupName": "高级参数",
"values": [
{ "fieldCode": "noOptions", "fieldName": "无选项字段", "optionsObject": [] }
]
}
]
})";
QJsonObject formData() { return QJsonDocument::fromJson(kFormData).object(); }
} // namespace
TEST(InversionFormParse, ParsesGroupsFieldsAndOptions) {
const auto groups = parseDynamicForm(formData());
ASSERT_EQ(groups.size(), 2u);
EXPECT_EQ(groups[0].groupName.toStdString(), std::string("基础参数"));
ASSERT_EQ(groups[0].fields.size(), 2u);
const auto& f0 = groups[0].fields[0];
EXPECT_EQ(f0.fieldCode.toStdString(), std::string("elevation"));
EXPECT_EQ(f0.fieldName.toStdString(), std::string("是否含高程"));
ASSERT_EQ(f0.options.size(), 2u);
EXPECT_EQ(f0.options[0].label.toStdString(), std::string(""));
EXPECT_EQ(f0.options[0].value.toStdString(), std::string("1"));
EXPECT_TRUE(groups[1].fields[0].options.empty());
}
TEST(InversionFormParse, EmptyDataYieldsNoGroups) {
EXPECT_TRUE(parseDynamicForm(QJsonObject{}).empty());
}
TEST(InversionFormParse, AssembleUsesSelectedValues) {
const auto groups = parseDynamicForm(formData());
QJsonObject selected{{"elevation", "0"}, {"method", "ls"}};
// fillDefaults=false反演运算仅含已选且非空的字段无选值字段不进体。
const QJsonObject out = assembleFieldMap(groups, selected, /*fillDefaults*/ false);
EXPECT_EQ(out.value("elevation").toString().toStdString(), std::string("0"));
EXPECT_EQ(out.value("method").toString().toStdString(), std::string("ls"));
EXPECT_FALSE(out.contains("noOptions")); // 无选项 + 未选 → 不进体
}
TEST(InversionFormParse, AssembleFillsDefaultsWhenRequested) {
const auto groups = parseDynamicForm(formData());
// fillDefaults=true生成视电阻率空选值回退首个选项。
const QJsonObject out = assembleFieldMap(groups, QJsonObject{}, /*fillDefaults*/ true);
EXPECT_EQ(out.value("elevation").toString().toStdString(), std::string("1")); // 首项 value
EXPECT_EQ(out.value("method").toString().toStdString(), std::string("ls"));
EXPECT_FALSE(out.contains("noOptions")); // 无选项 → 即便 fillDefaults 也无值可填
}
TEST(InversionFormParse, AssembleOmitsEmptySelectedValues) {
const auto groups = parseDynamicForm(formData());
QJsonObject selected{{"elevation", ""}}; // 显式空字符串
// fillDefaults=false空值不进体复刻原版 handleConfirm 的 if(selectedValue))。
const QJsonObject out = assembleFieldMap(groups, selected, /*fillDefaults*/ false);
EXPECT_FALSE(out.contains("elevation"));
}

View File

@ -0,0 +1,130 @@
#include <gtest/gtest.h>
#include <QJsonArray>
#include <QJsonObject>
#include "panels/chart/InversionProcessOps.hpp"
using namespace geopro::app;
// ── I1/O1 网格化 toGrid 体 ────────────────────────────────────────────────
TEST(InversionProcessOps, GridToBodyFieldsLinear) {
GridToParams p;
p.dsObjectId = QStringLiteral("ds1");
p.actionCode = QStringLiteral("alg_kriging");
p.xMin = -1.0; p.xMax = 99.0; p.yMin = -2.0; p.yMax = 50.0;
p.vMin = 5.0; p.vMax = 500.0;
p.xSize = 100; p.ySize = 60;
p.xSpacing = 1.0; p.ySpacing = 0.86;
p.logFormat = false;
auto b = buildGridToBody(p);
EXPECT_EQ(b.value("dsObjectId").toString().toStdString(), "ds1");
EXPECT_EQ(b.value("actionCode").toString().toStdString(), "alg_kriging");
EXPECT_DOUBLE_EQ(b.value("xminValue").toDouble(), -1.0);
EXPECT_DOUBLE_EQ(b.value("xmaxValue").toDouble(), 99.0);
EXPECT_DOUBLE_EQ(b.value("yminValue").toDouble(), -2.0);
EXPECT_DOUBLE_EQ(b.value("ymaxValue").toDouble(), 50.0);
EXPECT_EQ(b.value("xsize").toInt(), 100); // 小写
EXPECT_EQ(b.value("ysize").toInt(), 60); // 小写
EXPECT_DOUBLE_EQ(b.value("xSpacing").toDouble(), 1.0); // 驼峰
EXPECT_DOUBLE_EQ(b.value("ySpacing").toDouble(), 0.86); // 驼峰
EXPECT_DOUBLE_EQ(b.value("vminValue").toDouble(), 5.0);
EXPECT_DOUBLE_EQ(b.value("vmaxValue").toDouble(), 500.0);
EXPECT_EQ(b.value("saveDataValueType").toInt(), 1); // 线性=1
}
TEST(InversionProcessOps, GridToBodyLogFormat) {
GridToParams p;
p.logFormat = true;
EXPECT_EQ(buildGridToBody(p).value("saveDataValueType").toInt(), 2); // 对数=2
}
// ── I3 白化 whitenData 体 ──────────────────────────────────────────────────
TEST(InversionProcessOps, WhitenMethod1ExternalSendsBoolWay) {
WhitenParams p;
p.dsObjectId = QStringLiteral("ds1");
p.whiteningMethod = 1;
p.boundaryExtension = 3.5;
p.whiteningType = 0; // 外部 → whitenedWay=true
auto b = buildWhitenBody(p);
EXPECT_EQ(b.value("whiteningMethod").toInt(), 1);
EXPECT_DOUBLE_EQ(b.value("boundaryExtension").toDouble(), 3.5);
EXPECT_TRUE(b.value("whitenedWay").toBool());
EXPECT_FALSE(b.contains("whitenedDataId"));
}
TEST(InversionProcessOps, WhitenMethod1InternalWayFalse) {
WhitenParams p;
p.whiteningMethod = 1;
p.whiteningType = 1; // 内部 → whitenedWay=false
EXPECT_FALSE(buildWhitenBody(p).value("whitenedWay").toBool());
}
TEST(InversionProcessOps, WhitenMethod2SendsFileId) {
WhitenParams p;
p.whiteningMethod = 2;
p.whitenedDataId = QStringLiteral("file-9");
auto b = buildWhitenBody(p);
EXPECT_EQ(b.value("whitenedDataId").toString().toStdString(), "file-9");
EXPECT_FALSE(b.contains("boundaryExtension"));
}
TEST(InversionProcessOps, WhitenMethod3SendsSubType) {
WhitenParams p;
p.whiteningMethod = 3;
p.modelWhiteningSubType = 1; // 矩形
auto b = buildWhitenBody(p);
EXPECT_EQ(b.value("modelWhiteningSubType").toInt(), 1);
}
// ── I4 滤波 applyFilter 体 + code 映射 ──────────────────────────────────────
TEST(InversionProcessOps, BoundaryAndNoDataCodes) {
EXPECT_EQ(filterBoundaryCode(QStringLiteral("whitening")), 1);
EXPECT_EQ(filterBoundaryCode(QStringLiteral("skip")), 2);
EXPECT_EQ(filterBoundaryCode(QStringLiteral("edgePoint")), 3);
EXPECT_EQ(filterBoundaryCode(QStringLiteral("filling")), 4);
EXPECT_EQ(filterNoDataCode(QStringLiteral("expansion")), 1);
EXPECT_EQ(filterNoDataCode(QStringLiteral("retain")), 2);
EXPECT_EQ(filterNoDataCode(QStringLiteral("skip")), 3);
EXPECT_EQ(filterNoDataCode(QStringLiteral("filling")), 4);
}
TEST(InversionProcessOps, FilterApplyBodyFields) {
FilterApplyParams p;
p.dsObjectId = QStringLiteral("ds1");
p.dataEdge = QStringLiteral("filling");
p.dataEdgeValue = 12.0;
p.noDataPoints = QStringLiteral("skip");
p.noDataValue = 0.0;
p.number = 3;
p.row = 3; p.column = 3;
p.matrix = {{0, 1, 0}, {1, 1, 1}, {0, 1, 0}};
p.filteringMethod = QStringLiteral("中值滤波");
auto b = buildFilterApplyBody(p);
EXPECT_EQ(b.value("boundary").toInt(), 4); // filling
EXPECT_DOUBLE_EQ(b.value("boundaryValue").toDouble(), 12.0);
EXPECT_EQ(b.value("noDataPoints").toInt(), 3); // skip
EXPECT_DOUBLE_EQ(b.value("noDataPointsValue").toDouble(), 0.0);
EXPECT_EQ(b.value("number").toInt(), 3);
EXPECT_EQ(b.value("row").toInt(), 3);
EXPECT_EQ(b.value("column").toInt(), 3);
EXPECT_EQ(b.value("filteringMethod").toString().toStdString(), std::string("中值滤波"));
const auto form = b.value("rowColumValue").toObject().value("form").toArray();
ASSERT_EQ(form.size(), 3);
EXPECT_DOUBLE_EQ(form.at(1).toArray().at(1).toDouble(), 1.0); // 中心
EXPECT_DOUBLE_EQ(form.at(0).toArray().at(0).toDouble(), 0.0); // 角
}
TEST(InversionProcessOps, NewFilterBodyFields) {
auto b = buildNewFilterBody(QStringLiteral("自定义滤波器1"), QStringLiteral("pj1"),
QStringLiteral("grp-7"), {{1, 1}, {1, 1}});
EXPECT_EQ(b.value("type").toInt(), 1);
EXPECT_EQ(b.value("name").toString().toStdString(), std::string("自定义滤波器1"));
EXPECT_EQ(b.value("projectId").toString().toStdString(), "pj1");
EXPECT_EQ(b.value("parentId").toString().toStdString(), "grp-7");
const auto form = b.value("rowColumValue").toObject().value("form").toArray();
ASSERT_EQ(form.size(), 2);
EXPECT_EQ(form.at(0).toArray().size(), 2);
}

View File

@ -0,0 +1,87 @@
#include <gtest/gtest.h>
#include <cmath>
#include <QJsonObject>
#include "panels/chart/ScatterDataOps.hpp"
using namespace geopro::app;
using geopro::core::ScatterField;
TEST(ScatterDataOps, ValueTypeFromCode) {
EXPECT_EQ(scatterValueTypeFromCode(QStringLiteral("linearity")), ScatterValueType::Linearity);
EXPECT_EQ(scatterValueTypeFromCode(QStringLiteral("inverse")), ScatterValueType::Inverse);
EXPECT_EQ(scatterValueTypeFromCode(QStringLiteral("logarithm")), ScatterValueType::Logarithm);
// 未知回退线性。
EXPECT_EQ(scatterValueTypeFromCode(QStringLiteral("xyz")), ScatterValueType::Linearity);
}
TEST(ScatterDataOps, ApplyValueTypeLinearIsIdentity) {
std::vector<double> v{1.0, 10.0, 100.0};
auto out = applyScatterValueType(v, ScatterValueType::Linearity);
ASSERT_EQ(out.size(), 3u);
EXPECT_DOUBLE_EQ(out[0], 1.0);
EXPECT_DOUBLE_EQ(out[2], 100.0);
}
TEST(ScatterDataOps, ApplyValueTypeInverse) {
std::vector<double> v{2.0, 0.0, -4.0};
auto out = applyScatterValueType(v, ScatterValueType::Inverse);
EXPECT_DOUBLE_EQ(out[0], 0.5);
EXPECT_DOUBLE_EQ(out[1], 0.0); // v==0 → 保持 0避免 inf
EXPECT_DOUBLE_EQ(out[2], -0.25);
}
TEST(ScatterDataOps, ApplyValueTypeLog) {
std::vector<double> v{100.0, 0.0, -5.0};
auto out = applyScatterValueType(v, ScatterValueType::Logarithm);
EXPECT_DOUBLE_EQ(out[0], 2.0); // log10(100)
EXPECT_DOUBLE_EQ(out[1], 0.0); // v<=0 → 保持原值
EXPECT_DOUBLE_EQ(out[2], -5.0); // v<=0 → 保持原值
}
TEST(ScatterDataOps, CollectIdsForHideTakesVisible) {
ScatterField f;
f.id = {"a", "b", "c", ""};
f.displayStatus = {0, 1, 0, 0}; // a,c 可见b 隐藏;空 id 跳过
// 隐藏取可见点a, c
auto hideIds = collectScatterIds(f, /*hide*/ true);
ASSERT_EQ(hideIds.size(), 2);
EXPECT_EQ(hideIds.at(0).toString().toStdString(), "a");
EXPECT_EQ(hideIds.at(1).toString().toStdString(), "c");
}
TEST(ScatterDataOps, CollectIdsForShowTakesHidden) {
ScatterField f;
f.id = {"a", "b", "c"};
f.displayStatus = {0, 1, 1}; // b,c 隐藏
auto showIds = collectScatterIds(f, /*hide*/ false);
ASSERT_EQ(showIds.size(), 2);
EXPECT_EQ(showIds.at(0).toString().toStdString(), "b");
EXPECT_EQ(showIds.at(1).toString().toStdString(), "c");
}
TEST(ScatterDataOps, FilterBodyFields) {
auto body = buildScatterFilterBody(QStringLiteral("ds1"), QStringLiteral("R0"), -10.5, 200.0);
EXPECT_EQ(body.value("sourceDsObjectId").toString().toStdString(), "ds1");
EXPECT_EQ(body.value("sourceVFieldCode").toString().toStdString(), "R0");
EXPECT_DOUBLE_EQ(body.value("min").toDouble(), -10.5);
EXPECT_DOUBLE_EQ(body.value("max").toDouble(), 200.0);
}
TEST(ScatterDataOps, SaveRawDataBodyNewHasName) {
auto body = buildSaveRawDataBody(QStringLiteral("ds1"), /*operationType*/ 1,
QStringLiteral("新数据"));
EXPECT_EQ(body.value("dsId").toString().toStdString(), "ds1");
EXPECT_EQ(body.value("operationType").toInt(), 1);
EXPECT_TRUE(body.contains("name"));
EXPECT_EQ(body.value("name").toString().toStdString(), std::string("新数据"));
}
TEST(ScatterDataOps, SaveRawDataBodyOverwriteOmitsName) {
auto body = buildSaveRawDataBody(QStringLiteral("ds1"), /*operationType*/ 0,
QStringLiteral("ignored"));
EXPECT_EQ(body.value("operationType").toInt(), 0);
EXPECT_FALSE(body.contains("name")); // 覆盖不带 name
}

View File

@ -56,3 +56,18 @@ TEST(DatasetChartDto, ParsesAnomalyPolyline) {
EXPECT_DOUBLE_EQ(v[0].localPts[1].x, 3.0);
EXPECT_TRUE(v[0].dashed);
}
TEST(DatasetChartDto, ParsesAnomalyIdentityFields) {
// I10/I11/I12 需要 id删除/更新/定位)、备注、异常类型 id、创建时间。
auto arr = QJsonDocument::fromJson(
R"([{"id":"exc-1","exceptionName":"A1","exceptionTypeName":"Zone",
"exceptionTypeId":"type-9","exceptionMarkType":3,"remark":"备注X",
"createTime":"2026-06-22 10:00:00",
"location":{"coordinate":[{"x":1,"y":2},{"x":3,"y":4},{"x":5,"y":6}]}}])").array();
auto v = parseDatasetAnomalies(arr);
ASSERT_EQ(v.size(), 1u);
EXPECT_EQ(v[0].id, "exc-1");
EXPECT_EQ(v[0].exceptionTypeId, "type-9");
EXPECT_EQ(v[0].remark, "备注X");
EXPECT_EQ(v[0].createTime, "2026-06-22 10:00:00");
EXPECT_EQ(static_cast<int>(v[0].markType), 3);
}

View File

@ -75,4 +75,35 @@ TEST(GridDto, EmptyDataYieldsSeqOnlyColumnNoRows) {
EXPECT_EQ(t.columns[0].code.toStdString(), "__seq");
EXPECT_EQ(t.rows.size(), 0u);
EXPECT_EQ(t.total, 0);
EXPECT_TRUE(t.functionButtons.empty()); // 无 functionList → 无功能按钮
}
TEST(GridDto, EmptyFunctionListYieldsNoButtons) {
// 真实夹具 functionList:[] → functionButtons 空(不渲染工具条)。
auto t = parseGridTable(gridData(), 1, 50);
EXPECT_TRUE(t.functionButtons.empty());
}
TEST(GridDto, ParsesFunctionListIntoButtons) {
// functionList = DDGridFunctionVO[]functionCode/functionNameChn/enable→ functionButtons。
const char* kData = R"({
"gridHeaderDisplay": [
{ "columnCode": "x", "columnNameChn": "x", "columnSort": 1 },
{ "columnCode": "y", "columnNameChn": "y", "columnSort": 2 }
],
"functionList": [
{ "functionCode": "inversion", "functionNameChn": "反演", "enable": true },
{ "functionCode": "other", "functionNameChn": "其它", "enable": false }
],
"total": 1,
"rowList": [ { "x": 1.0, "y": 2.0, "id": "1" } ]
})";
auto t = parseGridTable(QJsonDocument::fromJson(kData).object(), 1, 50);
ASSERT_EQ(t.functionButtons.size(), 2u);
EXPECT_EQ(t.functionButtons[0].code.toStdString(), "inversion");
EXPECT_EQ(t.functionButtons[0].nameChn.toStdString(), "反演");
EXPECT_TRUE(t.functionButtons[0].enable);
EXPECT_EQ(t.functionButtons[1].code.toStdString(), "other");
EXPECT_FALSE(t.functionButtons[1].enable); // enable=false视图按 v-show 不渲染)
}

View File

@ -103,6 +103,26 @@ TEST(MeasurementDto, ParsesScatterPositionalRows) {
EXPECT_DOUBLE_EQ(s.electrodeNo[2], 3.0);
}
TEST(MeasurementDto, ParsesScatterPointMetadata) {
// [i]信息 / 显隐持久化用元数据id(col8) / displayStatus(col7) / row(col15) / pseu(col16)。
auto p = parseMeasurementScatter(obj(kScatterData), obj(kColorBarData));
const auto& s = p.scatter;
ASSERT_EQ(s.id.size(), 3u);
EXPECT_EQ(s.id[0], "1453611521843200");
EXPECT_EQ(s.id[1], "1453611521843201");
ASSERT_EQ(s.displayStatus.size(), 3u);
EXPECT_EQ(s.displayStatus[0], 0); // col7=0 → 可见
ASSERT_EQ(s.row.size(), 3u);
EXPECT_DOUBLE_EQ(s.row[0], 1.0); // DataRow = col15
EXPECT_DOUBLE_EQ(s.row[2], 3.0);
ASSERT_EQ(s.pseu.size(), 3u);
EXPECT_DOUBLE_EQ(s.pseu[0], 242.952988); // Pseu_Resis = col16
}
// scatter/graph 含 scatterGraphConf 时:工具条下拉项 + 默认选中 + x/y 本地重绘备选列。
static const char* kScatterDataWithConf = R"({
"electrodeList": [