feat(detail): 数据集详情视图交互复刻(measurement/inversion/grid)
对照原版 web 1:1 复刻数据集详情视图的写操作交互,补齐既有视图的全部 可交互能力。 基础设施 - 新增写操作命令仓储 IDatasetCommandRepository + ApiDatasetCommandRepository (26 个写/查接口,端点逐字对照原版 apis),回调式异步沿用 ApiColorTemplateRepository 模式 - 写操作注入链平行 setColorTemplateRepo:main→Panel→Page→DetailViewFactory→视图, 透传 cmdRepo + dsIdGetter - 新增共享对话框 InversionFormDialog/SaveAsDialog/ScatterFilterDialog/GridWizardDialog/ WhiteningDialog/FilterDialog/ExceptionDialog/ExceptionDetailDialog/AutoAnnotationDialog - 纯函数 InversionFormParse/ScatterDataOps/InversionProcessOps/ContourSimplify + 单测 measurement(M1-M13):可见性持久化、数据过滤、X/Y/V轴、值类型、色阶配置、 生成视电阻率、反演运算、另存为、导出DAT、信息点选 inversion 网格(I1-I15):网格化向导、白化、滤波、等值线提示、简化容差(真生效)、 异常增删改查+定位、自动标注、描述保存、另存为 inversion 原数据(O1-O3) + grid 反演(G1,functionList 驱动) 后置/降级(台账 §6.4):M14框选、M2行级可见性、M3过滤直方图、I9图上绘形、 I14富文本(Qt无Quill)、I3白化tmObjectId透传 测试 285/285 通过
This commit is contained in:
parent
08b8ebbf01
commit
12813bd8d0
|
|
@ -42,16 +42,20 @@
|
||||||
| ddCode | 详情组件 / 含义 | 空间形态 |
|
| ddCode | 详情组件 / 含义 | 空间形态 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| dd_trajectory_data | ElectrodeCom 电极/测线 | **线**(lat/lon 序列)|
|
| dd_trajectory_data | ElectrodeCom 电极/测线 | **线**(lat/lon 序列)|
|
||||||
| dd_transient_electromagnetic_trajectory_data | BatteryComMapLineCom 瞬变电磁测线 | **线** |
|
| //dd_transient_electromagnetic_trajectory_data | BatteryComMapLineCom 瞬变电磁测线 | **线** |
|
||||||
| dd_radar_channel_trajectory / dd_radar_rtk_trajectory | 雷达通道/RTK 轨迹 | **线** |
|
| //dd_radar_channel_trajectory / dd_radar_rtk_trajectory | 雷达通道/RTK 轨迹 | **线** |
|
||||||
| dd_inversion_data | ContourCom 等值面 | **竖直剖面**(沿测线距离 × 深度)|
|
| dd_inversion_data | ContourCom 等值面 | **剖面** |
|
||||||
| dd_ert_measurement_data | ScattersCom 散点 | **拟剖面**(电极对 × 视深,散点)|
|
| dd_ert_measurement_data | ScattersCom 散点 | **原始数据**(电极对 × 视深,散点) |
|
||||||
| dd_ert_measurement_gr_data | GroundResistanceCom 接地电阻 | 沿线序列 |
|
| //dd_ert_measurement_gr_data | GroundResistanceCom 接地电阻 | 沿线序列 |
|
||||||
| dd_gpr_channel_image / dd_radar / dd_gpr_channel_detail | 雷达图像/成果 | **竖直 B-scan**(沿轨迹 × 时深)|
|
| //dd_gpr_channel_image / dd_radar / //dd_gpr_channel_detail | 雷达图像/成果,//重新分析 | **竖直 B-scan**(沿轨迹 × 时深)|
|
||||||
| dd_grid | DdGridCom 网格 | 网格(**面 or 剖面,依数据而定,存疑**)|
|
| dd_grid | DdGridCom 网格,//扩展支持接地电阻 | 网格(**面 or 剖面,依数据而定,存疑**)|
|
||||||
| dd_voxel / dd_Structual3D / dd_Property3D | 体素 / 结构 / 属性 3D | **三维体** |
|
| dd_voxel / (dd_Structual3D) / //dd_Property3D | 体素 / (结构) / //属性 3D | **三维体** |
|
||||||
| dd_3d_show | ThreeModelCom 3D 模型 | **三维模型** |
|
| //dd_3d_show | ThreeModelCom 3D 模型 | **三维模型** |
|
||||||
| dd_slice | 切片 | 三维分析 |
|
| dd_slice | 切片 | 三维分析 |
|
||||||
|
| ??dd_time_sensor | TimingSensor 时序传感器(折线图+数据表)| 时序曲线(非空间)**待确认** |
|
||||||
|
| ??dd_current_method_indicator | DdElectricDetectionCom 电流法指标(指标表+散点状态图)| 指标/状态(非空间)**待确认** |
|
||||||
|
|
||||||
|
> `??` 标记:原版 datasetInfo 有此详情组件,但初版台账漏列,详情视图是否纳入客户端范围**待确认**(2026-06-22 补)。雷达/GPR 家族(`//` 标记)因客户端将重构、客户需求未定,整体搁置。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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→改单点 | 同 M1(ids=[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) | ✅ **已通**(复用 ColorScaleConfigDialog;properties 含 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 | 色阶配置 | 散点色阶(type1,businessCode 空) | ✅ **已通**(openInversionColorScale;properties 含 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),再接 UI;UI 视觉对照原版。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 复刻收尾状态(2026-06-22)
|
||||||
|
|
||||||
|
三份审查后的修正项已落地,build 通过 + 测试全过(285/285)。
|
||||||
|
|
||||||
|
### 6.1 已 100% 接通项
|
||||||
|
|
||||||
|
- **measurement(M1~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 审查修正项(本轮)
|
||||||
|
|
||||||
|
- **重复 connect(M13/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 交互重 | 接入 QwtPlotPicker(RubberBand 矩形)+ 选区命中→批量 saveDisplayStatus;保留占位提示 |
|
||||||
|
| **M2 行级可见性 switch** | DataTableView 需新增可选开关列 + 行级 popconfirm 交互 | 给 measurement 列表加 optional 开关列,复用 saveDisplayStatus(ids=[record.id],status 取反) |
|
||||||
|
| **M3 过滤直方图** | 过滤范围已通,仅缺直方图绘制(须取 getDataFilterConfig 分桶并渲染) | 在 ScatterFilterDialog 加直方图视图(分桶 + min/max 区间叠加) |
|
||||||
|
| **I9 异常图上绘形** | 表单已通;图上交互绘制多边形/折线/点(橡皮筋 + 顶点编辑)属重型 Qwt 交互 | 接入图上绘制工具(绘形→坐标回填 location),与表单提交合流 |
|
||||||
|
| **I14 Quill 富文本** | 原版 attachedParameters.deltaContent 为 Quill Delta;Qt 暂降级为纯文本 | 引入富文本编辑器(QTextEdit 富文本 ↔ Delta 互转)或保持纯文本兜底 |
|
||||||
|
| **I3 白化 tmObjectId 透传** | 客户端视图未透传 `structParentId`(白化模板列表用),现兜底空串 | 上游改造:数据集列表把 `structParentId` 接进视图(属上游数据流改造) |
|
||||||
|
|
@ -39,6 +39,20 @@ add_executable(geopro_desktop WIN32
|
||||||
panels/ObjectExceptionPanel.cpp
|
panels/ObjectExceptionPanel.cpp
|
||||||
panels/DescriptionPanel.cpp
|
panels/DescriptionPanel.cpp
|
||||||
panels/chart/RawDataChartView.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/GridDataChartView.cpp
|
||||||
panels/chart/DataTableView.cpp
|
panels/chart/DataTableView.cpp
|
||||||
panels/chart/TablePager.cpp
|
panels/chart/TablePager.cpp
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@
|
||||||
#include "api/ApiProjectRepository.hpp"
|
#include "api/ApiProjectRepository.hpp"
|
||||||
#include "api/ApiDatasetRepository.hpp"
|
#include "api/ApiDatasetRepository.hpp"
|
||||||
#include "api/ApiColorTemplateRepository.hpp"
|
#include "api/ApiColorTemplateRepository.hpp"
|
||||||
|
#include "api/ApiDatasetCommandRepository.hpp"
|
||||||
#include "api/Api3dRepository.hpp"
|
#include "api/Api3dRepository.hpp"
|
||||||
#include "panels/ObjectTreePanel.hpp"
|
#include "panels/ObjectTreePanel.hpp"
|
||||||
#include "login/LoginWindow.hpp"
|
#include "login/LoginWindow.hpp"
|
||||||
|
|
@ -234,6 +235,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
geopro::data::IAsyncProjectRepository& projectRepo,
|
geopro::data::IAsyncProjectRepository& projectRepo,
|
||||||
geopro::data::IAsyncDatasetRepository& datasetRepo,
|
geopro::data::IAsyncDatasetRepository& datasetRepo,
|
||||||
geopro::data::IColorTemplateRepository& colorTplRepo,
|
geopro::data::IColorTemplateRepository& colorTplRepo,
|
||||||
|
geopro::data::IDatasetCommandRepository& cmdRepo,
|
||||||
geopro::controller::WorkbenchNavController& nav,
|
geopro::controller::WorkbenchNavController& nav,
|
||||||
geopro::controller::DatasetDetailController& detailCtrl)
|
geopro::controller::DatasetDetailController& detailCtrl)
|
||||||
{
|
{
|
||||||
|
|
@ -947,6 +949,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
auto* detailPanel = new geopro::app::DatasetDetailPanel();
|
auto* detailPanel = new geopro::app::DatasetDetailPanel();
|
||||||
// 注入色阶模板仓储 + 当前 projectId 取值回调(网格剖面「色阶配置」编辑器的另存/打开/新建色阶)。
|
// 注入色阶模板仓储 + 当前 projectId 取值回调(网格剖面「色阶配置」编辑器的另存/打开/新建色阶)。
|
||||||
detailPanel->setColorTemplateRepo(&colorTplRepo, [&nav] { return nav.currentProjectId(); });
|
detailPanel->setColorTemplateRepo(&colorTplRepo, [&nav] { return nav.currentProjectId(); });
|
||||||
|
// 注入反演命令仓储(measurement 反演运算/生成视电阻率)。projectId 取值仍由页内 projectIdGetter 提供。
|
||||||
|
detailPanel->setCommandRepo(&cmdRepo);
|
||||||
auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情"));
|
auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情"));
|
||||||
// ForceNoScrollArea:禁止 ADS 默认把整块内容(含标题栏/页签栏)包进 QScrollArea。
|
// ForceNoScrollArea:禁止 ADS 默认把整块内容(含标题栏/页签栏)包进 QScrollArea。
|
||||||
// 否则内容最小尺寸超过 dock 时,整个面板(标题+页签+图)一起滚动。改为内容自适应填充;
|
// 否则内容最小尺寸超过 dock 时,整个面板(标题+页签+图)一起滚动。改为内容自适应填充;
|
||||||
|
|
@ -1844,6 +1848,8 @@ int main(int argc, char* argv[])
|
||||||
geopro::data::ApiDatasetRepository datasetRepo(api);
|
geopro::data::ApiDatasetRepository datasetRepo(api);
|
||||||
// 色阶模板仓储(lvl 模板 + clr 色阶):同一共享会话 ApiClient,注入 2D/3D 色阶编辑器。
|
// 色阶模板仓储(lvl 模板 + clr 色阶):同一共享会话 ApiClient,注入 2D/3D 色阶编辑器。
|
||||||
geopro::data::ApiColorTemplateRepository colorTplRepo(api);
|
geopro::data::ApiColorTemplateRepository colorTplRepo(api);
|
||||||
|
// 反演命令仓储(反演运算 / 生成视电阻率 / 模型列表 / 动态表单):同一共享会话 ApiClient。
|
||||||
|
geopro::data::ApiDatasetCommandRepository cmdRepo(api);
|
||||||
// 策略注册表须比 detailCtrl 活得久(同作用域、在前声明)。
|
// 策略注册表须比 detailCtrl 活得久(同作用域、在前声明)。
|
||||||
geopro::controller::ChartStrategyRegistry chartRegistry;
|
geopro::controller::ChartStrategyRegistry chartRegistry;
|
||||||
chartRegistry.add(std::make_unique<geopro::app::ErtInversionStrategy>());
|
chartRegistry.add(std::make_unique<geopro::app::ErtInversionStrategy>());
|
||||||
|
|
@ -1863,7 +1869,8 @@ int main(int argc, char* argv[])
|
||||||
// 启动防护:工作台构建期间任何同步加载失败(如样本数据缺失)都不应让进程静默退出,
|
// 启动防护:工作台构建期间任何同步加载失败(如样本数据缺失)都不应让进程静默退出,
|
||||||
// 而是给出可见错误提示。否则登录后窗口未显示、进程消失,用户无从排查。
|
// 而是给出可见错误提示。否则登录后窗口未显示、进程消失,用户无从排查。
|
||||||
try {
|
try {
|
||||||
buildWorkbench(*window, repo, projectRepo, datasetRepo, colorTplRepo, nav, detailCtrl);
|
buildWorkbench(*window, repo, projectRepo, datasetRepo, colorTplRepo, cmdRepo, nav,
|
||||||
|
detailCtrl);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
QMessageBox::critical(
|
QMessageBox::critical(
|
||||||
nullptr, QStringLiteral("启动失败"),
|
nullptr, QStringLiteral("启动失败"),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
#include "panels/AnomalyTablePanel.hpp"
|
#include "panels/AnomalyTablePanel.hpp"
|
||||||
#include <QVBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QTableWidget>
|
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QTableWidget>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
static QString markName(int t) { return t == 1 ? "点" : t == 3 ? "多边形" : "多段线"; }
|
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()));
|
table_->setRowCount(static_cast<int>(list.size()));
|
||||||
for (int i = 0; i < static_cast<int>(list.size()); ++i) {
|
for (int i = 0; i < static_cast<int>(list.size()); ++i) {
|
||||||
const auto& a = list[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, 0, new QTableWidgetItem(QString::fromStdString(a.name)));
|
||||||
table_->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(a.typeName)));
|
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, 2, new QTableWidgetItem(markName(static_cast<int>(a.markType))));
|
||||||
table_->setItem(i, 3, new QTableWidgetItem(i < (int)createTimes.size() ? createTimes[i] : ""));
|
table_->setItem(i, 3, new QTableWidgetItem(ct));
|
||||||
table_->setItem(i, 4, new QTableWidgetItem(i < (int)remarks.size() ? remarks[i] : ""));
|
table_->setItem(i, 4, new QTableWidgetItem(rm));
|
||||||
auto* eye = new QToolButton(table_); eye->setCheckable(true); eye->setChecked(true);
|
|
||||||
eye->setText("👁");
|
// 操作列:定位 / 详情 / 删除(对照原版 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) {
|
connect(eye, &QToolButton::toggled, this, [this, i](bool on) {
|
||||||
if (on) hidden_.erase(i); else hidden_.insert(i);
|
if (on) hidden_.erase(i); else hidden_.insert(i);
|
||||||
emit hiddenChanged(hidden_);
|
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
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,23 @@
|
||||||
class QTableWidget;
|
class QTableWidget;
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
// ds 级异常表:名称/异常类型/几何类型/创建时间/备注/操作(显隐眼睛)。行显隐 → 信号驱动图表叠加。
|
// ds 级异常表:名称/异常类型/几何类型/创建时间/备注/操作。
|
||||||
|
// 操作列对照原版 contourPage:定位 / 详情 / 删除(删除带二次确认)。
|
||||||
|
// 行级显隐(眼睛)保留:信号驱动图表叠加(与原版分开,眼睛为客户端既有能力)。
|
||||||
class AnomalyTablePanel : public QWidget {
|
class AnomalyTablePanel : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit AnomalyTablePanel(QWidget* parent = nullptr);
|
explicit AnomalyTablePanel(QWidget* parent = nullptr);
|
||||||
|
// createTimes/remarks 形参保留兼容旧调用;为空时回退到 Anomaly 自带字段。
|
||||||
void setAnomalies(const std::vector<geopro::core::Anomaly>& list,
|
void setAnomalies(const std::vector<geopro::core::Anomaly>& list,
|
||||||
const std::vector<QString>& createTimes,
|
const std::vector<QString>& createTimes = {},
|
||||||
const std::vector<QString>& remarks);
|
const std::vector<QString>& remarks = {});
|
||||||
signals:
|
signals:
|
||||||
void hiddenChanged(const std::set<int>& hiddenIndices);
|
void hiddenChanged(const std::set<int>& hiddenIndices);
|
||||||
|
// 操作列:传出异常在当前列表中的下标(调用方据此取 Anomaly 拿 id/坐标)。
|
||||||
|
void locateRequested(int index);
|
||||||
|
void detailRequested(int index);
|
||||||
|
void deleteRequested(int index);
|
||||||
private:
|
private:
|
||||||
QTableWidget* table_;
|
QTableWidget* table_;
|
||||||
std::set<int> hidden_;
|
std::set<int> hidden_;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ void DatasetDetailPage::setColorTemplateRepo(geopro::data::IColorTemplateReposit
|
||||||
projectIdGetter_ = std::move(projectIdGetter);
|
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,
|
void DatasetDetailPage::build(const QString& dsId, const QString& ddCode, const QString& dsName,
|
||||||
const std::vector<geopro::controller::TabSpec>& tabs) {
|
const std::vector<geopro::controller::TabSpec>& tabs) {
|
||||||
Q_ASSERT(views_.empty()); // build() 仅一次:views_ 为已 release 的裸指针,二次构建会泄漏旧视图
|
Q_ASSERT(views_.empty()); // build() 仅一次:views_ 为已 release 的裸指针,二次构建会泄漏旧视图
|
||||||
|
|
@ -43,9 +47,10 @@ void DatasetDetailPage::build(const QString& dsId, const QString& ddCode, const
|
||||||
QVector<PanelTab> panelTabs;
|
QVector<PanelTab> panelTabs;
|
||||||
for (size_t i = 0; i < tabs.size(); ++i) {
|
for (size_t i = 0; i < tabs.size(); ++i) {
|
||||||
const auto& spec = tabs[i];
|
const auto& spec = tabs[i];
|
||||||
// 仓储与 projectId 回调透传给工厂(仅 FilledContour 视图消费)。
|
// 仓储与 projectId 回调透传给工厂(FilledContour 用色阶模板仓储;Scatter 用反演命令仓储)。
|
||||||
auto view = makeDetailView(spec.kind, this, colorTplRepo_,
|
// dsIdGetter 用本页 dsId_(此处已赋值),随项目/数据集稳定。
|
||||||
projectIdGetter_); // 抛出由调用栈兜底(GuardedApplication)
|
auto view = makeDetailView(spec.kind, this, colorTplRepo_, projectIdGetter_, cmdRepo_,
|
||||||
|
[this] { return dsId_; }); // 抛出由调用栈兜底(GuardedApplication)
|
||||||
IDetailView* raw = view.release(); // QWidget 由 this/QwtPlot 父子树接管生命周期
|
IDetailView* raw = view.release(); // QWidget 由 this/QwtPlot 父子树接管生命周期
|
||||||
views_[i] = raw;
|
views_[i] = raw;
|
||||||
// lazy 页签:建覆盖该视图的加载遮罩(父为视图 widget,随其尺寸覆盖图区)。
|
// lazy 页签:建覆盖该视图的加载遮罩(父为视图 widget,随其尺寸覆盖图区)。
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
namespace geopro::data {
|
namespace geopro::data {
|
||||||
class IColorTemplateRepository;
|
class IColorTemplateRepository;
|
||||||
|
class IDatasetCommandRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
@ -27,6 +28,10 @@ public:
|
||||||
void setColorTemplateRepo(geopro::data::IColorTemplateRepository* repo,
|
void setColorTemplateRepo(geopro::data::IColorTemplateRepository* repo,
|
||||||
std::function<QString()> projectIdGetter);
|
std::function<QString()> projectIdGetter);
|
||||||
|
|
||||||
|
// 反演命令仓储注入(measurement 反演运算/生成视电阻率,须在 build 前设置)。
|
||||||
|
// dsId 用本页 dsId_(build 内构造 dsIdGetter,此时 dsId_ 已赋值);projectId 复用上面的 getter。
|
||||||
|
void setCommandRepo(geopro::data::IDatasetCommandRepository* repo);
|
||||||
|
|
||||||
// 按页签集构建页签(首次打开调一次)。dsId/ddCode/dsName 用于 tabNeeded。
|
// 按页签集构建页签(首次打开调一次)。dsId/ddCode/dsName 用于 tabNeeded。
|
||||||
void build(const QString& dsId, const QString& ddCode, const QString& dsName,
|
void build(const QString& dsId, const QString& ddCode, const QString& dsName,
|
||||||
const std::vector<geopro::controller::TabSpec>& tabs);
|
const std::vector<geopro::controller::TabSpec>& tabs);
|
||||||
|
|
@ -63,6 +68,9 @@ private:
|
||||||
// 色阶模板仓储注入(透传给 makeDetailView → 网格剖面色阶编辑器)。
|
// 色阶模板仓储注入(透传给 makeDetailView → 网格剖面色阶编辑器)。
|
||||||
geopro::data::IColorTemplateRepository* colorTplRepo_ = nullptr;
|
geopro::data::IColorTemplateRepository* colorTplRepo_ = nullptr;
|
||||||
std::function<QString()> projectIdGetter_;
|
std::function<QString()> projectIdGetter_;
|
||||||
|
|
||||||
|
// 反演命令仓储注入(透传给 makeDetailView → measurement 散点视图反演按钮)。
|
||||||
|
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ void DatasetDetailPanel::setColorTemplateRepo(geopro::data::IColorTemplateReposi
|
||||||
projectIdGetter_ = std::move(projectIdGetter);
|
projectIdGetter_ = std::move(projectIdGetter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DatasetDetailPanel::setCommandRepo(geopro::data::IDatasetCommandRepository* repo) {
|
||||||
|
cmdRepo_ = repo;
|
||||||
|
}
|
||||||
|
|
||||||
DatasetDetailPanel::DatasetDetailPanel(QWidget* parent) : QTabWidget(parent) {
|
DatasetDetailPanel::DatasetDetailPanel(QWidget* parent) : QTabWidget(parent) {
|
||||||
setTabsClosable(true);
|
setTabsClosable(true);
|
||||||
connect(this, &QTabWidget::tabCloseRequested, this, [this](int i) { widget(i)->deleteLater(); });
|
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);
|
p = new DatasetDetailPage(this);
|
||||||
// 注入须在 build 前(build 内造视图时即透传给工厂)。
|
// 注入须在 build 前(build 内造视图时即透传给工厂)。
|
||||||
p->setColorTemplateRepo(colorTplRepo_, projectIdGetter_);
|
p->setColorTemplateRepo(colorTplRepo_, projectIdGetter_);
|
||||||
|
p->setCommandRepo(cmdRepo_);
|
||||||
p->build(dsId, ddCode, dsName, tabs); // ddCode 透传 → 页内 tabNeeded 携带
|
p->build(dsId, ddCode, dsName, tabs); // ddCode 透传 → 页内 tabNeeded 携带
|
||||||
const QString title = dsName.isEmpty() ? dsId : dsName; // 页签标题用数据名(空则回退 id)
|
const QString title = dsName.isEmpty() ? dsId : dsName; // 页签标题用数据名(空则回退 id)
|
||||||
const int idx = addTab(p, title);
|
const int idx = addTab(p, title);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include "DatasetDetailTab.hpp" // geopro::controller::TabSpec
|
#include "DatasetDetailTab.hpp" // geopro::controller::TabSpec
|
||||||
namespace geopro::data {
|
namespace geopro::data {
|
||||||
class IColorTemplateRepository;
|
class IColorTemplateRepository;
|
||||||
|
class IDatasetCommandRepository;
|
||||||
}
|
}
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
class DatasetDetailPage;
|
class DatasetDetailPage;
|
||||||
|
|
@ -21,6 +22,9 @@ public:
|
||||||
void setColorTemplateRepo(geopro::data::IColorTemplateRepository* repo,
|
void setColorTemplateRepo(geopro::data::IColorTemplateRepository* repo,
|
||||||
std::function<QString()> projectIdGetter);
|
std::function<QString()> projectIdGetter);
|
||||||
|
|
||||||
|
// 反演命令仓储:透传给每个新建的详情页(measurement 反演运算/生成视电阻率用)。
|
||||||
|
void setCommandRepo(geopro::data::IDatasetCommandRepository* repo);
|
||||||
|
|
||||||
// 数据集打开:find-or-create 页 → build(tabs) → 加/抬该面板页签。
|
// 数据集打开:find-or-create 页 → build(tabs) → 加/抬该面板页签。
|
||||||
void onDatasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName,
|
void onDatasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName,
|
||||||
const std::vector<geopro::controller::TabSpec>& tabs);
|
const std::vector<geopro::controller::TabSpec>& tabs);
|
||||||
|
|
@ -42,5 +46,8 @@ private:
|
||||||
// 色阶模板仓储注入(新页 build 前 setColorTemplateRepo 透传)。
|
// 色阶模板仓储注入(新页 build 前 setColorTemplateRepo 透传)。
|
||||||
geopro::data::IColorTemplateRepository* colorTplRepo_ = nullptr;
|
geopro::data::IColorTemplateRepository* colorTplRepo_ = nullptr;
|
||||||
std::function<QString()> projectIdGetter_;
|
std::function<QString()> projectIdGetter_;
|
||||||
|
|
||||||
|
// 反演命令仓储注入(新页 build 前 setCommandRepo 透传)。
|
||||||
|
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
|
||||||
};
|
};
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,38 @@
|
||||||
#include "panels/DescriptionPanel.hpp"
|
#include "panels/DescriptionPanel.hpp"
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QPushButton>
|
||||||
#include <QTextEdit>
|
#include <QTextEdit>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "Theme.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
DescriptionPanel::DescriptionPanel(QWidget* parent) : QWidget(parent) {
|
DescriptionPanel::DescriptionPanel(QWidget* parent) : QWidget(parent) {
|
||||||
auto* lay = new QVBoxLayout(this);
|
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_ = new QTextEdit(this);
|
||||||
edit_->setReadOnly(true);
|
|
||||||
edit_->setPlaceholderText(QStringLiteral("暂无描述"));
|
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) {
|
void DescriptionPanel::setText(const QString& text) { edit_->setPlainText(text); }
|
||||||
edit_->setPlainText(text);
|
|
||||||
}
|
QString DescriptionPanel::text() const { return edit_->toPlainText(); }
|
||||||
|
|
||||||
|
void DescriptionPanel::setSaveEnabled(bool on) { saveBtn_->setEnabled(on); }
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,26 @@
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
class QTextEdit;
|
class QTextEdit;
|
||||||
|
class QPushButton;
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
// 数据集描述面板:只读文本,供网格数据底部页签「描述」使用。
|
// 数据集描述面板:可编辑文本 + 保存按钮(I14)。
|
||||||
|
// 原版用 Quill 富文本(Delta),Qt 无对应控件 → 退化为纯文本编辑 + 保存;
|
||||||
|
// 保存时由调用方组装 {description, attachedParameters:{deltaContent}}(见 GridDataChartView)。
|
||||||
class DescriptionPanel : public QWidget {
|
class DescriptionPanel : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit DescriptionPanel(QWidget* parent = nullptr);
|
explicit DescriptionPanel(QWidget* parent = nullptr);
|
||||||
void setText(const QString& text);
|
void setText(const QString& text);
|
||||||
|
QString text() const;
|
||||||
|
// 注入「保存」可用性:无 cmdRepo/dsId 时禁用保存按钮(占位)。
|
||||||
|
void setSaveEnabled(bool on);
|
||||||
|
signals:
|
||||||
|
void saveRequested(const QString& text);
|
||||||
private:
|
private:
|
||||||
QTextEdit* edit_;
|
QTextEdit* edit_;
|
||||||
|
QPushButton* saveBtn_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <qwt_scale_map.h>
|
#include <qwt_scale_map.h>
|
||||||
|
|
||||||
#include "panels/chart/ColorMapService.hpp"
|
#include "panels/chart/ColorMapService.hpp"
|
||||||
|
#include "panels/chart/ContourSimplify.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
|
|
@ -41,6 +42,7 @@ void ContourPlotItem::setData(const core::Grid& g, ColorMapService* svc,
|
||||||
static_cast<int>(g.y.size()) < ny) {
|
static_cast<int>(g.y.size()) < ny) {
|
||||||
fillImage_ = QImage();
|
fillImage_ = QImage();
|
||||||
dataBBox_ = QRectF();
|
dataBBox_ = QRectF();
|
||||||
|
linesRaw_.clear();
|
||||||
lines_.clear();
|
lines_.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -57,15 +59,60 @@ void ContourPlotItem::setData(const core::Grid& g, ColorMapService* svc,
|
||||||
opt.upsample = 2;
|
opt.upsample = 2;
|
||||||
opt.makeLines = true;
|
opt.makeLines = true;
|
||||||
auto res = render::buildContourBands(g, svc->scale(), opt);
|
auto res = render::buildContourBands(g, svc->scale(), opt);
|
||||||
lines_ = std::move(res.lines);
|
linesRaw_ = std::move(res.lines);
|
||||||
|
lines_ = linesRaw_;
|
||||||
// buildContourBands 当前未回填 level(恒 0);在此按线上代表点采网格值并吸附到最近色阶级,
|
// buildContourBands 当前未回填 level(恒 0);在此按线上代表点采网格值并吸附到最近色阶级,
|
||||||
// 使标注显示真实等值线值。
|
// 使标注显示真实等值线值。
|
||||||
resolveLineLevels(g, svc->scale());
|
resolveLineLevels(g, svc->scale());
|
||||||
|
linesRaw_ = lines_; // level 回填后同步到原始集(简化保留 level)。
|
||||||
|
applySimplify(); // 按当前容差抽稀(首次 tol=0 即原样)。
|
||||||
} else {
|
} else {
|
||||||
|
linesRaw_.clear();
|
||||||
lines_.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) {
|
void ContourPlotItem::resolveLineLevels(const core::Grid& g, const core::ColorScale& cs) {
|
||||||
const auto stops = cs.stopValues();
|
const auto stops = cs.stopValues();
|
||||||
if (stops.empty() || lines_.empty()) return;
|
if (stops.empty() || lines_.empty()) return;
|
||||||
|
|
@ -158,6 +205,19 @@ QRectF ContourPlotItem::boundingRect() const {
|
||||||
return dataBBox_;
|
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,
|
void ContourPlotItem::draw(QPainter* painter, const QwtScaleMap& xMap, const QwtScaleMap& yMap,
|
||||||
const QRectF& /*canvasRect*/) const {
|
const QRectF& /*canvasRect*/) const {
|
||||||
if (dataBBox_.isNull()) return;
|
if (dataBBox_.isNull()) return;
|
||||||
|
|
@ -253,18 +313,21 @@ void ContourPlotItem::draw(QPainter* painter, const QwtScaleMap& xMap, const Qwt
|
||||||
painter->save();
|
painter->save();
|
||||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
painter->setRenderHint(QPainter::Antialiasing, true);
|
||||||
painter->setBrush(Qt::NoBrush);
|
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;
|
if (a.localPts.empty()) continue;
|
||||||
|
const bool hl = (ai == highlightIdx_); // I12 当前定位高亮
|
||||||
QColor col(QString::fromStdString(a.lineColor));
|
QColor col(QString::fromStdString(a.lineColor));
|
||||||
if (!col.isValid()) col = QColor(0, 0, 0);
|
if (!col.isValid()) col = QColor(0, 0, 0);
|
||||||
QPen pen(col);
|
QPen pen(hl ? QColor(255, 255, 0) : col); // 高亮黄(对照原版 #ffff00)
|
||||||
pen.setWidthF(a.lineWidth > 0 ? a.lineWidth : 1.0);
|
pen.setWidthF(hl ? std::max(3.0, a.lineWidth) : (a.lineWidth > 0 ? a.lineWidth : 1.0));
|
||||||
pen.setStyle(a.dashed ? Qt::DashLine : Qt::SolidLine);
|
pen.setStyle(hl ? Qt::SolidLine : (a.dashed ? Qt::DashLine : Qt::SolidLine));
|
||||||
painter->setPen(pen);
|
painter->setPen(pen);
|
||||||
|
|
||||||
if (a.markType == core::AnomalyMarkType::Point) {
|
if (a.markType == core::AnomalyMarkType::Point) {
|
||||||
const QPointF c = mapPt(a.localPts.front());
|
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 {
|
} else {
|
||||||
QPolygonF poly;
|
QPolygonF poly;
|
||||||
poly.reserve(static_cast<int>(a.localPts.size()));
|
poly.reserve(static_cast<int>(a.localPts.size()));
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,22 @@ public:
|
||||||
void setShowLines(bool on) { showLines_ = on; }
|
void setShowLines(bool on) { showLines_ = on; }
|
||||||
void setShowLabels(bool on) { showLabels_ = on; }
|
void setShowLabels(bool on) { showLabels_ = on; }
|
||||||
void setShowAnomalies(bool on) { showAnomalies_ = 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 setLineColor(const core::Rgba& c) { lineColor_ = c; }
|
||||||
void setLineDashed(bool dashed) { lineDashed_ = dashed; }
|
void setLineDashed(bool dashed) { lineDashed_ = dashed; }
|
||||||
void setLabelColor(const core::Rgba& c) { labelColor_ = c; }
|
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; }
|
int rtti() const override { return QwtPlotItem::Rtti_PlotUserItem; }
|
||||||
|
|
||||||
QRectF boundingRect() const override;
|
QRectF boundingRect() const override;
|
||||||
|
|
@ -48,10 +59,13 @@ public:
|
||||||
private:
|
private:
|
||||||
void buildFillImage(const core::Grid& g, ColorMapService* svc);
|
void buildFillImage(const core::Grid& g, ColorMapService* svc);
|
||||||
void resolveLineLevels(const core::Grid& g, const core::ColorScale& cs);
|
void resolveLineLevels(const core::Grid& g, const core::ColorScale& cs);
|
||||||
|
void applySimplify(); // 从 linesRaw_ 按 simplifyTol_ 重算 lines_(保留 level)
|
||||||
|
|
||||||
QImage fillImage_; // 预渲染填充热力图(ARGB32,含透明无数据区)
|
QImage fillImage_; // 预渲染填充热力图(ARGB32,含透明无数据区)
|
||||||
QRectF dataBBox_; // 数据包围盒(x[xmin,xmax] y[ymin,ymax])
|
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_; // 异常叠加
|
std::vector<core::Anomaly> anoms_; // 异常叠加
|
||||||
|
|
||||||
bool showLines_ = true;
|
bool showLines_ = true;
|
||||||
|
|
@ -60,6 +74,7 @@ private:
|
||||||
core::Rgba lineColor_{0, 0, 0, 255}; // 等值线色(默认黑)
|
core::Rgba lineColor_{0, 0, 0, 255}; // 等值线色(默认黑)
|
||||||
bool lineDashed_ = false; // 等值线虚实(默认实线)
|
bool lineDashed_ = false; // 等值线虚实(默认实线)
|
||||||
core::Rgba labelColor_{0, 0, 0, 255}; // 标注色(默认黑)
|
core::Rgba labelColor_{0, 0, 0, 255}; // 标注色(默认黑)
|
||||||
|
int highlightIdx_ = -1; // I12 当前高亮异常下标(-1=无)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -1,10 +1,17 @@
|
||||||
#include "panels/chart/DataTableView.hpp"
|
#include "panels/chart/DataTableView.hpp"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <QCursor>
|
||||||
|
#include <QHBoxLayout>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
#include <QPushButton>
|
||||||
#include <QTableView>
|
#include <QTableView>
|
||||||
|
#include <QToolTip>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "panels/chart/InversionFormDialog.hpp"
|
||||||
#include "panels/chart/TablePager.hpp"
|
#include "panels/chart/TablePager.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
@ -111,6 +118,14 @@ DataTableView::DataTableView(QWidget* parent) : QWidget(parent) {
|
||||||
lay->setContentsMargins(0, 0, 0, 0);
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
lay->setSpacing(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);
|
model_ = new TablePayloadModel(this);
|
||||||
table_ = new QTableView(this);
|
table_ = new QTableView(this);
|
||||||
table_->setModel(model_);
|
table_->setModel(model_);
|
||||||
|
|
@ -162,6 +177,54 @@ void DataTableView::setPayload(const QVariant& payload) {
|
||||||
} else {
|
} else {
|
||||||
pager_->hide();
|
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
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
#include <QAbstractTableModel>
|
#include <QAbstractTableModel>
|
||||||
#include <QStyledItemDelegate>
|
#include <QStyledItemDelegate>
|
||||||
|
#include <QString>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include "model/detail/DetailPayloads.hpp"
|
#include "model/detail/DetailPayloads.hpp"
|
||||||
#include "panels/chart/IDetailView.hpp"
|
#include "panels/chart/IDetailView.hpp"
|
||||||
|
|
||||||
class QTableView;
|
class QTableView;
|
||||||
|
class QWidget;
|
||||||
|
class QHBoxLayout;
|
||||||
|
|
||||||
|
namespace geopro::data {
|
||||||
|
class IDatasetCommandRepository;
|
||||||
|
}
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
|
|
@ -48,6 +57,9 @@ class TablePager;
|
||||||
// 通用数据列表视图:IDetailView + QTableView(+ 分页型载荷时底部 TablePager 分页器)。
|
// 通用数据列表视图:IDetailView + QTableView(+ 分页型载荷时底部 TablePager 分页器)。
|
||||||
// measurement/grid/trajectory 列表共用。载荷 pageSize>0(dd_grid)时显示分页器并转发翻页请求;
|
// measurement/grid/trajectory 列表共用。载荷 pageSize>0(dd_grid)时显示分页器并转发翻页请求;
|
||||||
// 否则隐藏分页器(全量列表)。
|
// 否则隐藏分页器(全量列表)。
|
||||||
|
// 顶部功能按钮行:仅当载荷 functionButtons 非空(dd_grid)时显示,按钮文案/可见来自服务端
|
||||||
|
// functionList;点 code=="inversion" → 弹反演动态表单对话框(复用 InversionFormDialog/Mode::Inversion)。
|
||||||
|
// 其余 Table 复用场景(measurement 列表/trajectory/gr)functionButtons 为空 → 工具条隐藏,无任何变化。
|
||||||
class DataTableView : public QWidget, public IDetailView {
|
class DataTableView : public QWidget, public IDetailView {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
|
|
@ -56,14 +68,29 @@ public:
|
||||||
QWidget* widget() override { return this; }
|
QWidget* widget() override { return this; }
|
||||||
void setPayload(const QVariant& payload) override; // 坏/空 variant → 保持空态不崩
|
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:
|
signals:
|
||||||
// 分页器请求加载某页(翻页/跳页/改每页条数)。壳据此触发控制器 loadTabPaged。
|
// 分页器请求加载某页(翻页/跳页/改每页条数)。壳据此触发控制器 loadTabPaged。
|
||||||
void pageRequested(int pageNo, int pageSize);
|
void pageRequested(int pageNo, int pageSize);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void rebuildToolbar(const std::vector<geopro::core::TableFunctionButton>& buttons);
|
||||||
|
void onFunctionButton(const QString& code); // 功能按钮路由(仅 inversion 起效)
|
||||||
|
|
||||||
|
QWidget* toolbar_; // 顶部功能按钮行容器(functionButtons 空时隐藏)
|
||||||
|
QHBoxLayout* toolbarLay_; // 功能按钮布局(重建时清空重填)
|
||||||
QTableView* table_;
|
QTableView* table_;
|
||||||
TablePayloadModel* model_;
|
TablePayloadModel* model_;
|
||||||
TablePager* pager_; // 分页器(pageSize>0 时显示,否则隐藏)
|
TablePager* pager_; // 分页器(pageSize>0 时显示,否则隐藏)
|
||||||
|
|
||||||
|
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
|
||||||
|
std::function<QString()> dsIdGetter_;
|
||||||
|
std::function<QString()> projectIdGetter_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -14,18 +14,32 @@ namespace geopro::app {
|
||||||
|
|
||||||
std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget* parent,
|
std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget* parent,
|
||||||
geopro::data::IColorTemplateRepository* colorTplRepo,
|
geopro::data::IColorTemplateRepository* colorTplRepo,
|
||||||
std::function<QString()> projectIdGetter) {
|
std::function<QString()> projectIdGetter,
|
||||||
|
geopro::data::IDatasetCommandRepository* cmdRepo,
|
||||||
|
std::function<QString()> dsIdGetter) {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case controller::ViewKind::Scatter:
|
case controller::ViewKind::Scatter: {
|
||||||
return std::unique_ptr<IDetailView>(new RawDataChartView(parent));
|
auto* raw = new RawDataChartView(parent);
|
||||||
|
// 注入反演命令仓储 + dsId/projectId 取值回调(measurement 反演运算/生成视电阻率)。
|
||||||
|
raw->setCommandRepo(cmdRepo, dsIdGetter, projectIdGetter);
|
||||||
|
return std::unique_ptr<IDetailView>(raw);
|
||||||
|
}
|
||||||
case controller::ViewKind::FilledContour: {
|
case controller::ViewKind::FilledContour: {
|
||||||
auto* grid = new GridDataChartView(parent);
|
auto* grid = new GridDataChartView(parent);
|
||||||
// 注入色阶模板仓储 + projectId 取值回调(网格剖面「色阶配置」编辑器用)。
|
// 注入色阶模板仓储 + 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);
|
return std::unique_ptr<IDetailView>(grid);
|
||||||
}
|
}
|
||||||
case controller::ViewKind::Table:
|
case controller::ViewKind::Table: {
|
||||||
return std::unique_ptr<IDetailView>(new DataTableView(parent));
|
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:
|
case controller::ViewKind::Bar:
|
||||||
return std::unique_ptr<IDetailView>(new BarChartView(parent));
|
return std::unique_ptr<IDetailView>(new BarChartView(parent));
|
||||||
case controller::ViewKind::LineProfile:
|
case controller::ViewKind::LineProfile:
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ class QWidget;
|
||||||
|
|
||||||
namespace geopro::data {
|
namespace geopro::data {
|
||||||
class IColorTemplateRepository;
|
class IColorTemplateRepository;
|
||||||
|
class IDatasetCommandRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
@ -20,9 +21,12 @@ class IDetailView;
|
||||||
// Bar/LineProfile/PolylineMap/Table 留待后续阶段(measurement/trajectory/grid)补,
|
// Bar/LineProfile/PolylineMap/Table 留待后续阶段(measurement/trajectory/grid)补,
|
||||||
// 现阶段命中会抛 std::runtime_error(明确失败,而非静默空指针)。
|
// 现阶段命中会抛 std::runtime_error(明确失败,而非静默空指针)。
|
||||||
// colorTplRepo/projectIdGetter:FilledContour 视图的色阶模板仓储注入(可空 → 编辑器后端按钮禁用)。
|
// colorTplRepo/projectIdGetter:FilledContour 视图的色阶模板仓储注入(可空 → 编辑器后端按钮禁用)。
|
||||||
|
// cmdRepo/dsIdGetter:Scatter 视图(measurement)反演运算/生成视电阻率命令仓储注入(可空 → 按钮占位提示)。
|
||||||
std::unique_ptr<IDetailView> makeDetailView(
|
std::unique_ptr<IDetailView> makeDetailView(
|
||||||
controller::ViewKind kind, QWidget* parent,
|
controller::ViewKind kind, QWidget* parent,
|
||||||
geopro::data::IColorTemplateRepository* colorTplRepo = nullptr,
|
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
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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("坐标(x,y):"), 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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 {
|
||||||
|
// 找名为「自定义滤波器」的分组节点 id(newFilter 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
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
// 滤波处理对话框(I4,1: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; // 「自定义滤波器」分组节点 id(newFilter 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
|
||||||
|
|
@ -3,11 +3,18 @@
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
|
#include <QCursor>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QPointer>
|
||||||
#include <QSignalBlocker>
|
#include <QSignalBlocker>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QSlider>
|
#include <QSlider>
|
||||||
|
#include <QTimer>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
|
#include <QToolTip>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include <qwt_plot.h>
|
#include <qwt_plot.h>
|
||||||
|
|
@ -23,10 +30,19 @@
|
||||||
#include "panels/AnomalyTablePanel.hpp"
|
#include "panels/AnomalyTablePanel.hpp"
|
||||||
#include "panels/DescriptionPanel.hpp"
|
#include "panels/DescriptionPanel.hpp"
|
||||||
#include "panels/chart/ChartTheme.hpp"
|
#include "panels/chart/ChartTheme.hpp"
|
||||||
|
#include "panels/chart/AutoAnnotationDialog.hpp"
|
||||||
#include "panels/chart/ColorBarWidget.hpp"
|
#include "panels/chart/ColorBarWidget.hpp"
|
||||||
#include "panels/chart/ColorMapService.hpp"
|
#include "panels/chart/ColorMapService.hpp"
|
||||||
|
#include "panels/chart/ContourHoverTip.hpp"
|
||||||
#include "panels/chart/ContourPlotItem.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/LivePanner.hpp"
|
||||||
|
#include "panels/chart/SaveAsDialog.hpp"
|
||||||
|
#include "panels/chart/WhiteningDialog.hpp"
|
||||||
|
#include "repo/IDatasetCommandRepository.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
|
|
@ -65,16 +81,23 @@ GridDataChartView::GridDataChartView(QWidget* parent) : QWidget(parent) {
|
||||||
|
|
||||||
auto* lblSimplify = new QLabel(QStringLiteral("简化容差:"), toolbar);
|
auto* lblSimplify = new QLabel(QStringLiteral("简化容差:"), toolbar);
|
||||||
|
|
||||||
|
// 简化容差:对照原版 a-slider min0 max2 step0.1,默认 0.5。客户端用整数滑块 0~20 映射 /10。
|
||||||
simplifySlider_ = new QSlider(Qt::Horizontal, toolbar);
|
simplifySlider_ = new QSlider(Qt::Horizontal, toolbar);
|
||||||
simplifySlider_->setRange(0, 100);
|
simplifySlider_->setRange(0, 20);
|
||||||
simplifySlider_->setValue(50);
|
simplifySlider_->setValue(5); // 0.5
|
||||||
simplifySlider_->setFixedWidth(80);
|
simplifySlider_->setFixedWidth(80);
|
||||||
|
|
||||||
simplifyValueLabel_ = new QLabel(QStringLiteral("0.5"), toolbar);
|
simplifyValueLabel_ = new QLabel(QStringLiteral("0.5"), toolbar);
|
||||||
simplifyValueLabel_->setFixedWidth(28);
|
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) {
|
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);
|
auto* btnAnomalyLabel = new QToolButton(toolbar);
|
||||||
|
|
@ -174,6 +197,33 @@ GridDataChartView::GridDataChartView(QWidget* parent) : QWidget(parent) {
|
||||||
connect(btnColorScale, &QToolButton::clicked, this,
|
connect(btnColorScale, &QToolButton::clicked, this,
|
||||||
[this]() { openColorScaleEditor(); });
|
[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_);
|
applyChartPlotTheme(plot_);
|
||||||
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, plot_,
|
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, plot_,
|
||||||
|
|
@ -230,6 +280,9 @@ void GridDataChartView::rebuildContour() {
|
||||||
contourItem_->setLineColor(lineCfg_.lineColor); // 线形⚙ 配置
|
contourItem_->setLineColor(lineCfg_.lineColor); // 线形⚙ 配置
|
||||||
contourItem_->setLineDashed(lineCfg_.dashed);
|
contourItem_->setLineDashed(lineCfg_.dashed);
|
||||||
contourItem_->setLabelColor(lineCfg_.labelColor);
|
contourItem_->setLabelColor(lineCfg_.labelColor);
|
||||||
|
// I8 应用当前简化容差;I7 把等值线提示绑定到新建项。
|
||||||
|
if (simplifySlider_) contourItem_->setSimplifyTolerance(simplifySlider_->value() / 10.0);
|
||||||
|
if (contourTip_) contourTip_->setItem(contourItem_);
|
||||||
contourItem_->attach(plot_);
|
contourItem_->attach(plot_);
|
||||||
|
|
||||||
// 轴范围 = 数据范围(x=距离、y=深度/高程)。
|
// 轴范围 = 数据范围(x=距离、y=深度/高程)。
|
||||||
|
|
@ -274,4 +327,186 @@ void GridDataChartView::openColorScaleEditor() {
|
||||||
colorBar_->setColorScale(gridScale_);
|
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 Delta;Qt 退化为纯文本:
|
||||||
|
// 优先 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
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@
|
||||||
class QSlider;
|
class QSlider;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QCheckBox;
|
class QCheckBox;
|
||||||
|
class QTimer;
|
||||||
class QwtPlot;
|
class QwtPlot;
|
||||||
class QwtPlotRescaler;
|
class QwtPlotRescaler;
|
||||||
|
|
||||||
namespace geopro::data {
|
namespace geopro::data {
|
||||||
class IColorTemplateRepository;
|
class IColorTemplateRepository;
|
||||||
|
class IDatasetCommandRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
@ -29,6 +31,7 @@ class DescriptionPanel;
|
||||||
class ColorBarWidget;
|
class ColorBarWidget;
|
||||||
class ColorMapService;
|
class ColorMapService;
|
||||||
class ContourPlotItem;
|
class ContourPlotItem;
|
||||||
|
class ContourHoverTip;
|
||||||
|
|
||||||
// 网格数据图表视图:工具条 + QwtPlot(白底 + 真实比尺 + 实时平移/滚轮缩放,x 轴在底部)
|
// 网格数据图表视图:工具条 + QwtPlot(白底 + 真实比尺 + 实时平移/滚轮缩放,x 轴在底部)
|
||||||
// + 独立色阶条 + 底部双页签(异常列表/描述)。
|
// + 独立色阶条 + 底部双页签(异常列表/描述)。
|
||||||
|
|
@ -52,9 +55,30 @@ public:
|
||||||
void setColorTemplateRepo(geopro::data::IColorTemplateRepository* repo,
|
void setColorTemplateRepo(geopro::data::IColorTemplateRepository* repo,
|
||||||
std::function<QString()> projectIdGetter);
|
std::function<QString()> projectIdGetter);
|
||||||
|
|
||||||
|
// 注入反演命令仓储 + dsId/projectId 取值回调(网格化/白化/滤波/另存为按钮)。
|
||||||
|
// 可传空仓储 → 这些按钮退化为「暂未实现」占位提示。
|
||||||
|
void setCommandRepo(geopro::data::IDatasetCommandRepository* repo,
|
||||||
|
std::function<QString()> dsIdGetter,
|
||||||
|
std::function<QString()> projectIdGetter);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void rebuildContour(); // 按当前显隐开关重建并重绘 ContourPlotItem
|
void rebuildContour(); // 按当前显隐开关重建并重绘 ContourPlotItem
|
||||||
void openColorScaleEditor(); // 「色阶配置」→ 共享色阶编辑器(色阶 + 层级⚙ + 线形⚙)
|
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;
|
QwtPlot* plot_ = nullptr;
|
||||||
QwtPlotRescaler* rescaler_ = nullptr;
|
QwtPlotRescaler* rescaler_ = nullptr;
|
||||||
|
|
@ -64,6 +88,8 @@ private:
|
||||||
QSlider* simplifySlider_ = nullptr;
|
QSlider* simplifySlider_ = nullptr;
|
||||||
QLabel* simplifyValueLabel_ = nullptr;
|
QLabel* simplifyValueLabel_ = nullptr;
|
||||||
QCheckBox* chkShowLabels_ = nullptr; // 工具条「显示等值线标注」(线形⚙ 改标注显隐后同步)
|
QCheckBox* chkShowLabels_ = nullptr; // 工具条「显示等值线标注」(线形⚙ 改标注显隐后同步)
|
||||||
|
QTimer* simplifyDebounce_ = nullptr; // I8 简化容差防抖(~300ms)
|
||||||
|
ContourHoverTip* contourTip_ = nullptr; // I7 等值线提示(hover)
|
||||||
|
|
||||||
// 渲染状态
|
// 渲染状态
|
||||||
ColorMapService* colorSvc_ = nullptr; // heap,setGridData 重建
|
ColorMapService* colorSvc_ = nullptr; // heap,setGridData 重建
|
||||||
|
|
@ -81,6 +107,10 @@ private:
|
||||||
// 色阶模板仓储 + projectId 取值回调(注入;空则编辑器后端按钮禁用)。
|
// 色阶模板仓储 + projectId 取值回调(注入;空则编辑器后端按钮禁用)。
|
||||||
geopro::data::IColorTemplateRepository* tplRepo_ = nullptr;
|
geopro::data::IColorTemplateRepository* tplRepo_ = nullptr;
|
||||||
std::function<QString()> projectIdGetter_;
|
std::function<QString()> projectIdGetter_;
|
||||||
|
|
||||||
|
// 反演命令仓储 + dsId 取值回调(注入;空则处理类按钮占位提示)。
|
||||||
|
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
|
||||||
|
std::function<QString()> dsIdGetter_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
// 配一个坐标 spinbox(6 位小数,宽范围)。
|
||||||
|
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)/xSize,y 间距同 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
|
||||||
|
|
@ -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):
|
||||||
|
// 步骤 1:listGridAlgorithm 选算法(单选列表,scriptName 显示 / scriptCode 提交)。
|
||||||
|
// 步骤 2:getGridRawDataParams 取 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
|
||||||
|
|
@ -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.cpp(Qt-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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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; // optionsObject(Select 选项)
|
||||||
|
};
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
#include "panels/chart/InversionProcessOps.hpp"
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
QJsonObject buildGridToBody(const GridToParams& p) {
|
||||||
|
// 字段名/casing 严格对照原版 toGridTheData:xsize/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},
|
||||||
|
// saveDataValueType:1=线性 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
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 反演网格视图「处理类」操作的纯逻辑(仅依赖 QtCore JSON,无 Widgets/MOC)。
|
||||||
|
// 拆出独立 TU 以便单测(与 ScatterDataOps / InversionFormParse 同范式)。
|
||||||
|
// 字段名/取值一律对照原版 web(GridDialog/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:边界扩展 + 内/外白化(whiteningType:0 外部 / 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)────────────────────
|
||||||
|
// 数据边缘处理方式 code(whitening→1 / skip→2 / edgePoint→3 / filling→4)。
|
||||||
|
int filterBoundaryCode(const QString& dataEdge);
|
||||||
|
// 无数据点处理方式 code(expansion→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.form(QJsonArray 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
|
||||||
|
|
@ -1,23 +1,45 @@
|
||||||
#include "panels/chart/RawDataChartView.hpp"
|
#include "panels/chart/RawDataChartView.hpp"
|
||||||
|
#include "ColorScaleConfigDialog.hpp"
|
||||||
#include "panels/chart/ChartTheme.hpp"
|
#include "panels/chart/ChartTheme.hpp"
|
||||||
#include "panels/chart/ColorBarWidget.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/ScatterHoverTip.hpp"
|
||||||
#include "panels/chart/ScatterPlotItem.hpp"
|
#include "panels/chart/ScatterPlotItem.hpp"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "repo/IDatasetCommandRepository.hpp"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QFileDialog>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QPen>
|
#include <QPen>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
#include <QPointer>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QSaveFile>
|
||||||
|
#include <QSignalBlocker>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
#include <QToolTip>
|
#include <QToolTip>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include <qwt_scale_map.h>
|
||||||
|
|
||||||
#include <qwt_plot.h>
|
#include <qwt_plot.h>
|
||||||
#include <qwt_plot_canvas.h>
|
#include <qwt_plot_canvas.h>
|
||||||
#include <qwt_plot_marker.h>
|
#include <qwt_plot_marker.h>
|
||||||
|
|
@ -67,6 +89,13 @@ RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) {
|
||||||
tbLay->addWidget(btnSaveAs);
|
tbLay->addWidget(btnSaveAs);
|
||||||
tbLay->addStretch();
|
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);
|
lay->addWidget(toolbar);
|
||||||
|
|
||||||
// ---- QwtPlot(stretch 填满剩余空间)----
|
// ---- QwtPlot(stretch 填满剩余空间)----
|
||||||
|
|
@ -257,6 +286,41 @@ void styleToolIconButton(QToolButton* btn, const QIcon& icon) {
|
||||||
btn->setCursor(Qt::PointingHandCursor);
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组装色阶 properties(colorBar + 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
|
} // namespace
|
||||||
|
|
||||||
void RawDataChartView::showNotImplemented(QWidget* anchor) {
|
void RawDataChartView::showNotImplemented(QWidget* anchor) {
|
||||||
|
|
@ -266,6 +330,333 @@ void RawDataChartView::showNotImplemented(QWidget* anchor) {
|
||||||
QToolTip::showText(pos, QStringLiteral("暂未实现"), 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:反演原数据散点色阶(type1,businessCode 空串,对照原版 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/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) 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(隐藏取可见点 / 显示取隐藏点),status:0=显示 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);
|
||||||
|
|
||||||
|
// 持久化到后端(saveColorGradation,businessCode=当前 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;
|
||||||
|
};
|
||||||
|
// 复刻原版 scatterInfos:A / 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() {
|
void RawDataChartView::replotForAxis() {
|
||||||
// 本地换 x/y(无网络):按下拉 fieldCode 从备选列取数据,重设 scatter.x/.y 并重绘。
|
// 本地换 x/y(无网络):按下拉 fieldCode 从备选列取数据,重设 scatter.x/.y 并重绘。
|
||||||
if (!xCombo_ || !yCombo_ || !scatterItem_) return;
|
if (!xCombo_ || !yCombo_ || !scatterItem_) return;
|
||||||
|
|
@ -305,73 +696,91 @@ void RawDataChartView::buildMeasurementToolbar(const geopro::core::ScatterToolba
|
||||||
auto* btnInfo = new QToolButton(toolbar);
|
auto* btnInfo = new QToolButton(toolbar);
|
||||||
btnInfo->setToolTip(QStringLiteral("信息"));
|
btnInfo->setToolTip(QStringLiteral("信息"));
|
||||||
styleToolIconButton(btnInfo, makeInfoIcon());
|
styleToolIconButton(btnInfo, makeInfoIcon());
|
||||||
connect(btnInfo, &QToolButton::clicked, this, [this, btnInfo]() { showNotImplemented(btnInfo); });
|
|
||||||
auto* btnMarquee = new QToolButton(toolbar);
|
auto* btnMarquee = new QToolButton(toolbar);
|
||||||
btnMarquee->setToolTip(QStringLiteral("框选"));
|
btnMarquee->setToolTip(QStringLiteral("框选"));
|
||||||
styleToolIconButton(btnMarquee, makeMarqueeIcon());
|
styleToolIconButton(btnMarquee, makeMarqueeIcon());
|
||||||
connect(btnMarquee, &QToolButton::clicked, this, [this, btnMarquee]() { showNotImplemented(btnMarquee); });
|
|
||||||
// 主题热切:重绘图标(info 锚定品牌蓝,marquee 描边随次要文本色)。
|
// 主题热切:重绘图标(info 锚定品牌蓝,marquee 描边随次要文本色)。
|
||||||
connect(&ThemeManager::instance(), &ThemeManager::changed, btnInfo,
|
connect(&ThemeManager::instance(), &ThemeManager::changed, btnInfo,
|
||||||
[btnInfo]() { btnInfo->setIcon(makeInfoIcon()); });
|
[btnInfo]() { btnInfo->setIcon(makeInfoIcon()); });
|
||||||
connect(&ThemeManager::instance(), &ThemeManager::changed, btnMarquee,
|
connect(&ThemeManager::instance(), &ThemeManager::changed, btnMarquee,
|
||||||
[btnMarquee]() { btnMarquee->setIcon(makeMarqueeIcon()); });
|
[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* btnShow = new QPushButton(QStringLiteral("显示"), toolbar);
|
||||||
auto* btnHide = new QPushButton(QStringLiteral("隐藏"), toolbar);
|
auto* btnHide = new QPushButton(QStringLiteral("隐藏"), toolbar);
|
||||||
connect(btnShow, &QPushButton::clicked, this, [this]() {
|
connect(btnShow, &QPushButton::clicked, this, [this]() { onShowHide(/*hide*/ false); });
|
||||||
if (scatterItem_) { scatterItem_->setScatterVisible(true); plot_->replot(); }
|
connect(btnHide, &QPushButton::clicked, this, [this]() { onShowHide(/*hide*/ true); });
|
||||||
});
|
|
||||||
connect(btnHide, &QPushButton::clicked, this, [this]() {
|
|
||||||
if (scatterItem_) { scatterItem_->setScatterVisible(false); plot_->replot(); }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 数据过滤:占位(暂未实现)。
|
// 数据过滤:范围过滤弹窗(M3)。
|
||||||
auto* btnFilter = new QPushButton(QStringLiteral("数据过滤"), toolbar);
|
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);
|
xCombo_ = new QComboBox(toolbar);
|
||||||
fillCombo(xCombo_, conf.x, conf.defaultX, QString());
|
fillCombo(xCombo_, conf.x, conf.defaultX, QString());
|
||||||
yCombo_ = new QComboBox(toolbar);
|
yCombo_ = new QComboBox(toolbar);
|
||||||
fillCombo(yCombo_, conf.y, conf.defaultY, QString());
|
fillCombo(yCombo_, conf.y, conf.defaultY, QString());
|
||||||
auto* vCombo = new QComboBox(toolbar);
|
vCombo_ = new QComboBox(toolbar);
|
||||||
fillCombo(vCombo, conf.v, conf.defaultV, QString());
|
fillCombo(vCombo_, conf.v, conf.defaultV, QString());
|
||||||
auto* methodCombo = new QComboBox(toolbar);
|
valueTypeCombo_ = new QComboBox(toolbar);
|
||||||
fillCombo(methodCombo, conf.method, QString(), conf.defaultMethod);
|
// 值类型固定三项(原版 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,
|
connect(xCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||||
[this](int) { replotForAxis(); });
|
[this](int) { replotForAxis(); });
|
||||||
connect(yCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
connect(yCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||||
[this](int) { replotForAxis(); });
|
[this](int) { replotForAxis(); });
|
||||||
// v / method:换选项 → 暂未实现(需重新请求散点/色阶,属重交互,本轮不做)。
|
// V 值切换:重新请求散点+色阶(用 activated 仅用户操作触发,填充期不误触)。
|
||||||
connect(vCombo, QOverload<int>::of(&QComboBox::activated), this,
|
connect(vCombo_, QOverload<int>::of(&QComboBox::activated), this,
|
||||||
[this, vCombo](int) { showNotImplemented(vCombo); });
|
[this](int) { reloadForVValue(); });
|
||||||
connect(methodCombo, QOverload<int>::of(&QComboBox::activated), this,
|
connect(valueTypeCombo_, QOverload<int>::of(&QComboBox::activated), this,
|
||||||
[this, methodCombo](int) { showNotImplemented(methodCombo); });
|
[this](int) { applyValueType(); });
|
||||||
|
|
||||||
// 色阶配置:占位(暂未实现)。
|
// 色阶配置:复用 ColorScaleConfigDialog(散点上色路径),保存 + 本地重绘(M8)。
|
||||||
auto* btnColorScale = new QPushButton(QStringLiteral("色阶配置"), toolbar);
|
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* btnGen = new QPushButton(QStringLiteral("生成视电阻率数据"), toolbar);
|
||||||
auto* btnInvert = new QPushButton(QStringLiteral("反演运算"), toolbar);
|
auto* btnInvert = new QPushButton(QStringLiteral("反演运算"), toolbar);
|
||||||
auto* btnSaveAs = new QPushButton(QStringLiteral("另存为"), toolbar);
|
auto* btnSaveAs = new QPushButton(QStringLiteral("另存为"), toolbar);
|
||||||
for (auto* b : {btnGen, btnInvert, btnSaveAs}) {
|
for (auto* b : {btnGen, btnInvert, btnSaveAs}) {
|
||||||
b->setObjectName(QStringLiteral("primaryBtn")); // 蓝色主按钮(下方 QSS)
|
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); });
|
||||||
|
// 另存为:新增/覆盖弹窗 → saveRawData(M11)。
|
||||||
|
connect(btnSaveAs, &QPushButton::clicked, this,
|
||||||
|
[this, btnSaveAs]() { openSaveAs(btnSaveAs); });
|
||||||
|
|
||||||
tbLay->addWidget(btnInfo);
|
tbLay->addWidget(btnInfo);
|
||||||
tbLay->addWidget(btnMarquee);
|
tbLay->addWidget(btnMarquee);
|
||||||
tbLay->addWidget(btnShow);
|
tbLay->addWidget(btnShow);
|
||||||
tbLay->addWidget(btnHide);
|
tbLay->addWidget(btnHide);
|
||||||
tbLay->addWidget(btnFilter);
|
tbLay->addWidget(btnFilter);
|
||||||
|
tbLay->addWidget(btnExport);
|
||||||
tbLay->addWidget(xCombo_);
|
tbLay->addWidget(xCombo_);
|
||||||
tbLay->addWidget(yCombo_);
|
tbLay->addWidget(yCombo_);
|
||||||
tbLay->addWidget(vCombo);
|
tbLay->addWidget(vCombo_);
|
||||||
tbLay->addWidget(methodCombo);
|
tbLay->addWidget(valueTypeCombo_);
|
||||||
tbLay->addWidget(btnColorScale);
|
tbLay->addWidget(btnColorScale);
|
||||||
tbLay->addStretch(); // 把主操作推到右侧
|
tbLay->addStretch(); // 把主操作推到右侧
|
||||||
tbLay->addWidget(btnGen);
|
tbLay->addWidget(btnGen);
|
||||||
|
|
@ -405,6 +814,7 @@ void RawDataChartView::setPayload(const QVariant& payload) {
|
||||||
|
|
||||||
void RawDataChartView::setData(const geopro::core::ScatterPayload& p) {
|
void RawDataChartView::setData(const geopro::core::ScatterPayload& p) {
|
||||||
data_ = p;
|
data_ = p;
|
||||||
|
baseV_ = data_.scatter.v; // 缓存原始 v(线性),M7 值类型变换从原值算,不累积误差
|
||||||
if (hoverTip_) hoverTip_->setField(&data_.scatter); // 显式重绑(地址稳定,消除隐式依赖)
|
if (hoverTip_) hoverTip_->setField(&data_.scatter); // 显式重绑(地址稳定,消除隐式依赖)
|
||||||
|
|
||||||
// measurement 载荷(toolbar 非空):首次到来时建并替换工具条(视觉 1:1)。反演留空 → 不动。
|
// measurement 载荷(toolbar 非空):首次到来时建并替换工具条(视觉 1:1)。反演留空 → 不动。
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include "model/detail/DetailPayloads.hpp"
|
#include "model/detail/DetailPayloads.hpp"
|
||||||
#include "panels/chart/ColorMapService.hpp"
|
#include "panels/chart/ColorMapService.hpp"
|
||||||
|
|
@ -6,9 +9,14 @@
|
||||||
|
|
||||||
class QComboBox;
|
class QComboBox;
|
||||||
class QVBoxLayout;
|
class QVBoxLayout;
|
||||||
|
class QLabel;
|
||||||
class QwtPlot;
|
class QwtPlot;
|
||||||
class QwtPlotRescaler;
|
class QwtPlotRescaler;
|
||||||
|
|
||||||
|
namespace geopro::data {
|
||||||
|
class IDatasetCommandRepository;
|
||||||
|
}
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
class ColorBarWidget;
|
class ColorBarWidget;
|
||||||
|
|
@ -32,6 +40,16 @@ public:
|
||||||
// 供外部访问(已不再是占位,保留兼容接口返回 plot_)
|
// 供外部访问(已不再是占位,保留兼容接口返回 plot_)
|
||||||
QWidget* plotArea() const;
|
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:
|
private:
|
||||||
// 工具条按载荷类型二选一:反演原数据 = ctor 默认建的 inversion 工具条;measurement =
|
// 工具条按载荷类型二选一:反演原数据 = ctor 默认建的 inversion 工具条;measurement =
|
||||||
// 首个非空 ScatterToolbarConf 到来时建一次并替换(视觉 1:1)。建好后缓存,后续 setData 复用。
|
// 首个非空 ScatterToolbarConf 到来时建一次并替换(视觉 1:1)。建好后缓存,后续 setData 复用。
|
||||||
|
|
@ -40,6 +58,27 @@ private:
|
||||||
void replotForAxis();
|
void replotForAxis();
|
||||||
// “暂未实现”轻提示(占位按钮/下拉点击)。
|
// “暂未实现”轻提示(占位按钮/下拉点击)。
|
||||||
void showNotImplemented(QWidget* anchor);
|
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 原数据散点色阶(type1,businessCode='')
|
||||||
|
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_;
|
geopro::core::ScatterPayload data_;
|
||||||
QwtPlot* plot_;
|
QwtPlot* plot_;
|
||||||
|
|
@ -53,11 +92,25 @@ private:
|
||||||
bool measurementToolbar_ = false; // 已建 measurement 工具条
|
bool measurementToolbar_ = false; // 已建 measurement 工具条
|
||||||
QComboBox* xCombo_ = nullptr; // measurement x 下拉
|
QComboBox* xCombo_ = nullptr; // measurement x 下拉
|
||||||
QComboBox* yCombo_ = nullptr; // measurement y 下拉
|
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 接管绘制,但我们仍持有指针
|
// 使用 unique_ptr 管理生命周期;attach 后 QwtPlot 接管绘制,但我们仍持有指针
|
||||||
ColorMapService* colorSvc_ = nullptr; // heap,由 setData 重建
|
ColorMapService* colorSvc_ = nullptr; // heap,由 setData 重建
|
||||||
ScatterPlotItem* scatterItem_ = nullptr;
|
ScatterPlotItem* scatterItem_ = nullptr;
|
||||||
ScatterHoverTip* hoverTip_ = nullptr; // 散点 hover 提示(QObject,this 持有)
|
ScatterHoverTip* hoverTip_ = nullptr; // 散点 hover 提示(QObject,this 持有)
|
||||||
|
|
||||||
|
// 反演命令仓储 + dsId/projectId 取值回调(注入;空则反演按钮占位)。
|
||||||
|
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
|
||||||
|
std::function<QString()> dsIdGetter_;
|
||||||
|
std::function<QString()> projectIdGetter_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -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-group:1=新增 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
|
||||||
|
|
@ -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,两形态可复用):
|
||||||
|
// - RawData(measurement「另存为」,原版 saveRawDataValue):新增/覆盖单选 +(新增时)名称框。
|
||||||
|
// 提交体 {dsId, operationType(1新增/0覆盖), name?}(覆盖不带 name)。
|
||||||
|
// - Inversion(inversion「另存为」,原版 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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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/v(v==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);
|
||||||
|
|
||||||
|
// 收集「显示/隐藏」要持久化的点 id(M1)。
|
||||||
|
// hide=true → 收集当前可见(displayStatus==0)的点 id(原版隐藏全部已选/可见点)。
|
||||||
|
// hide=false → 收集当前隐藏(displayStatus!=0)的点 id(原版显示全部已隐藏点)。
|
||||||
|
// id 为空串的点跳过(无效)。
|
||||||
|
QJsonArray collectScatterIds(const geopro::core::ScatterField& field, bool hide);
|
||||||
|
|
||||||
|
// 组装散点过滤请求体(M3,applyScatterFilter):
|
||||||
|
// {sourceDsObjectId, sourceVFieldCode, min, max}(字段名对照原版 applyScatterFilterInfo)。
|
||||||
|
QJsonObject buildScatterFilterBody(const QString& dsObjectId, const QString& vFieldCode,
|
||||||
|
double min, double max);
|
||||||
|
|
||||||
|
// 组装 measurement「另存为」请求体(M11,saveRawData,对照原版 saveRawDataValue):
|
||||||
|
// {dsId, operationType(1新增/0覆盖)},仅新增(operationType==1)才带 name。
|
||||||
|
QJsonObject buildSaveRawDataBody(const QString& dsId, int operationType, const QString& name);
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
// 白化对话框(I3,1: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
|
||||||
|
|
@ -16,6 +16,7 @@ struct Anomaly {
|
||||||
std::string typeName; // exceptionTypeName
|
std::string typeName; // exceptionTypeName
|
||||||
std::string exceptionTypeId; // 异常类型 id(保存请求 exceptionTypeId)
|
std::string exceptionTypeId; // 异常类型 id(保存请求 exceptionTypeId)
|
||||||
std::string remark; // 备注
|
std::string remark; // 备注
|
||||||
|
std::string createTime; // 创建时间(异常列表展示用,只读)
|
||||||
AnomalyMarkType markType = AnomalyMarkType::Polyline;
|
AnomalyMarkType markType = AnomalyMarkType::Polyline;
|
||||||
std::vector<Vec2> localPts; // 2D 局部坐标(剖面详情:x=距离, y=深度)
|
std::vector<Vec2> localPts; // 2D 局部坐标(剖面详情:x=距离, y=深度)
|
||||||
// VTK 三维:异常多边形/折线/点的世界 3D 坐标(落在所在切片平面上)+ 平面(法向/一点),
|
// VTK 三维:异常多边形/折线/点的世界 3D 坐标(落在所在切片平面上)+ 平面(法向/一点),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
namespace geopro::core {
|
namespace geopro::core {
|
||||||
|
|
||||||
// 规则三维标量场(IInterpolator 输出;render 层转 vtkImageData)。
|
// 规则三维标量场(IInterpolator 输出;render 层转 vtkImageData)。
|
||||||
|
|
@ -55,6 +56,14 @@ struct ScatterField {
|
||||||
std::vector<double> a, b, m, n;
|
std::vector<double> a, b, m, n;
|
||||||
std::vector<double> electrodeX;
|
std::vector<double> electrodeX;
|
||||||
std::vector<double> electrodeNo;
|
std::vector<double> electrodeNo;
|
||||||
|
// measurement 散点 [i]信息 / 显隐 / 持久化用(反演留空)。与 x/y/v 同序、一一对应:
|
||||||
|
// id = 点 id(saveDisplayStatus 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
|
} // namespace geopro::core
|
||||||
|
|
|
||||||
|
|
@ -60,15 +60,26 @@ struct TableColumn {
|
||||||
TableColumnKind kind = TableColumnKind::Text;
|
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)+ 总数(分页用)。
|
// 通用表格载荷:列定义 + 预格式化的行(每格 QString)+ 总数(分页用)。
|
||||||
// 分页(dd_grid 列表,服务端分页 vxe-pager):pageSize>0 时视图渲染分页器,pageNo 为当前页(1 基);
|
// 分页(dd_grid 列表,服务端分页 vxe-pager):pageSize>0 时视图渲染分页器,pageNo 为当前页(1 基);
|
||||||
// pageSize=0(默认)= 不分页(measurement/trajectory 全量列表,一次性返回所有行)。
|
// pageSize=0(默认)= 不分页(measurement/trajectory 全量列表,一次性返回所有行)。
|
||||||
|
// functionButtons:仅 dd_grid 列表非空(来自服务端 functionList),驱动列表上方功能按钮行。
|
||||||
struct TablePayload {
|
struct TablePayload {
|
||||||
std::vector<TableColumn> columns;
|
std::vector<TableColumn> columns;
|
||||||
std::vector<std::vector<QString>> rows;
|
std::vector<std::vector<QString>> rows;
|
||||||
int total = 0;
|
int total = 0;
|
||||||
int pageNo = 1; // 当前页(1 基);分页用
|
int pageNo = 1; // 当前页(1 基);分页用
|
||||||
int pageSize = 0; // 每页条数;>0 才渲染分页器(vxe-pager),0=不分页
|
int pageSize = 0; // 每页条数;>0 才渲染分页器(vxe-pager),0=不分页
|
||||||
|
std::vector<TableFunctionButton> functionButtons; // dd_grid 功能按钮(其余场景空)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 柱状图系列:名称(图例/legend)+ 各类目的 y 值 + 填充色(hex,如 #5470c6;数据色,两主题一致)。
|
// 柱状图系列:名称(图例/legend)+ 各类目的 y 值 + 填充色(hex,如 #5470c6;数据色,两主题一致)。
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ add_library(geopro_data STATIC
|
||||||
api/ApiProjectRepository.cpp
|
api/ApiProjectRepository.cpp
|
||||||
api/ApiDatasetRepository.cpp
|
api/ApiDatasetRepository.cpp
|
||||||
api/ApiColorTemplateRepository.cpp
|
api/ApiColorTemplateRepository.cpp
|
||||||
|
api/ApiDatasetCommandRepository.cpp
|
||||||
api/Api3dRepository.cpp
|
api/Api3dRepository.cpp
|
||||||
api/DatasetLoadHandles.cpp
|
api/DatasetLoadHandles.cpp
|
||||||
api/NavRequest.cpp)
|
api/NavRequest.cpp)
|
||||||
|
|
|
||||||
|
|
@ -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" // parseMeasurementScatter(V 值重载复用初始加载解析)
|
||||||
|
#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 自行 deleteLater;batch 本身随末次信号后 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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -65,8 +65,12 @@ std::vector<Anomaly> parseDatasetAnomalies(const QJsonArray& arr) {
|
||||||
for (auto e : arr) {
|
for (auto e : arr) {
|
||||||
const QJsonObject o = e.toObject();
|
const QJsonObject o = e.toObject();
|
||||||
Anomaly a;
|
Anomaly a;
|
||||||
|
a.id = o.value("id").toString().toStdString(); // 删除/更新/定位需要持久化 id
|
||||||
a.name = o.value("exceptionName").toString().toStdString();
|
a.name = o.value("exceptionName").toString().toStdString();
|
||||||
a.typeName = o.value("exceptionTypeName").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=面
|
const int mt = o.value("exceptionMarkType").toInt(2); // 1=点 2=线 3=面
|
||||||
a.markType = (mt >= 1 && mt <= 3) ? static_cast<AnomalyMarkType>(mt)
|
a.markType = (mt >= 1 && mt <= 3) ? static_cast<AnomalyMarkType>(mt)
|
||||||
: AnomalyMarkType::Polyline; // 越界值兜底为线
|
: AnomalyMarkType::Polyline; // 越界值兜底为线
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "dto/GridDto.hpp"
|
#include "dto/GridDto.hpp"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
#include <QJsonValue>
|
#include <QJsonValue>
|
||||||
|
|
||||||
#include "dto/TrajectoryDto.hpp" // parseGridHeaderTable(通用 gridHeaderDisplay+rowList 解析器)复用
|
#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.total = data.value(QStringLiteral("total")).toInt(t.total);
|
||||||
t.pageNo = pn;
|
t.pageNo = pn;
|
||||||
t.pageSize = ps;
|
t.pageSize = ps;
|
||||||
|
|
||||||
|
// 功能按钮:data.functionList(DDGridFunctionVO[])→ 列表上方功能按钮行(原版 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;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ namespace geopro::data::dto {
|
||||||
// 复用通用 parseGridHeaderTable(gridHeaderDisplay→x/y 列 + rowList→行),再前插「序号」列
|
// 复用通用 parseGridHeaderTable(gridHeaderDisplay→x/y 列 + rowList→行),再前插「序号」列
|
||||||
// (vxe seq 列:按页偏移自增 = (pageNo-1)*pageSize + 行内序号);total 取 data.total(非 __rowTotal);
|
// (vxe seq 列:按页偏移自增 = (pageNo-1)*pageSize + 行内序号);total 取 data.total(非 __rowTotal);
|
||||||
// 回填 pageNo/pageSize 供视图渲染分页器。pageNo/pageSize 为本次请求参数(仓储已解析默认值后传入)。
|
// 回填 pageNo/pageSize 供视图渲染分页器。pageNo/pageSize 为本次请求参数(仓储已解析默认值后传入)。
|
||||||
|
// 另解析 data.functionList(DDGridFunctionVO[])→ functionButtons(驱动列表上方功能按钮行,含「反演」)。
|
||||||
geopro::core::TablePayload parseGridTable(const QJsonObject& data, int pageNo, int pageSize);
|
geopro::core::TablePayload parseGridTable(const QJsonObject& data, int pageNo, int pageSize);
|
||||||
|
|
||||||
} // namespace geopro::data::dto
|
} // namespace geopro::data::dto
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,15 @@ namespace {
|
||||||
constexpr int kColHorizontalDistance = 0; // x 备选:平距
|
constexpr int kColHorizontalDistance = 0; // x 备选:平距
|
||||||
constexpr int kColSlopeDistance = 1; // x:斜距(默认)
|
constexpr int kColSlopeDistance = 1; // x:斜距(默认)
|
||||||
constexpr int kColPseudoDepth = 3; // y:伪深度(负,向下,默认)
|
constexpr int kColPseudoDepth = 3; // y:伪深度(负,向下,默认)
|
||||||
|
constexpr int kColVisible = 7; // 可见性:0=显示 1=隐藏(displayStatus)
|
||||||
|
constexpr int kColId = 8; // 点 id(saveDisplayStatus ids[])
|
||||||
constexpr int kColElevationPseudoDepth = 9; // y 备选:伪深度+高程
|
constexpr int kColElevationPseudoDepth = 9; // y 备选:伪深度+高程
|
||||||
constexpr int kColA = 10;
|
constexpr int kColA = 10;
|
||||||
constexpr int kColB = 11;
|
constexpr int kColB = 11;
|
||||||
constexpr int kColM = 12;
|
constexpr int kColM = 12;
|
||||||
constexpr int kColN = 13;
|
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) {
|
double numAt(const QJsonArray& arr, int i) {
|
||||||
if (i < 0 || i >= arr.size()) return 0.0;
|
if (i < 0 || i >= arr.size()) return 0.0;
|
||||||
|
|
@ -28,6 +31,15 @@ double numAt(const QJsonArray& arr, int i) {
|
||||||
return 0.0;
|
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/缺省→空串)。
|
// 把 JSON 值预格式化为单元格 QString(数字按原值,null/缺省→空串)。
|
||||||
QString cellText(const QJsonValue& v) {
|
QString cellText(const QJsonValue& v) {
|
||||||
if (v.isDouble()) {
|
if (v.isDouble()) {
|
||||||
|
|
@ -75,6 +87,11 @@ ScatterPayload parseMeasurementScatter(const QJsonObject& scatterData, const QJs
|
||||||
s.b.push_back(numAt(row, kColB));
|
s.b.push_back(numAt(row, kColB));
|
||||||
s.m.push_back(numAt(row, kColM));
|
s.m.push_back(numAt(row, kColM));
|
||||||
s.n.push_back(numAt(row, kColN));
|
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 无需再请求)。
|
// x/y 下拉本地重绘备选列(与上面 push 同序、一一对应 → 视图换 x/y 无需再请求)。
|
||||||
p.altXHorizontal.push_back(numAt(row, kColHorizontalDistance));
|
p.altXHorizontal.push_back(numAt(row, kColHorizontalDistance));
|
||||||
p.altXSlope.push_back(numAt(row, kColSlopeDistance));
|
p.altXSlope.push_back(numAt(row, kColSlopeDistance));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "model/detail/DetailPayloads.hpp" // core::ScatterPayload(V 值重载返回完整载荷)
|
||||||
|
|
||||||
|
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 之外的 dsId),status 为 int(0/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, ...}(对应原版 saveRawDataValue,projectSpace 模块)。
|
||||||
|
// 原版仅约束 dsObjectId 必填;operationType/name 由调用方按场景组装,故收 body。
|
||||||
|
virtual void saveRawData(const QJsonObject& body,
|
||||||
|
std::function<void(bool ok, QString msg)> cb) = 0;
|
||||||
|
|
||||||
|
// 导出 DAT:GET /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, ...}(对应原版 whitenTheData,3 种 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}
|
||||||
|
// (对应原版 queryExceptionTypeData,type = 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;
|
||||||
|
|
||||||
|
// 更新 dsObject:PUT /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
|
||||||
|
|
@ -139,6 +139,26 @@ target_sources(geopro_tests PRIVATE
|
||||||
app/test_dataset_dimension.cpp
|
app/test_dataset_dimension.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/app/DatasetDimension.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)。
|
# controller 层:DatasetDetailController 编排集成测试(QSignalSpy 验证 datasetOpened/tabReady/loadFailed)。
|
||||||
find_package(Qt6 COMPONENTS Test REQUIRED)
|
find_package(Qt6 COMPONENTS Test REQUIRED)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -56,3 +56,18 @@ TEST(DatasetChartDto, ParsesAnomalyPolyline) {
|
||||||
EXPECT_DOUBLE_EQ(v[0].localPts[1].x, 3.0);
|
EXPECT_DOUBLE_EQ(v[0].localPts[1].x, 3.0);
|
||||||
EXPECT_TRUE(v[0].dashed);
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,4 +75,35 @@ TEST(GridDto, EmptyDataYieldsSeqOnlyColumnNoRows) {
|
||||||
EXPECT_EQ(t.columns[0].code.toStdString(), "__seq");
|
EXPECT_EQ(t.columns[0].code.toStdString(), "__seq");
|
||||||
EXPECT_EQ(t.rows.size(), 0u);
|
EXPECT_EQ(t.rows.size(), 0u);
|
||||||
EXPECT_EQ(t.total, 0);
|
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 不渲染)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,26 @@ TEST(MeasurementDto, ParsesScatterPositionalRows) {
|
||||||
EXPECT_DOUBLE_EQ(s.electrodeNo[2], 3.0);
|
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 本地重绘备选列。
|
// scatter/graph 含 scatterGraphConf 时:工具条下拉项 + 默认选中 + x/y 本地重绘备选列。
|
||||||
static const char* kScatterDataWithConf = R"({
|
static const char* kScatterDataWithConf = R"({
|
||||||
"electrodeList": [
|
"electrodeList": [
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue