feat/dataset-detail-chart #5
|
|
@ -0,0 +1,114 @@
|
||||||
|
# 数据详情「按类型渲染」通用引擎 + 迁移 inversion + measurement_data 实现计划
|
||||||
|
|
||||||
|
> 2026-06-12。承接 `2026-06-11-dataset-detail-other-dd-types.md`(Phase 1 策略分派已 done)。
|
||||||
|
> **决策(用户已拍板=选项2)**:建通用渲染引擎,并把已验收的 `dd_inversion_data` 一起迁到新引擎、删旧专用代码,终态只有一套。
|
||||||
|
> **铁律**:不确定必用 Playwright 实看原版;严格 1:1;无活样本类型 BLOCKED。
|
||||||
|
> **工作方式**:subagent-driven + TDD(真实夹具) + 破坏性接口改形原子落地保持构建绿。
|
||||||
|
|
||||||
|
## 0. 背景与动机
|
||||||
|
|
||||||
|
详情视图有多种 dd 类型,每类页签集 + 图形 kind 不同。现状把"两页签(chartReady/gridReady) + 两个专用 Load 句柄 + 写死 inversion 字段"耦合进三层。要支持 measurement_data / gr_data(柱状) / trajectory(轨迹) / grid(白化) 等且可持续扩展,需围绕「页签描述符」统一。
|
||||||
|
|
||||||
|
## 1. Phase 0 实测成果(2026-06-12,真实响应,已抓夹具)
|
||||||
|
|
||||||
|
API base:`http://tenant.geomative.cn/pop-api/business`。Auth header:`geomativeauthorization: Geomative <token>`。
|
||||||
|
同一 TM(project=1438889436225536 / structParentId=1438889842614272)集齐 4 类样本:
|
||||||
|
|
||||||
|
| ddCode | dsId 样本 | 端点 | 数据形状 | 页签 → render kind |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `dd_ert_measurement_data` 原始 | 1453611522236416 | `GET dd/ert/measurement/scatter/graph?dsObjectId=&vFieldCode=`(图) + `GET dd/ert/measurement/rows?dsObjectId=`(列表) | 图:`{electrodeList[40], scatterGraphConf{x[2],v[6],y[3],vcalculationMethod[3]}, scatterGraphData{hasCoordinate,hasElevation,min[9],max[9],rows[576]位置数组}}`;列表:`{filedList[12], rowList[576]键控对象,含 vmap{R,V,I,R0_RD,SP,R0}}` | 散点伪剖面=Scatter + 列表=Table |
|
||||||
|
| `dd_ert_measurement_gr_data` 接地电阻 | 1453611522285569 | `GET dd/ert/measurement/gr/rows?dsObjectId=` | `array[40]` of `{electrodeId,testDate,testTime,p1Rg,p1RgStatus,p2Rg,p2RgStatus}` | 柱状图=Bar(X=electrodeId,Y=电阻Ω,系列P1) + 列表=Table |
|
||||||
|
| `dd_trajectory_data` 坐标 | 1438893961060352 | `GET dd/ert/trajectory/rows?dsObjectId=`(+`/line?…&frontCrsCode` 400 需参数) | `{rowList[40]{electrodeNo,projectX,projectY,longitude,latitude,elevation,lineNo,channelNo,…}, gridHeaderDisplay[14]}` | 地图=PolylineMap(平面折线,非GIS底图) + 列表=Table + 高程=LineProfile |
|
||||||
|
| `dd_grid` 白化 | 1438944148742207 | `GET dd/ert/grid/rows?dsObjectId=&pageNo=&pageSize=` | `{rowList[N]{x,y,id}, gridHeaderDisplay[2], total}` **分页** | 列表=Table(仅,分页) |
|
||||||
|
|
||||||
|
夹具:`tests/fixtures/dd/ert-measurement-scatter.json`、`ert-measurement-rows.json`(真实响应,rows 裁剪至 20,保 conf/min/max/electrodeList 全量)。
|
||||||
|
|
||||||
|
**散点位置数组列序(rows[i],与键控 rows 比对得)**:`[0]平距 [1]斜距 [2]layerNo(null) [3]伪深度(负,向下) [4]选中v原值 [5]log10(值) [6]导数态 [7]0 [8]id(str) [9]高程伪深度 [10]a [11]b [12]m [13]n [14]K [15]rowNo [16]选中v值(=色)`。默认 vFieldCode 空 → v=视电阻率 R0;x=平距;y=伪深度。色阶 cauto(同反演原数据,按 v 有限值 min/max 归一化)。
|
||||||
|
|
||||||
|
**渲染 kind 全集**:Scatter / FilledContour / Bar / LineProfile / PolylineMap / Table /(未来 Image,GPR BLOCKED)。
|
||||||
|
**通用 Table**:`{gridHeaderDisplay 或 filedList(列定义) + rowList(对象)}` 形状被 measurement列表/grid白化/trajectory列表共用 → 一个 `DataTableView` + 一个 `parseTable` 通吃(分页与列格式化为 parse-time/decorator,见 §4)。
|
||||||
|
|
||||||
|
## 2. 目标架构
|
||||||
|
|
||||||
|
依赖箭头:`controller(ViewKind/TabSpec/Controller) ← app/strategies, app/views ; data(DetailLoad/Repo) 产出 QVariant 载荷`。
|
||||||
|
|
||||||
|
### 2.1 载荷(payload)——`src/model/detail/DetailPayloads.hpp`(纯数据,无 Qt-widget 依赖,Q_DECLARE_METATYPE)
|
||||||
|
- `ScatterPayload { core::ScatterField scatter; core::ColorScale scale; }`(反演原数据 + measurement 散点共用;≈现 `ChartParts`)
|
||||||
|
- `ContourPayload { core::Grid grid; core::ColorScale scale; std::vector<core::Anomaly> anomalies; }`(≈现 `GridParts`)
|
||||||
|
- `TablePayload { std::vector<TableColumn> columns; std::vector<std::vector<QString>> rows; int total = 0; }`;`TableColumn { QString code, title; int width; int sort; }`
|
||||||
|
- (Bar/LineProfile/Polyline 载荷待对应类型阶段再加,YAGNI)
|
||||||
|
|
||||||
|
### 2.2 页签描述符 + 策略——`src/controller/DatasetDetailTab.hpp` + `IDatasetChartStrategy.hpp`
|
||||||
|
```cpp
|
||||||
|
enum class ViewKind { Scatter, FilledContour, Bar, LineProfile, PolylineMap, Table /*,Image*/ };
|
||||||
|
struct TabSpec { QString title; ViewKind kind; QString loaderKey; bool lazy=false; bool paginated=false; };
|
||||||
|
struct IDatasetChartStrategy { virtual std::string ddCode() const=0; virtual std::vector<TabSpec> tabs() const=0; }; // tabs() 取代 hasGridPhase()
|
||||||
|
```
|
||||||
|
`ChartStrategyRegistry` 不变。`ErtInversionStrategy::tabs()` = `{{"原数据",Scatter,"inversion.scatter",false},{"网格数据",FilledContour,"inversion.grid",true}}`。
|
||||||
|
|
||||||
|
### 2.3 通用异步句柄——`src/data/api/DatasetLoadHandles.*`
|
||||||
|
合并 `ChartLoad`/`GridLoad` → 单 `DetailLoad{ virtual void abort(); signals: done(QVariant); failed(QString); }` + `ApiDetailLoad`(包 ApiBatch + Parser=`std::function<QVariant(const QList<net::ApiResponse>&)>`)。**aborted_ 守卫 + 双 try/catch→failed + deleteLater 三件套从 ApiChartLoad 原样照搬**。删 `ChartParts`/`GridParts`(被 payload 取代)或保留作中间态(由实现者定,优先删以免双份)。
|
||||||
|
|
||||||
|
### 2.4 仓储——`IAsyncDatasetRepository::loadAsync(loaderKey,dsId)` + `ApiDatasetRepository` 分派表
|
||||||
|
唯一端点+解析器集中处。`if (key=="inversion.scatter") return makeInversionScatter(dsId); …`;未知 key 抛。每 make 建 ApiBatch + 注入返回 `QVariant::fromValue(payload)` 的解析器。删 `loadChartAsync/loadGridAsync`。
|
||||||
|
> measurement scatter 的 `dsObjectId` 是 **query 参数**(`?dsObjectId=&vFieldCode=`,默认 vFieldCode 空),≠反演 path 参数;用现有 `enc()`。
|
||||||
|
|
||||||
|
### 2.5 控制器——`DatasetDetailController` 变通用(无 per-ddCode if)
|
||||||
|
```cpp
|
||||||
|
slots: openDataset(dsId,ddCode,dsName), loadTab(dsId,ddCode,tabIndex), focusDataset(dsId);
|
||||||
|
signals: datasetOpened(dsId,dsName, std::vector<TabSpec>), tabLoadStarted(dsId,tabIndex),
|
||||||
|
tabReady(dsId,tabIndex,QVariant), loadFailed(dsId,msg), focusRequested(dsId);
|
||||||
|
```
|
||||||
|
`openDataset`:查 registry,无→`loadFailed("暂不支持…")`(降级不变);`emit datasetOpened(...,strategy->tabs())`;对每个非 lazy 页签 `loadTab`。
|
||||||
|
`loadTab`:**唯一保存 §5.0 安全不变量处**,按 tab 槽位:`inflight_`=`QMap<int,QPointer<DetailLoad>>`;abort 旧槽 → 新 load → `tabLoadStarted` → done 回调里 `if (load!=inflight_.value(i)) return;`(身份比对)→ `tabReady`。析构 abort 全部。删 `chartReady/gridReady/ChartData/GridData/loadGridData`。
|
||||||
|
|
||||||
|
### 2.6 视图 + 工厂 + 壳
|
||||||
|
- `src/app/panels/chart/IDetailView.hpp`:`{ QWidget* widget(); void setPayload(const QVariant&); }`。
|
||||||
|
- `RawDataChartView`/`GridDataChartView` 实现 IDetailView:`setPayload` 解包 `ScatterPayload`/`ContourPayload` → 调原 `setData(...)`(**渲染代码零改**)。坏/空 variant → 可见"渲染数据格式错误"不崩。
|
||||||
|
- `DetailViewFactory.{hpp,cpp}`:`makeDetailView(ViewKind,parent)` switch → 对应视图。
|
||||||
|
- `DatasetDetailPage`:`build(tabs)` 按 TabSpec 建页签(`buildTabbedPanel` + 工厂);lazy 页签首次激活发 `tabNeeded(dsId,ddCode,i)`(泛化现 gridDataNeeded);`setTabPayload(i,QVariant)` → `views_[i]->setPayload`。删写死 `rawView_/gridView_/kGridTabIndex`。
|
||||||
|
- `DatasetDetailPanel`:`onDatasetOpened(dsId,dsName,tabs)` 建 page→build;按 dsId 转发 tabReady/tabNeeded。dsId 去重壳逻辑不动。
|
||||||
|
- `main.cpp`:接线改 datasetOpened/tabReady/tabNeeded/tabLoadStarted;registry 注册不变(后续加 MeasurementStrategy)。**与控制器改形同提交(原子)。**
|
||||||
|
|
||||||
|
## 3. 实施序列(任务)
|
||||||
|
|
||||||
|
### E1a 引擎地基(加性,构建保持绿) [task #1]
|
||||||
|
新增 payloads / TabSpec/ViewKind / DetailLoad+ApiDetailLoad(与旧句柄并存) / `loadAsync` 分派(inversion.scatter/grid 产 payload),与旧 `loadChartAsync/loadGridAsync` 并存。TDD:DTO→payload、句柄桩(`tests/data/test_dataset_load_handles.cpp` 仿现有桩)、loadAsync 分派。**构建+测试全绿,旧路径不动。**
|
||||||
|
|
||||||
|
### E1b 破坏性改形 + inversion 迁移(原子) [task #2]
|
||||||
|
控制器/策略接口/视图 IDetailView/工厂/Page/Panel/main.cpp 全部切到 tab 引擎;删旧 `chartReady/gridReady/ChartData/GridData/ChartLoad/GridLoad/loadChartAsync/loadGridAsync/hasGridPhase`。改全部相关测试(controller/strategy_registry/handles)。**一个提交内落地,构建+测试绿 + 跑 app 双击标准 ds `1458990939709440` 目视回归两页签(散点+网格等值面+异常)正常。**
|
||||||
|
|
||||||
|
### E2 measurement_data(③散点伪剖面 + 列表) [task #3]
|
||||||
|
|
||||||
|
**Phase 0 色阶补查(2026-06-12,已坐实,夹具齐)**:measurement 散点**有 colorBar**,来自 `POST lvl/colorGradation/getDetail` body **`{dsObjectId, businessCode:"<vFieldCode,默认R0>", type:3}`**(注意:businessCode=v字段码、type=3,≠反演的 type1/2;用 businessCode='' 会返回 null)。响应是**与反演同构的混合格式 colorBar**(hex + rgba alpha 0–1,18 个对数间距 stops `0.00→#00008B … 208.40→#FF0000 … 1323.20→rgba(48,0,48,1)`,`lvlSchemeType:normal`/equalAreaLayerCount/labelConfig/lineConfig)——**现有 `DatasetChartDto` colorBar 解析器(AlphaScale::Unit)直接可用**。截图核对:散点按 v 值**绝对值离散映射**到 colorBar(同反演**网格** `colorAtDiscrete`,**非**反演原数据的连续 cauto),右侧 ColorBarWidget 显示离散级。夹具:`tests/fixtures/dd/ert-measurement-{scatter,rows,colorbar}.json`;原版截图 `meas-scatter-original.jpeg`。
|
||||||
|
|
||||||
|
**⚠️ 着色模型修正(2026-06-12 读原版 Plotly `_fullData` 实测,推翻"离散"判断)**:原版是 **Plotly scattergl**,散点**连续 cauto 着色**(非离散!):`marker.color`=原始 v 值(col4,**含负值**,本例范围 -1066.59~1232.44);`cauto:true` → `cmin/cmax`=数据 v 的真实 min/max(**含负离群值**);`colorscale`=连续 18 段,**position=colorBar值/maxColorBar值(1323.20)**;上色=`norm(v)=(v-cmin)/(cmax-cmin)` 查 colorscale 连续插值。验证 v=50.18→norm 0.4857→暗紫(stop 0.358 与 0.540 之间)。**这与 inversion 原数据完全同路(交接 §0.2:cauto 数据范围 + colorscale 位置 val/maxVal),measurement 必须用 `discreteColor=false` 走同一条已验收路径**,而非我 E2 误用的 colorAtDiscrete。负离群值撑大数据范围 → R0=25~250 全挤色阶中段暗紫,故原版几乎全暗紫(仅极端值现橙/蓝)。colorBar 图例(竖排右侧离散色块标 0.00~1323.20)独立于 cauto,显示 colorBar 自身 value→color。hover=浮动黑底框 X/Y/Value/a/b/m/n(另有底部 footer A=B=M=N=DataRow=Pseu_Resis=)。
|
||||||
|
|
||||||
|
**散点渲染规格(截图确认)**:x=**斜距 slopeDistance**(toolbar 默认,col1;≈平距,几乎不可辨,存疑标用户核对)、y=**伪深度 pseudoDepth**(col3,负向下)、色=**视电阻率 R0**(col16,离散 colorBar)、x 轴在**顶部**(同反演原数据)、y=0 处画 **40 个灰色菱形=电极位置**(electrodeList 的 x)、hover 显示 `A= B= M= N= DataRow= Pseu_Resis=`(col10-13=A/B/M/N + 值)。工具条(散点图/数据列表 切换、显示/隐藏、数据过滤、x/y/v/计算方法下拉、色阶配置、生成视电阻率、反演运算、另存为、导出)=**范围外**,MVP 只渲染默认静态视图。
|
||||||
|
- 端点:scatter `GET dd/ert/measurement/scatter/graph?dsObjectId=&vFieldCode=`(空=默认R0)+ colorBar `POST lvl/colorGradation/getDetail{dsObjectId,businessCode:"R0",type:3}`;列表 `GET dd/ert/measurement/rows?dsObjectId=`。loaderKey `ert_measurement.scatter` 用 ApiBatch 组 scatter+colorBar 两请求(类比反演 loadChartAsync 的 scatter+type1)。
|
||||||
|
|
||||||
|
- `src/data/dto/MeasurementDto.{hpp,cpp}`:`parseMeasurementScatter(scatterJson, colorBarJson)→ScatterPayload`(位置数组 rows:x=col1 斜距/y=col3 伪深度/色值=col16;colorBar 复用现有解析器→离散 ColorScale;电极 electrodeList→灰菱形,可放 ScatterPayload 扩展或单列);`parseTable(QJsonObject)→TablePayload`(通用:列来自 `gridHeaderDisplay` 或 `filedList`,值预格式化为 QString;measurement 的 `vmap` 由解析器摊平为列——按 filedList 列码取 vmap[code])。
|
||||||
|
- `src/app/panels/chart/DataTableView.{hpp,cpp}`:IDetailView + QTableView + QAbstractTableModel(由 TablePayload 构),列宽/标题/排序来自 TableColumn。
|
||||||
|
- `src/app/strategies/MeasurementStrategy.hpp`:ddCode=`dd_ert_measurement_data`,tabs=`{{"散点伪剖面",Scatter,"ert_measurement.scatter",false},{"列表",Table,"ert_measurement.rows",true}}`。
|
||||||
|
- `ApiDatasetRepository`:加 `ert_measurement.scatter`(scatter/graph,query 参数 + 默认 vFieldCode 空)、`ert_measurement.rows`(measurement/rows)loaderKey。
|
||||||
|
- `DetailViewFactory`:加 `Table` case。`main.cpp`:注册 MeasurementStrategy 一行。
|
||||||
|
- TDD 用 `tests/fixtures/dd` 夹具:`tests/data/test_measurement_dto.cpp`(散点位置解码/y 负号/列提取/vmap 摊平) + 控制器 2 页签谱 + loadAsync 分派。
|
||||||
|
- 构建+测试绿 + 跑 app 双击「ERT原始数据」对照原版散点伪剖面 + 列表目视核对。
|
||||||
|
|
||||||
|
## 4. 风险与裁断(architect 评估)
|
||||||
|
|
||||||
|
1. **不要过度泛化控制器信号面**:`tabReady(dsId,i,QVariant)` 单信号已吸收全部近期形状(类型擦除)。激进泛化"句柄/载荷"(合一 DetailLoad/QVariant,是纯去重),但控制器信号保持扁平最小。分页的 `pageNo` 等真做 dd_grid 再加一个默认参,加性无重构。
|
||||||
|
2. **QVariant 类型擦除以编译期安全换扩展性**:错的 loaderKey→ViewKind→payload 三元组是运行期 cast miss 而非编译错。缓解:downcast 仅在每视图 `setPayload` 一处;坏 variant 出可见错误不崩(配合现 GuardedApplication::notify);每策略一条 round-trip 测试。接受此权衡(否则 N×3 个 XxxLoad/XxxReady/XxxData 爆炸)。
|
||||||
|
3. **不要提前耦合 BLOCKED 的 GPR/radar**:引擎结构上可容纳(加 ViewKind::Image/GisMap + 新视图+解析器+loaderKey,控制器/句柄零改),故不被锁死;但现在不加任何 enum 值/桩(1:1 + 无样本)。trajectory 的 PolylineMap 是金丝雀,证明非图非表视图也能套进引擎。
|
||||||
|
|
||||||
|
**通用 Table 的边界**:list 形状(measurement/trajectory/grid)共用对;但 ① 分页(dd_grid 独有)= `PaginatedTableView` 装饰器或 spec `paginated` 标志 + page 发 `loadTab(...,pageNo)`,**基类 DataTableView 保持哑**;② 列格式化/值语义(measurement 的 vmap 摊平、gr 的 status 标志着色)= **parse-time** 处理(TablePayload 存预格式化 QString),需要彩色状态格时再加 `TableColumn.kind` 提示(YAGNI)。bar 不是表复用——gr_data 主页签是 Bar,列表页签才复用 parseTable。
|
||||||
|
|
||||||
|
## 5. 安全不变量(spec §5.0,务必保留)
|
||||||
|
|
||||||
|
"abort 后绝不回灌"三件套:① 每层 `aborted_` 入口守卫;② 控制器句柄身份比对(`if (load!=inflight_.value(i)) return;`);③ 一律 `deleteLater`。错误判定:业务 `code!=200 || !rawError.isEmpty()`(HTTP 200 也可能 code=500)。迁移后这三件套从"按 Chart/Grid 阶段"改"按页签槽位",逐字保留,不得重造削弱。
|
||||||
|
|
||||||
|
## 6. 构建/测试命令
|
||||||
|
|
||||||
|
- 构建:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-build.ps1`(LNK1104 先 `Get-Process geopro_desktop|Stop-Process -Force`)。
|
||||||
|
- 测试:`powershell.exe -ExecutionPolicy Bypass -File scripts/dev-test.ps1`(先 build)。基线 **122/122 绿**。
|
||||||
|
- 单测过滤:`build/release/tests/geopro_tests.exe --gtest_filter=<Suite>.*`。
|
||||||
|
|
@ -29,6 +29,8 @@ add_executable(geopro_desktop WIN32
|
||||||
panels/DescriptionPanel.cpp
|
panels/DescriptionPanel.cpp
|
||||||
panels/chart/RawDataChartView.cpp
|
panels/chart/RawDataChartView.cpp
|
||||||
panels/chart/GridDataChartView.cpp
|
panels/chart/GridDataChartView.cpp
|
||||||
|
panels/chart/DataTableView.cpp
|
||||||
|
panels/chart/DetailViewFactory.cpp
|
||||||
panels/chart/ChartTheme.cpp
|
panels/chart/ChartTheme.cpp
|
||||||
panels/chart/ColorMapService.cpp
|
panels/chart/ColorMapService.cpp
|
||||||
panels/chart/ColorBarWidget.cpp
|
panels/chart/ColorBarWidget.cpp
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@
|
||||||
#include "WorkbenchNavController.hpp"
|
#include "WorkbenchNavController.hpp"
|
||||||
#include "DatasetDetailController.hpp"
|
#include "DatasetDetailController.hpp"
|
||||||
#include "panels/chart/ErtInversionStrategy.hpp"
|
#include "panels/chart/ErtInversionStrategy.hpp"
|
||||||
|
#include "panels/chart/MeasurementStrategy.hpp"
|
||||||
#include "api/ApiProjectRepository.hpp"
|
#include "api/ApiProjectRepository.hpp"
|
||||||
#include "api/ApiDatasetRepository.hpp"
|
#include "api/ApiDatasetRepository.hpp"
|
||||||
#include "panels/ObjectTreePanel.hpp"
|
#include "panels/ObjectTreePanel.hpp"
|
||||||
|
|
@ -514,37 +515,23 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
if (!dsId.isEmpty()) detailCtrl.openDataset(dsId, ddCode, dsName);
|
if (!dsId.isEmpty()) detailCtrl.openDataset(dsId, ddCode, dsName);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── 控制器信号 → 详情面板:数据就绪开页 / 聚焦请求 ──
|
// ── 控制器信号 → 详情面板(tab 引擎):开页 / 页签就绪 / 加载中 / 聚焦 ──
|
||||||
QObject::connect(
|
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::datasetOpened,
|
||||||
&detailCtrl, &geopro::controller::DatasetDetailController::chartReady, detailPanel,
|
detailPanel, &geopro::app::DatasetDetailPanel::onDatasetOpened);
|
||||||
[detailPanel](const geopro::controller::DatasetDetailController::ChartData& d) {
|
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::tabReady,
|
||||||
detailPanel->openOrUpdate(d);
|
detailPanel, &geopro::app::DatasetDetailPanel::onTabReady);
|
||||||
});
|
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::tabLoadStarted,
|
||||||
|
detailPanel, &geopro::app::DatasetDetailPanel::onTabLoadStarted);
|
||||||
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::focusRequested,
|
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::focusRequested,
|
||||||
detailPanel, [detailPanel](const QString& dsId) {
|
detailPanel, &geopro::app::DatasetDetailPanel::focusDataset);
|
||||||
detailPanel->focusDataset(dsId);
|
// ── 页签懒加载:lazy 页签首次激活 → 控制器按 (dsId,ddCode,tabIndex) 拉载荷 → 回填 ──
|
||||||
});
|
QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::tabNeeded, &detailCtrl,
|
||||||
// ── 网格数据懒加载:网格页首次激活 → 拉 rows+色阶type2+异常 → 回填对应页 ──
|
&geopro::controller::DatasetDetailController::loadTab);
|
||||||
QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::gridDataNeeded, &detailCtrl,
|
// context 用 detailPanel:析构即自动断连,避免野指针。window 比 detailPanel 活得久,
|
||||||
&geopro::controller::DatasetDetailController::loadGridData);
|
// 捕 &window 取状态栏安全。失败时清该页 lazy 遮罩(幂等)并状态栏提示。
|
||||||
QObject::connect(
|
|
||||||
&detailCtrl, &geopro::controller::DatasetDetailController::gridReady, detailPanel,
|
|
||||||
[detailPanel](const geopro::controller::DatasetDetailController::GridData& d) {
|
|
||||||
detailPanel->setGridData(d);
|
|
||||||
});
|
|
||||||
QObject::connect(
|
|
||||||
&detailCtrl, &geopro::controller::DatasetDetailController::loadStarted, detailPanel,
|
|
||||||
[detailPanel](const QString& dsId,
|
|
||||||
geopro::controller::DatasetDetailController::LoadPhase phase) {
|
|
||||||
if (phase == geopro::controller::DatasetDetailController::LoadPhase::Grid)
|
|
||||||
detailPanel->setGridLoading(dsId, true);
|
|
||||||
});
|
|
||||||
// context 用 detailPanel(与 loadStarted 一致):detailPanel 析构即自动断连,避免野指针。
|
|
||||||
// window 比 detailPanel 活得久,lambda 捕 &window 取状态栏安全。loadFailed 无 phase:
|
|
||||||
// Chart 失败时 gridOverlay 本已隐藏,setGridLoading(false) 幂等无害;Grid 失败时正确清遮罩。
|
|
||||||
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::loadFailed, detailPanel,
|
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::loadFailed, detailPanel,
|
||||||
[&window, detailPanel](const QString& dsId, const QString& msg) {
|
[&window, detailPanel](const QString& dsId, const QString& msg) {
|
||||||
detailPanel->setGridLoading(dsId, false);
|
detailPanel->onLoadFailed(dsId, msg);
|
||||||
window.statusBar()->showMessage(
|
window.statusBar()->showMessage(
|
||||||
QStringLiteral("数据集 %1 加载失败:%2").arg(dsId, msg), 5000);
|
QStringLiteral("数据集 %1 加载失败:%2").arg(dsId, msg), 5000);
|
||||||
});
|
});
|
||||||
|
|
@ -924,6 +911,7 @@ int main(int argc, char* argv[])
|
||||||
// 策略注册表须比 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>());
|
||||||
|
chartRegistry.add(std::make_unique<geopro::app::MeasurementStrategy>());
|
||||||
geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry);
|
geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry);
|
||||||
|
|
||||||
// ── 外壳:标准 QMainWindow(原生标题栏)。buildWorkbench 直接用其
|
// ── 外壳:标准 QMainWindow(原生标题栏)。buildWorkbench 直接用其
|
||||||
|
|
|
||||||
|
|
@ -6,63 +6,78 @@
|
||||||
#include "Glyphs.hpp"
|
#include "Glyphs.hpp"
|
||||||
#include "PanelHeader.hpp"
|
#include "PanelHeader.hpp"
|
||||||
#include "panels/LoadingOverlay.hpp"
|
#include "panels/LoadingOverlay.hpp"
|
||||||
#include "panels/chart/GridDataChartView.hpp"
|
#include "panels/chart/DetailViewFactory.hpp"
|
||||||
#include "panels/chart/RawDataChartView.hpp"
|
#include "panels/chart/IDetailView.hpp"
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr int kGridTabIndex = 1; // 「网格数据」页签在 tabs 中的索引
|
|
||||||
}
|
|
||||||
|
|
||||||
DatasetDetailPage::DatasetDetailPage(QWidget* parent) : QWidget(parent) {
|
DatasetDetailPage::DatasetDetailPage(QWidget* parent) : QWidget(parent) {
|
||||||
auto* lay = new QVBoxLayout(this);
|
auto* lay = new QVBoxLayout(this);
|
||||||
lay->setContentsMargins(0, 0, 0, 0);
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
lay->setSpacing(0);
|
lay->setSpacing(0);
|
||||||
|
}
|
||||||
|
|
||||||
rawView_ = new RawDataChartView(this);
|
void DatasetDetailPage::build(const QString& dsId, const QString& ddCode, const QString& dsName,
|
||||||
gridView_ = new GridDataChartView(this);
|
const std::vector<geopro::controller::TabSpec>& tabs) {
|
||||||
gridOverlay_ = new LoadingOverlay(gridView_); // 父为 gridView_,随其尺寸覆盖网格图区
|
Q_ASSERT(views_.empty()); // build() 仅一次:views_ 为已 release 的裸指针,二次构建会泄漏旧视图
|
||||||
|
if (!views_.empty()) return;
|
||||||
|
dsId_ = dsId;
|
||||||
|
ddCode_ = ddCode;
|
||||||
|
dsName_ = dsName;
|
||||||
|
tabs_ = tabs;
|
||||||
|
views_.assign(tabs.size(), nullptr);
|
||||||
|
loaded_.assign(tabs.size(), false);
|
||||||
|
requested_.assign(tabs.size(), false);
|
||||||
|
overlays_.clear();
|
||||||
|
|
||||||
const QVector<PanelTab> tabs = {
|
// 按 TabSpec 经工厂造视图,并组装为带表头页签的面板。
|
||||||
{Glyph::Detail, QStringLiteral("原数据"), rawView_, false},
|
QVector<PanelTab> panelTabs;
|
||||||
{Glyph::Dataset, QStringLiteral("网格数据"), gridView_, false},
|
for (size_t i = 0; i < tabs.size(); ++i) {
|
||||||
};
|
const auto& spec = tabs[i];
|
||||||
|
auto view = makeDetailView(spec.kind, this); // 抛出由调用栈兜底(GuardedApplication)
|
||||||
|
IDetailView* raw = view.release(); // QWidget 由 this/QwtPlot 父子树接管生命周期
|
||||||
|
views_[i] = raw;
|
||||||
|
// lazy 页签:建覆盖该视图的加载遮罩(父为视图 widget,随其尺寸覆盖图区)。
|
||||||
|
if (spec.lazy) overlays_[static_cast<int>(i)] = new LoadingOverlay(raw->widget());
|
||||||
|
panelTabs.append({Glyph::Detail, spec.title, raw->widget(), false});
|
||||||
|
}
|
||||||
const QVector<HeaderAction> actions = {
|
const QVector<HeaderAction> actions = {
|
||||||
{Glyph::Download, QStringLiteral("导出")},
|
{Glyph::Download, QStringLiteral("导出")},
|
||||||
};
|
};
|
||||||
|
|
||||||
auto tabbedPanel = buildTabbedPanel(tabs, actions);
|
auto tabbedPanel = buildTabbedPanel(panelTabs, actions);
|
||||||
lay->addWidget(tabbedPanel.container);
|
layout()->addWidget(tabbedPanel.container);
|
||||||
|
|
||||||
// 「网格数据」页签首次激活 → 懒加载网格数据(rows 服务端网格化慢,故不随开页同步拉)。
|
// lazy 页签首次激活 → 发 tabNeeded 请求懒加载(数据慢,故不随开页同步拉)。
|
||||||
if (tabbedPanel.tabGroup) {
|
if (tabbedPanel.tabGroup) {
|
||||||
connect(tabbedPanel.tabGroup, &QButtonGroup::idClicked, this, [this](int idx) {
|
connect(tabbedPanel.tabGroup, &QButtonGroup::idClicked, this, [this](int idx) {
|
||||||
if (idx != kGridTabIndex || gridRequested_ || dsId_.isEmpty()) return;
|
if (idx < 0 || idx >= static_cast<int>(tabs_.size())) return;
|
||||||
gridRequested_ = true;
|
if (!tabs_[static_cast<size_t>(idx)].lazy) return;
|
||||||
emit gridDataNeeded(dsId_, ddCode_);
|
if (requested_[static_cast<size_t>(idx)] || loaded_[static_cast<size_t>(idx)]) return;
|
||||||
|
if (dsId_.isEmpty()) return;
|
||||||
|
requested_[static_cast<size_t>(idx)] = true;
|
||||||
|
emit tabNeeded(dsId_, ddCode_, idx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatasetDetailPage::setData(const geopro::controller::DatasetDetailController::ChartData& d) {
|
void DatasetDetailPage::setTabPayload(int tabIndex, const QVariant& payload) {
|
||||||
dsId_ = d.dsId;
|
if (tabIndex < 0 || tabIndex >= static_cast<int>(views_.size())) return;
|
||||||
ddCode_ = d.ddCode;
|
if (auto* v = views_[static_cast<size_t>(tabIndex)]) v->setPayload(payload);
|
||||||
gridRequested_ = false; // 新数据集 → 网格数据需重新按需加载
|
loaded_[static_cast<size_t>(tabIndex)] = true;
|
||||||
rawView_->setData(d);
|
requested_[static_cast<size_t>(tabIndex)] = true; // 已加载,切回不再重复请求
|
||||||
gridView_->setData(d);
|
setTabLoading(tabIndex, false); // 数据到达,隐藏遮罩
|
||||||
setGridLoading(false); // 重开/换 ds:重置遮罩状态,避免上次的「加载中」残留
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatasetDetailPage::setGridData(
|
void DatasetDetailPage::setTabLoading(int tabIndex, bool on) {
|
||||||
const geopro::controller::DatasetDetailController::GridData& d) {
|
auto it = overlays_.find(tabIndex);
|
||||||
gridRequested_ = true; // 已加载,切回网格页不再重复请求
|
if (it == overlays_.end()) return; // 非 lazy 页签无遮罩
|
||||||
gridView_->setGridData(d.grid, d.gridScale, d.anomalies);
|
if (on) it.value()->showOver(); else it.value()->hide();
|
||||||
setGridLoading(false); // 数据到达,隐藏遮罩
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatasetDetailPage::setGridLoading(bool on) {
|
void DatasetDetailPage::clearAllLoadingOverlays() {
|
||||||
if (on) gridOverlay_->showOver(); else gridOverlay_->hide();
|
for (auto* overlay : overlays_)
|
||||||
|
if (overlay) overlay->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,51 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QVariant>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include "DatasetDetailController.hpp"
|
#include "DatasetDetailTab.hpp" // geopro::controller::TabSpec
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
class RawDataChartView;
|
class IDetailView;
|
||||||
class GridDataChartView;
|
|
||||||
class LoadingOverlay;
|
class LoadingOverlay;
|
||||||
|
|
||||||
// 单个数据集详情页:下划线页签「原数据 / 网格数据」+ 右侧「导出」操作。
|
// 单个数据集详情页:按策略 tabs() 动态建页签 + 右侧「导出」操作。
|
||||||
// 内部分别由 RawDataChartView / GridDataChartView 实现各自三层布局。
|
// 每页签由工厂造的 IDetailView 承载;lazy 页签首次激活时发 tabNeeded 请求懒加载。
|
||||||
class DatasetDetailPage : public QWidget {
|
class DatasetDetailPage : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit DatasetDetailPage(QWidget* parent = nullptr);
|
explicit DatasetDetailPage(QWidget* parent = nullptr);
|
||||||
void setData(const geopro::controller::DatasetDetailController::ChartData& d);
|
|
||||||
// 网格数据到达(懒加载结果)→ 下发给 GridDataChartView 并标记已加载。
|
// 按页签集构建页签(首次打开调一次)。dsId/ddCode/dsName 用于 tabNeeded。
|
||||||
void setGridData(const geopro::controller::DatasetDetailController::GridData& d);
|
void build(const QString& dsId, const QString& ddCode, const QString& dsName,
|
||||||
// 网格懒加载进行中(true)/结束(false)时切换遮罩显隐。
|
const std::vector<geopro::controller::TabSpec>& tabs);
|
||||||
void setGridLoading(bool on);
|
|
||||||
|
// 页签载荷到达 → 下发给对应视图并标记已加载、隐藏遮罩。
|
||||||
|
void setTabPayload(int tabIndex, const QVariant& payload);
|
||||||
|
// 页签加载进行中 → 对 lazy 页签显示遮罩(非 lazy 页签无遮罩,幂等忽略)。
|
||||||
|
void setTabLoading(int tabIndex, bool on);
|
||||||
|
// 清掉本页全部加载遮罩(失败兜底用,不假设页签数;幂等)。
|
||||||
|
void clearAllLoadingOverlays();
|
||||||
|
|
||||||
QString dsId() const { return dsId_; }
|
QString dsId() const { return dsId_; }
|
||||||
|
int tabCount() const { return static_cast<int>(tabs_.size()); }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
// 「网格数据」页签首次激活且本页网格数据未加载 → 请求懒加载。
|
// lazy 页签首次激活且未加载 → 请求懒加载。
|
||||||
void gridDataNeeded(const QString& dsId, const QString& ddCode);
|
void tabNeeded(const QString& dsId, const QString& ddCode, int tabIndex);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString dsId_;
|
QString dsId_;
|
||||||
QString ddCode_;
|
QString ddCode_;
|
||||||
bool gridRequested_ = false; // 已请求过(避免重复发信号)
|
QString dsName_;
|
||||||
RawDataChartView* rawView_;
|
std::vector<geopro::controller::TabSpec> tabs_;
|
||||||
GridDataChartView* gridView_;
|
// 与 tabs_ 同序。每个 IDetailView 持有的 QWidget 经 build() 以 this 为父接管,
|
||||||
LoadingOverlay* gridOverlay_; // 网格懒加载期间覆盖 gridView_ 的遮罩
|
// 生命周期由 Qt 父子树清理(不在此 delete);build() 仅调用一次(见其断言)。
|
||||||
|
std::vector<IDetailView*> views_;
|
||||||
|
std::vector<bool> loaded_; // 各页签是否已加载(避免重复请求)
|
||||||
|
std::vector<bool> requested_; // lazy 页签是否已请求过
|
||||||
|
QMap<int, LoadingOverlay*> overlays_; // lazy 页签的加载遮罩(覆盖该视图)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -18,26 +18,35 @@ DatasetDetailPage* DatasetDetailPanel::pageFor(const QString& dsId) const {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatasetDetailPanel::openOrUpdate(const geopro::controller::DatasetDetailController::ChartData& d) {
|
void DatasetDetailPanel::onDatasetOpened(const QString& dsId, const QString& ddCode,
|
||||||
auto* p = pageFor(d.dsId);
|
const QString& dsName,
|
||||||
|
const std::vector<geopro::controller::TabSpec>& tabs) {
|
||||||
|
auto* p = pageFor(dsId);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
p = new DatasetDetailPage(this);
|
p = new DatasetDetailPage(this);
|
||||||
const QString title = d.dsName.isEmpty() ? d.dsId : d.dsName; // 页签标题用数据名(空则回退 id)
|
p->build(dsId, ddCode, dsName, tabs); // ddCode 透传 → 页内 tabNeeded 携带
|
||||||
|
const QString title = dsName.isEmpty() ? dsId : dsName; // 页签标题用数据名(空则回退 id)
|
||||||
const int idx = addTab(p, title);
|
const int idx = addTab(p, title);
|
||||||
setTabToolTip(idx, title); // 名称过长被省略时悬停可见全名
|
setTabToolTip(idx, title); // 名称过长被省略时悬停可见全名
|
||||||
// 页内「网格数据」页签首次激活 → 冒泡为面板信号(外部接控制器懒加载)。
|
// 页内 lazy 页签首次激活 → 冒泡为面板信号(外部接控制器 loadTab)。
|
||||||
connect(p, &DatasetDetailPage::gridDataNeeded, this, &DatasetDetailPanel::gridDataNeeded);
|
connect(p, &DatasetDetailPage::tabNeeded, this, &DatasetDetailPanel::tabNeeded);
|
||||||
}
|
}
|
||||||
p->setData(d);
|
|
||||||
setCurrentWidget(p);
|
setCurrentWidget(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatasetDetailPanel::setGridData(const geopro::controller::DatasetDetailController::GridData& d) {
|
void DatasetDetailPanel::onTabReady(const QString& dsId, int tabIndex, const QVariant& payload) {
|
||||||
if (auto* p = pageFor(d.dsId)) p->setGridData(d);
|
if (auto* p = pageFor(dsId)) p->setTabPayload(tabIndex, payload);
|
||||||
}
|
}
|
||||||
void DatasetDetailPanel::setGridLoading(const QString& dsId, bool on) {
|
|
||||||
if (auto* p = pageFor(dsId)) p->setGridLoading(on);
|
void DatasetDetailPanel::onTabLoadStarted(const QString& dsId, int tabIndex) {
|
||||||
|
if (auto* p = pageFor(dsId)) p->setTabLoading(tabIndex, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DatasetDetailPanel::onLoadFailed(const QString& dsId, const QString&) {
|
||||||
|
// 失败:清掉该页全部「加载中」遮罩(不假设页签数;幂等:非 lazy/已隐藏皆无害)。
|
||||||
|
if (auto* p = pageFor(dsId)) p->clearAllLoadingOverlays();
|
||||||
|
}
|
||||||
|
|
||||||
void DatasetDetailPanel::focusDataset(const QString& dsId) {
|
void DatasetDetailPanel::focusDataset(const QString& dsId) {
|
||||||
if (auto* p = pageFor(dsId)) setCurrentWidget(p);
|
if (auto* p = pageFor(dsId)) setCurrentWidget(p);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,29 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
#include <QTabWidget>
|
#include <QTabWidget>
|
||||||
#include "DatasetDetailController.hpp"
|
#include <QVariant>
|
||||||
|
#include "DatasetDetailTab.hpp" // geopro::controller::TabSpec
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
class DatasetDetailPage;
|
class DatasetDetailPage;
|
||||||
|
|
||||||
// 多 Tab 壳:每数据集一页(按 dsId 去重)。R095。
|
// 多 Tab 壳:每数据集一页(按 dsId 去重)。R095。tab 引擎版。
|
||||||
class DatasetDetailPanel : public QTabWidget {
|
class DatasetDetailPanel : public QTabWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit DatasetDetailPanel(QWidget* parent = nullptr);
|
explicit DatasetDetailPanel(QWidget* parent = nullptr);
|
||||||
void openOrUpdate(const geopro::controller::DatasetDetailController::ChartData& d); // 双击/数据到达
|
|
||||||
void setGridData(const geopro::controller::DatasetDetailController::GridData& d); // 网格数据懒加载到达
|
// 数据集打开:find-or-create 页 → build(tabs) → 加/抬该面板页签。
|
||||||
void setGridLoading(const QString& dsId, bool on); // 网格懒加载进行中/结束 → 转发给对应页
|
void onDatasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName,
|
||||||
|
const std::vector<geopro::controller::TabSpec>& tabs);
|
||||||
|
void onTabReady(const QString& dsId, int tabIndex, const QVariant& payload);
|
||||||
|
void onTabLoadStarted(const QString& dsId, int tabIndex);
|
||||||
|
void onLoadFailed(const QString& dsId, const QString& message);
|
||||||
void focusDataset(const QString& dsId); // 单击聚焦已开页
|
void focusDataset(const QString& dsId); // 单击聚焦已开页
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void activeDatasetChanged(const QString& dsId); // 反向联动数据集列表
|
void activeDatasetChanged(const QString& dsId); // 反向联动数据集列表
|
||||||
void gridDataNeeded(const QString& dsId, const QString& ddCode); // 网格页首次激活 → 请求懒加载
|
void tabNeeded(const QString& dsId, const QString& ddCode, int tabIndex); // lazy 页首激活 → 懒加载
|
||||||
|
|
||||||
private:
|
private:
|
||||||
DatasetDetailPage* pageFor(const QString& dsId) const;
|
DatasetDetailPage* pageFor(const QString& dsId) const;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,16 @@ namespace geopro::app {
|
||||||
static constexpr int kBarHeight = 18; // 色带高度(px)
|
static constexpr int kBarHeight = 18; // 色带高度(px)
|
||||||
static constexpr int kTickHeight = 4; // 刻度短线高度(px)
|
static constexpr int kTickHeight = 4; // 刻度短线高度(px)
|
||||||
static constexpr int kFontSize = 9; // 刻度字号
|
static constexpr int kFontSize = 9; // 刻度字号
|
||||||
|
static constexpr int kVBarWidth = 16; // 竖条色带宽度(px)
|
||||||
|
static constexpr int kVLabelGap = 4; // 竖条值标签与色带间距(px)
|
||||||
|
|
||||||
ColorBarWidget::ColorBarWidget(QWidget* parent)
|
ColorBarWidget::ColorBarWidget(QWidget* parent, Orientation orient)
|
||||||
: QWidget(parent) {
|
: QWidget(parent), orient_(orient) {
|
||||||
|
if (orient_ == Orientation::Vertical) {
|
||||||
|
setFixedWidth(64); // 竖条 + 左侧值标签
|
||||||
|
} else {
|
||||||
setFixedHeight(36);
|
setFixedHeight(36);
|
||||||
|
}
|
||||||
// 主题热切换:底色/边框/刻度字跟随主题重绘(色带本身=数据色,不变)。
|
// 主题热切换:底色/边框/刻度字跟随主题重绘(色带本身=数据色,不变)。
|
||||||
connect(&ThemeManager::instance(), &ThemeManager::changed, this,
|
connect(&ThemeManager::instance(), &ThemeManager::changed, this,
|
||||||
qOverload<>(&QWidget::update));
|
qOverload<>(&QWidget::update));
|
||||||
|
|
@ -26,6 +32,14 @@ void ColorBarWidget::setColorScale(const core::ColorScale& scale) {
|
||||||
void ColorBarWidget::paintEvent(QPaintEvent* /*event*/) {
|
void ColorBarWidget::paintEvent(QPaintEvent* /*event*/) {
|
||||||
QPainter p(this);
|
QPainter p(this);
|
||||||
p.setRenderHint(QPainter::Antialiasing, false);
|
p.setRenderHint(QPainter::Antialiasing, false);
|
||||||
|
if (orient_ == Orientation::Vertical) {
|
||||||
|
paintVertical(p);
|
||||||
|
} else {
|
||||||
|
paintHorizontal(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorBarWidget::paintHorizontal(QPainter& p) {
|
||||||
const int W = width();
|
const int W = width();
|
||||||
const int H = height();
|
const int H = height();
|
||||||
// 浅色=白底深字(原版 1:1);暗色=深底浅字,避免刺眼白条。色带格(数据色)两者不变。
|
// 浅色=白底深字(原版 1:1);暗色=深底浅字,避免刺眼白条。色带格(数据色)两者不变。
|
||||||
|
|
@ -79,4 +93,58 @@ void ColorBarWidget::paintEvent(QPaintEvent* /*event*/) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ColorBarWidget::paintVertical(QPainter& p) {
|
||||||
|
const int W = width();
|
||||||
|
const int H = height();
|
||||||
|
const bool dark = isDarkTheme();
|
||||||
|
const QColor bgColor = dark ? tokenColor("bg/panel") : QColor(Qt::white);
|
||||||
|
const QColor borderColor = dark ? tokenColor("border/strong") : QColor(120, 120, 120);
|
||||||
|
const QColor textColor = dark ? tokenColor("text/secondary"): QColor(60, 60, 60);
|
||||||
|
p.fillRect(0, 0, W, H, bgColor);
|
||||||
|
|
||||||
|
auto stops = scale_.stops();
|
||||||
|
if (stops.size() < 2) return;
|
||||||
|
|
||||||
|
QFont font = p.font();
|
||||||
|
font.setPixelSize(kFontSize);
|
||||||
|
p.setFont(font);
|
||||||
|
QFontMetrics fm(font);
|
||||||
|
|
||||||
|
// 竖条靠右;左侧留出值标签宽度。上下各留半行高,避免顶/底标签出界。
|
||||||
|
const int halfLine = fm.height() / 2 + 1;
|
||||||
|
const int barRight = W - 2;
|
||||||
|
const int barLeft = barRight - kVBarWidth;
|
||||||
|
const int barTop = halfLine;
|
||||||
|
const int barBottom = H - halfLine;
|
||||||
|
const int barH = barBottom - barTop;
|
||||||
|
if (barH < 10 || barLeft < 0) return;
|
||||||
|
|
||||||
|
// 等高色带:每相邻断点一格,最大值在顶、最小值在底(与原版 1323→0 自上而下一致)。
|
||||||
|
const int nSeg = static_cast<int>(stops.size()) - 1;
|
||||||
|
// segY(i): 第 i 个边界的 y(i=0 为最小值在底,i=nSeg 为最大值在顶)。
|
||||||
|
auto segY = [&](int i) {
|
||||||
|
return barBottom - static_cast<int>(static_cast<double>(i) / nSeg * barH);
|
||||||
|
};
|
||||||
|
for (int i = 0; i < nSeg; ++i) {
|
||||||
|
int yB = segY(i), yT = segY(i + 1);
|
||||||
|
if (yT >= yB) yT = yB - 1;
|
||||||
|
const auto& c = stops[i].second; // 段 [stop_i, stop_{i+1}) 取 stop_i 色(与离散一致)
|
||||||
|
p.fillRect(barLeft, yT, kVBarWidth, yB - yT, QColor(c.r, c.g, c.b, c.a));
|
||||||
|
}
|
||||||
|
// 外边框
|
||||||
|
p.setPen(QPen(borderColor, 1));
|
||||||
|
p.drawRect(barLeft, barTop, kVBarWidth - 1, barH - 1);
|
||||||
|
|
||||||
|
// 边界值标签(色带左侧,垂直居中于各边界)。
|
||||||
|
p.setPen(textColor);
|
||||||
|
for (int i = 0; i <= nSeg; ++i) {
|
||||||
|
int y = segY(i);
|
||||||
|
const QString label = QString::number(stops[i].first, 'f', 2);
|
||||||
|
int tw = fm.horizontalAdvance(label);
|
||||||
|
int tx = barLeft - kVLabelGap - tw;
|
||||||
|
if (tx < 0) tx = 0;
|
||||||
|
p.drawText(tx, y + fm.ascent() / 2 - 1, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,17 @@
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
// 独立色阶条 Widget,水平排布:上方彩色色带 + 下方刻度值。
|
// 独立色阶条 Widget,作为 QwtPlot 的兄弟 widget,不进入 Qwt 坐标系,
|
||||||
// 作为 QwtPlot 的兄弟 widget 布局在图表下方,不进入 Qwt 坐标系,
|
|
||||||
// 因此不随图表缩放/平移移动,也不与轴标注重叠。
|
// 因此不随图表缩放/平移移动,也不与轴标注重叠。
|
||||||
|
// Horizontal(默认,反演原数据):底部横条,等宽色带 + 下方边界刻度值。
|
||||||
|
// Vertical(measurement):右侧竖条,等高色带,最大值在顶、最小值在底,
|
||||||
|
// 边界值标在色带左侧(对齐原版 Plotly 离散图例)。
|
||||||
class ColorBarWidget : public QWidget {
|
class ColorBarWidget : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit ColorBarWidget(QWidget* parent = nullptr);
|
enum class Orientation { Horizontal, Vertical };
|
||||||
|
|
||||||
|
explicit ColorBarWidget(QWidget* parent = nullptr, Orientation orient = Orientation::Horizontal);
|
||||||
|
|
||||||
void setColorScale(const core::ColorScale& scale);
|
void setColorScale(const core::ColorScale& scale);
|
||||||
|
|
||||||
|
|
@ -18,7 +22,11 @@ protected:
|
||||||
void paintEvent(QPaintEvent* event) override;
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void paintHorizontal(QPainter& p);
|
||||||
|
void paintVertical(QPainter& p);
|
||||||
|
|
||||||
core::ColorScale scale_;
|
core::ColorScale scale_;
|
||||||
|
Orientation orient_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
#include "panels/chart/DataTableView.hpp"
|
||||||
|
|
||||||
|
#include <QHeaderView>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QTableView>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// 药丸开关配色(原版蓝色 ON)。
|
||||||
|
const QColor kSwitchOn(64, 158, 255); // 蓝色(ON/可见)
|
||||||
|
const QColor kSwitchOff(190, 190, 190); // 灰色(OFF/隐藏)
|
||||||
|
constexpr int kSwitchW = 36; // 轨道宽
|
||||||
|
constexpr int kSwitchH = 18; // 轨道高(=直径)
|
||||||
|
constexpr int kKnobMargin = 2; // 旋钮与轨道边距
|
||||||
|
constexpr int kToggleColW = 80; // Toggle 列固定窄宽(开关 36 + 两侧留白)
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TablePayloadModel::TablePayloadModel(QObject* parent) : QAbstractTableModel(parent) {}
|
||||||
|
|
||||||
|
void TablePayloadModel::setPayload(const geopro::core::TablePayload& payload) {
|
||||||
|
beginResetModel();
|
||||||
|
payload_ = payload;
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
int TablePayloadModel::rowCount(const QModelIndex& parent) const {
|
||||||
|
if (parent.isValid()) return 0;
|
||||||
|
return static_cast<int>(payload_.rows.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
int TablePayloadModel::columnCount(const QModelIndex& parent) const {
|
||||||
|
if (parent.isValid()) return 0;
|
||||||
|
return static_cast<int>(payload_.columns.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant TablePayloadModel::data(const QModelIndex& index, int role) const {
|
||||||
|
if (!index.isValid()) return {};
|
||||||
|
const size_t r = static_cast<size_t>(index.row());
|
||||||
|
const size_t c = static_cast<size_t>(index.column());
|
||||||
|
if (r >= payload_.rows.size() || c >= payload_.rows[r].size()) return {};
|
||||||
|
if (role == Qt::TextAlignmentRole)
|
||||||
|
return static_cast<int>(Qt::AlignCenter); // 单元内容水平+垂直居中
|
||||||
|
if (role == Qt::DisplayRole) return payload_.rows[r][c];
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant TablePayloadModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
||||||
|
if (role != Qt::DisplayRole) return {};
|
||||||
|
if (orientation == Qt::Horizontal) {
|
||||||
|
if (section < 0 || section >= static_cast<int>(payload_.columns.size())) return {};
|
||||||
|
return payload_.columns[static_cast<size_t>(section)].title;
|
||||||
|
}
|
||||||
|
return section + 1; // 行号
|
||||||
|
}
|
||||||
|
|
||||||
|
geopro::core::TableColumnKind TablePayloadModel::columnKind(int column) const {
|
||||||
|
if (column < 0 || column >= static_cast<int>(payload_.columns.size()))
|
||||||
|
return geopro::core::TableColumnKind::Text;
|
||||||
|
return payload_.columns[static_cast<size_t>(column)].kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleSwitchDelegate::ToggleSwitchDelegate(const TablePayloadModel* model, QObject* parent)
|
||||||
|
: QStyledItemDelegate(parent), model_(model) {}
|
||||||
|
|
||||||
|
void ToggleSwitchDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||||
|
const QModelIndex& index) const {
|
||||||
|
if (!model_ || model_->columnKind(index.column()) != geopro::core::TableColumnKind::Toggle) {
|
||||||
|
QStyledItemDelegate::paint(painter, option, index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 画背景(选中态/交替行),但不画文本。
|
||||||
|
QStyleOptionViewItem opt = option;
|
||||||
|
initStyleOption(&opt, index);
|
||||||
|
opt.text.clear();
|
||||||
|
if (const QWidget* w = opt.widget)
|
||||||
|
w->style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, w);
|
||||||
|
|
||||||
|
const bool on = index.data(Qt::DisplayRole).toString() == QLatin1String("1");
|
||||||
|
|
||||||
|
// 居中药丸开关。
|
||||||
|
const QRect cell = option.rect;
|
||||||
|
QRectF track(cell.center().x() - kSwitchW / 2.0, cell.center().y() - kSwitchH / 2.0,
|
||||||
|
kSwitchW, kSwitchH);
|
||||||
|
painter->save();
|
||||||
|
painter->setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
painter->setPen(Qt::NoPen);
|
||||||
|
painter->setBrush(on ? kSwitchOn : kSwitchOff);
|
||||||
|
painter->drawRoundedRect(track, kSwitchH / 2.0, kSwitchH / 2.0);
|
||||||
|
// 白色旋钮(ON 靠右,OFF 靠左)。
|
||||||
|
const double d = kSwitchH - 2.0 * kKnobMargin;
|
||||||
|
const double knobX = on ? track.right() - kKnobMargin - d : track.left() + kKnobMargin;
|
||||||
|
painter->setBrush(Qt::white);
|
||||||
|
painter->drawEllipse(QRectF(knobX, track.top() + kKnobMargin, d, d));
|
||||||
|
painter->restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize ToggleSwitchDelegate::sizeHint(const QStyleOptionViewItem& option,
|
||||||
|
const QModelIndex& index) const {
|
||||||
|
if (!model_ || model_->columnKind(index.column()) != geopro::core::TableColumnKind::Toggle)
|
||||||
|
return QStyledItemDelegate::sizeHint(option, index);
|
||||||
|
// 贴合开关的窄宽(+两侧内边距),高度沿用默认行高,避免 ResizeToContents 撑宽该列。
|
||||||
|
return QSize(kToggleColW, kSwitchH + 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
DataTableView::DataTableView(QWidget* parent) : QWidget(parent) {
|
||||||
|
auto* lay = new QVBoxLayout(this);
|
||||||
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
lay->setSpacing(0);
|
||||||
|
|
||||||
|
model_ = new TablePayloadModel(this);
|
||||||
|
table_ = new QTableView(this);
|
||||||
|
table_->setModel(model_);
|
||||||
|
table_->setAlternatingRowColors(true);
|
||||||
|
table_->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||||
|
table_->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||||
|
// 列宽策略在 setPayload 按列种类设置(Toggle 列固定窄宽;文本列均分 Stretch)。
|
||||||
|
// 不强制拉伸末列——末列通常是窄的 Toggle 列,拉伸会把它撑得过宽。
|
||||||
|
table_->horizontalHeader()->setStretchLastSection(false);
|
||||||
|
table_->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter); // 表头文本居中
|
||||||
|
table_->verticalHeader()->setDefaultSectionSize(24);
|
||||||
|
table_->verticalHeader()->setVisible(false); // 原版无行号列
|
||||||
|
|
||||||
|
// Toggle 列委托:把“隐藏/显示”列画成蓝色药丸开关。
|
||||||
|
table_->setItemDelegate(new ToggleSwitchDelegate(model_, table_));
|
||||||
|
|
||||||
|
lay->addWidget(table_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataTableView::setPayload(const QVariant& payload) {
|
||||||
|
if (!payload.canConvert<geopro::core::TablePayload>()) return; // 坏/空 → 空态
|
||||||
|
const auto t = payload.value<geopro::core::TablePayload>();
|
||||||
|
model_->setPayload(t);
|
||||||
|
|
||||||
|
// 列宽策略:文本列等宽均分,Toggle 列窄。
|
||||||
|
// Toggle 列 → Fixed 固定窄宽(贴合开关 + 留白),开关居中。
|
||||||
|
// 其余文本列 → Stretch(均分可用宽度,整体均衡,无单一巨列、无右侧空白)。
|
||||||
|
auto* header = table_->horizontalHeader();
|
||||||
|
for (size_t i = 0; i < t.columns.size(); ++i) {
|
||||||
|
const int col = static_cast<int>(i);
|
||||||
|
if (t.columns[i].kind == geopro::core::TableColumnKind::Toggle) {
|
||||||
|
header->setSectionResizeMode(col, QHeaderView::Fixed);
|
||||||
|
table_->setColumnWidth(col, kToggleColW);
|
||||||
|
} else {
|
||||||
|
header->setSectionResizeMode(col, QHeaderView::Stretch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QAbstractTableModel>
|
||||||
|
#include <QStyledItemDelegate>
|
||||||
|
#include <QWidget>
|
||||||
|
#include "model/detail/DetailPayloads.hpp"
|
||||||
|
#include "panels/chart/IDetailView.hpp"
|
||||||
|
|
||||||
|
class QTableView;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// TablePayload 驱动的只读表模型(列标题来自 TableColumn,单元为预格式化 QString)。
|
||||||
|
class TablePayloadModel : public QAbstractTableModel {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit TablePayloadModel(QObject* parent = nullptr);
|
||||||
|
void setPayload(const geopro::core::TablePayload& payload);
|
||||||
|
|
||||||
|
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||||
|
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||||
|
QVariant data(const QModelIndex& index, int role) const override;
|
||||||
|
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
|
||||||
|
|
||||||
|
// 列渲染种类(供委托判断是否画开关)。越界返回 Text。
|
||||||
|
geopro::core::TableColumnKind columnKind(int column) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
geopro::core::TablePayload payload_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle 列委托:把单元值("1"=ON/"0"=OFF)画成蓝色药丸开关(仅展示状态,暂不联动散点)。
|
||||||
|
// Text 列回退到默认绘制。
|
||||||
|
class ToggleSwitchDelegate : public QStyledItemDelegate {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit ToggleSwitchDelegate(const TablePayloadModel* model, QObject* parent = nullptr);
|
||||||
|
void paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||||
|
const QModelIndex& index) const override;
|
||||||
|
// Toggle 列返回贴合开关的窄尺寸(供 ResizeToContents 不至于撑宽);其余回退默认。
|
||||||
|
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const TablePayloadModel* model_; // 不拥有
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通用数据列表视图:IDetailView + QTableView。measurement/grid/trajectory 列表共用。
|
||||||
|
class DataTableView : public QWidget, public IDetailView {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit DataTableView(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
QWidget* widget() override { return this; }
|
||||||
|
void setPayload(const QVariant& payload) override; // 坏/空 variant → 保持空态不崩
|
||||||
|
|
||||||
|
private:
|
||||||
|
QTableView* table_;
|
||||||
|
TablePayloadModel* model_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
#include "panels/chart/DetailViewFactory.hpp"
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "panels/chart/DataTableView.hpp"
|
||||||
|
#include "panels/chart/GridDataChartView.hpp"
|
||||||
|
#include "panels/chart/RawDataChartView.hpp"
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget* parent) {
|
||||||
|
switch (kind) {
|
||||||
|
case controller::ViewKind::Scatter:
|
||||||
|
return std::unique_ptr<IDetailView>(new RawDataChartView(parent));
|
||||||
|
case controller::ViewKind::FilledContour:
|
||||||
|
return std::unique_ptr<IDetailView>(new GridDataChartView(parent));
|
||||||
|
case controller::ViewKind::Table:
|
||||||
|
return std::unique_ptr<IDetailView>(new DataTableView(parent));
|
||||||
|
case controller::ViewKind::Bar:
|
||||||
|
case controller::ViewKind::LineProfile:
|
||||||
|
case controller::ViewKind::PolylineMap:
|
||||||
|
// 后续阶段补:Bar(gr_data)/LineProfile,PolylineMap(trajectory)。
|
||||||
|
throw std::runtime_error("makeDetailView: ViewKind not yet implemented");
|
||||||
|
}
|
||||||
|
throw std::runtime_error("makeDetailView: unknown ViewKind");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
#pragma once
|
||||||
|
#include <memory>
|
||||||
|
#include "DatasetDetailTab.hpp" // geopro::controller::ViewKind
|
||||||
|
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
class IDetailView;
|
||||||
|
|
||||||
|
// 按 render kind 造详情视图。E1b 仅支持 Scatter / FilledContour(反演两页签);
|
||||||
|
// Bar/LineProfile/PolylineMap/Table 留待后续阶段(measurement/trajectory/grid)补,
|
||||||
|
// 现阶段命中会抛 std::runtime_error(明确失败,而非静默空指针)。
|
||||||
|
std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget* parent);
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
#include "IDatasetChartStrategy.hpp" // geopro::controller(geopro_controller PUBLIC include)
|
#include "IDatasetChartStrategy.hpp" // geopro::controller(geopro_controller PUBLIC include)
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
// ERT 反演策略:散点(chart) + 网格等值面(grid) 两阶段。
|
// ERT 反演策略:散点(原数据,同步) + 网格等值面(网格数据,懒加载) 两页签。
|
||||||
struct ErtInversionStrategy : controller::IDatasetChartStrategy {
|
struct ErtInversionStrategy : controller::IDatasetChartStrategy {
|
||||||
std::string ddCode() const override { return "dd_inversion_data"; }
|
std::string ddCode() const override { return "dd_inversion_data"; }
|
||||||
bool hasGridPhase() const override { return true; } // 反演有网格数据阶段
|
std::vector<controller::TabSpec> tabs() const override {
|
||||||
|
return {
|
||||||
|
{QStringLiteral("原数据"), controller::ViewKind::Scatter,
|
||||||
|
QStringLiteral("inversion.scatter"), /*lazy*/ false, /*paginated*/ false},
|
||||||
|
{QStringLiteral("网格数据"), controller::ViewKind::FilledContour,
|
||||||
|
QStringLiteral("inversion.grid"), /*lazy*/ true, /*paginated*/ false},
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -177,10 +177,13 @@ GridDataChartView::~GridDataChartView() {
|
||||||
delete colorSvc_;
|
delete colorSvc_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GridDataChartView::setData(const geopro::controller::DatasetDetailController::ChartData& d) {
|
void GridDataChartView::setPayload(const QVariant& payload) {
|
||||||
data_ = d;
|
if (!payload.canConvert<geopro::core::ContourPayload>()) {
|
||||||
// 开页:仅把 anomalies 喂给底部异常列表;图表区待网格数据懒加载后填充。
|
// 坏/空 variant:保持空态(不渲染、不崩)。
|
||||||
anomalyTable_->setAnomalies(d.anomalies, {}, {});
|
return;
|
||||||
|
}
|
||||||
|
const auto p = payload.value<geopro::core::ContourPayload>();
|
||||||
|
setGridData(p.grid, p.scale, p.anomalies);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GridDataChartView::setGridData(const geopro::core::Grid& grid,
|
void GridDataChartView::setGridData(const geopro::core::Grid& grid,
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@
|
||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "DatasetDetailController.hpp"
|
|
||||||
#include "model/Anomaly.hpp"
|
#include "model/Anomaly.hpp"
|
||||||
#include "model/ColorScale.hpp"
|
#include "model/ColorScale.hpp"
|
||||||
#include "model/Field.hpp"
|
#include "model/Field.hpp"
|
||||||
|
#include "model/detail/DetailPayloads.hpp"
|
||||||
|
#include "panels/chart/IDetailView.hpp"
|
||||||
|
|
||||||
class QSlider;
|
class QSlider;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
|
|
@ -24,24 +25,23 @@ class ContourPlotItem;
|
||||||
// 网格数据图表视图:工具条 + QwtPlot(白底 + 真实比尺 + 实时平移/滚轮缩放,x 轴在底部)
|
// 网格数据图表视图:工具条 + QwtPlot(白底 + 真实比尺 + 实时平移/滚轮缩放,x 轴在底部)
|
||||||
// + 独立色阶条 + 底部双页签(异常列表/描述)。
|
// + 独立色阶条 + 底部双页签(异常列表/描述)。
|
||||||
// 填充走 ContourPlotItem 栅格热力图 + 矢量等值线 + 标注 + 异常叠加。
|
// 填充走 ContourPlotItem 栅格热力图 + 矢量等值线 + 标注 + 异常叠加。
|
||||||
class GridDataChartView : public QWidget {
|
class GridDataChartView : public QWidget, public IDetailView {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit GridDataChartView(QWidget* parent = nullptr);
|
explicit GridDataChartView(QWidget* parent = nullptr);
|
||||||
~GridDataChartView() override;
|
~GridDataChartView() override;
|
||||||
|
|
||||||
// 开页时调用:仅喂底部异常列表(网格数据随网格页激活懒加载)。
|
|
||||||
void setData(const geopro::controller::DatasetDetailController::ChartData& d);
|
|
||||||
|
|
||||||
// 网格数据到达(懒加载结果):建色彩服务 + ContourPlotItem,挂到 QwtPlot,更新色阶条/异常表。
|
// 网格数据到达(懒加载结果):建色彩服务 + ContourPlotItem,挂到 QwtPlot,更新色阶条/异常表。
|
||||||
void setGridData(const geopro::core::Grid& grid, const geopro::core::ColorScale& gridScale,
|
void setGridData(const geopro::core::Grid& grid, const geopro::core::ColorScale& gridScale,
|
||||||
const std::vector<geopro::core::Anomaly>& anoms);
|
const std::vector<geopro::core::Anomaly>& anoms);
|
||||||
|
|
||||||
|
// IDetailView:解包 ContourPayload(grid + 色阶 + 异常)→ setGridData。坏/空 variant 保持空态。
|
||||||
|
QWidget* widget() override { return this; }
|
||||||
|
void setPayload(const QVariant& payload) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void rebuildContour(); // 按当前显隐开关重建并重绘 ContourPlotItem
|
void rebuildContour(); // 按当前显隐开关重建并重绘 ContourPlotItem
|
||||||
|
|
||||||
geopro::controller::DatasetDetailController::ChartData data_;
|
|
||||||
|
|
||||||
QwtPlot* plot_ = nullptr;
|
QwtPlot* plot_ = nullptr;
|
||||||
QwtPlotRescaler* rescaler_ = nullptr;
|
QwtPlotRescaler* rescaler_ = nullptr;
|
||||||
ColorBarWidget* colorBar_ = nullptr;
|
ColorBarWidget* colorBar_ = nullptr;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 详情页签视图统一接口:壳/工厂只依赖它,渲染细节由具体视图自行解包载荷。
|
||||||
|
// widget() 返回承载的 QWidget(多为 this);setPayload 解包 QVariant 载荷并渲染。
|
||||||
|
class IDetailView {
|
||||||
|
public:
|
||||||
|
virtual ~IDetailView() = default;
|
||||||
|
virtual QWidget* widget() = 0;
|
||||||
|
virtual void setPayload(const QVariant& payload) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
#include "IDatasetChartStrategy.hpp" // geopro::controller
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// ERT 原始数据(measurement)策略:散点伪剖面(同步)+ 数据列表(懒加载)两页签。
|
||||||
|
struct MeasurementStrategy : controller::IDatasetChartStrategy {
|
||||||
|
std::string ddCode() const override { return "dd_ert_measurement_data"; }
|
||||||
|
std::vector<controller::TabSpec> tabs() const override {
|
||||||
|
return {
|
||||||
|
{QStringLiteral("散点图"), controller::ViewKind::Scatter,
|
||||||
|
QStringLiteral("ert_measurement.scatter"), /*lazy*/ false, /*paginated*/ false},
|
||||||
|
{QStringLiteral("数据列表"), controller::ViewKind::Table,
|
||||||
|
QStringLiteral("ert_measurement.rows"), /*lazy*/ true, /*paginated*/ false},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -5,9 +5,17 @@
|
||||||
#include "panels/chart/ScatterPlotItem.hpp"
|
#include "panels/chart/ScatterPlotItem.hpp"
|
||||||
|
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <QCursor>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
#include <QIcon>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QPen>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QPushButton>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
|
#include <QToolTip>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include <qwt_plot.h>
|
#include <qwt_plot.h>
|
||||||
|
|
@ -29,9 +37,11 @@ RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) {
|
||||||
auto* lay = new QVBoxLayout(this);
|
auto* lay = new QVBoxLayout(this);
|
||||||
lay->setContentsMargins(0, 0, 0, 0);
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
lay->setSpacing(0);
|
lay->setSpacing(0);
|
||||||
|
rootLay_ = lay;
|
||||||
|
|
||||||
// ---- 工具条 ----
|
// ---- 工具条(默认 = 反演原数据;measurement 到来时 buildMeasurementToolbar 替换)----
|
||||||
auto* toolbar = new QWidget(this);
|
auto* toolbar = new QWidget(this);
|
||||||
|
toolbar_ = toolbar;
|
||||||
auto* tbLay = new QHBoxLayout(toolbar);
|
auto* tbLay = new QHBoxLayout(toolbar);
|
||||||
tbLay->setContentsMargins(4, 4, 4, 4);
|
tbLay->setContentsMargins(4, 4, 4, 4);
|
||||||
tbLay->setSpacing(4);
|
tbLay->setSpacing(4);
|
||||||
|
|
@ -111,9 +121,21 @@ RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) {
|
||||||
rescaler_->setAspectRatio(1.0);
|
rescaler_->setAspectRatio(1.0);
|
||||||
rescaler_->setEnabled(true);
|
rescaler_->setEnabled(true);
|
||||||
|
|
||||||
lay->addWidget(plot_, 1);
|
// ---- 图表行:plot(stretch)+ 右侧竖向色阶条(measurement 用,默认隐藏)----
|
||||||
|
auto* plotRow = new QWidget(this);
|
||||||
|
auto* plotRowLay = new QHBoxLayout(plotRow);
|
||||||
|
plotRowLay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
plotRowLay->setSpacing(0);
|
||||||
|
plotRowLay->addWidget(plot_, 1);
|
||||||
|
|
||||||
// ---- 独立色阶条(固定高 36px,QwtPlot 的兄弟 widget)----
|
colorBarV_ = new ColorBarWidget(plotRow, ColorBarWidget::Orientation::Vertical);
|
||||||
|
colorBarV_->setObjectName(QStringLiteral("rawColorScaleBarV"));
|
||||||
|
colorBarV_->setVisible(false); // 默认(反演原数据)用底部横条
|
||||||
|
plotRowLay->addWidget(colorBarV_);
|
||||||
|
|
||||||
|
lay->addWidget(plotRow, 1);
|
||||||
|
|
||||||
|
// ---- 底部独立色阶条(固定高 36px,反演原数据用)----
|
||||||
colorBar_ = new ColorBarWidget(this);
|
colorBar_ = new ColorBarWidget(this);
|
||||||
colorBar_->setObjectName(QStringLiteral("rawColorScaleBar"));
|
colorBar_->setObjectName(QStringLiteral("rawColorScaleBar"));
|
||||||
lay->addWidget(colorBar_);
|
lay->addWidget(colorBar_);
|
||||||
|
|
@ -133,27 +155,280 @@ QWidget* RawDataChartView::plotArea() const {
|
||||||
return plot_;
|
return plot_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RawDataChartView::setData(
|
namespace {
|
||||||
const geopro::controller::DatasetDetailController::ChartData& d) {
|
|
||||||
data_ = d;
|
// 把一组 FieldOption 填进下拉,并按 defaultCode(或 defaultName 回退)选中默认项。
|
||||||
|
void fillCombo(QComboBox* combo, const std::vector<geopro::core::FieldOption>& opts,
|
||||||
|
const QString& defaultCode, const QString& defaultName) {
|
||||||
|
for (const auto& o : opts) {
|
||||||
|
// 用户可见名为 name;userData 存 fieldCode(重绘/识别用)。
|
||||||
|
combo->addItem(o.name, o.code);
|
||||||
|
}
|
||||||
|
// 默认选中:优先匹配 fieldCode,否则匹配 name(method 的 code 全为 null,用 name)。
|
||||||
|
int idx = defaultCode.isEmpty() ? -1 : combo->findData(defaultCode);
|
||||||
|
if (idx < 0 && !defaultName.isEmpty()) idx = combo->findText(defaultName);
|
||||||
|
if (idx >= 0) combo->setCurrentIndex(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 工具条占位图标(线性风格,2× 超采样 → HiDPI 清晰)────────────────────
|
||||||
|
// 原版用 Arco line icons(viewBox 0 0 48 48,stroke-width 4,stroke=currentColor)。
|
||||||
|
// 这里以 QPainter 画细线风格,逻辑边长 logical px(按钮 setIconSize 用同值),
|
||||||
|
// 像素 2× 渲染并 setDevicePixelRatio(2) 保证缩放下不糊。
|
||||||
|
constexpr int kToolIconPx = 16; // 逻辑图标边长(与 setIconSize 对齐)
|
||||||
|
constexpr qreal kToolIconScale = 2.0; // 超采样倍率(HiDPI 清晰)
|
||||||
|
|
||||||
|
QPixmap makeToolIconCanvas(QPainter& p) {
|
||||||
|
// 调用方在 [0,kToolIconPx] 逻辑坐标系下作画;返回前缩放 + 设 dpr。
|
||||||
|
const int dim = qRound(kToolIconPx * kToolIconScale);
|
||||||
|
QPixmap pm(dim, dim);
|
||||||
|
pm.fill(Qt::transparent);
|
||||||
|
p.begin(&pm);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
p.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||||
|
p.scale(kToolIconScale, kToolIconScale); // 之后用逻辑 px 坐标
|
||||||
|
return pm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 信息图标:细描边圆 + 内部 "i"(圆点 + 竖线),品牌蓝(两主题一致,对应原版小蓝圈-i)。
|
||||||
|
QIcon makeInfoIcon() {
|
||||||
|
const QColor accent = tokenColor("accent/primary");
|
||||||
|
QPainter p;
|
||||||
|
QPixmap pm = makeToolIconCanvas(p);
|
||||||
|
const qreal s = kToolIconPx;
|
||||||
|
QPen pen(accent, 1.4);
|
||||||
|
pen.setCapStyle(Qt::RoundCap);
|
||||||
|
p.setPen(pen);
|
||||||
|
p.setBrush(Qt::NoBrush);
|
||||||
|
// 外圈(留 1.5px 边距,避免描边被裁)。
|
||||||
|
const qreal m = 1.5;
|
||||||
|
p.drawEllipse(QRectF(m, m, s - 2 * m, s - 2 * m));
|
||||||
|
// "i":上点 + 下竖线(居中)。
|
||||||
|
const qreal cx = s / 2.0;
|
||||||
|
p.setBrush(accent);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.drawEllipse(QPointF(cx, s * 0.34), 0.9, 0.9); // 点
|
||||||
|
QPen stem(accent, 1.4);
|
||||||
|
stem.setCapStyle(Qt::RoundCap);
|
||||||
|
p.setPen(stem);
|
||||||
|
p.drawLine(QPointF(cx, s * 0.46), QPointF(cx, s * 0.70)); // 竖
|
||||||
|
p.end();
|
||||||
|
pm.setDevicePixelRatio(kToolIconScale);
|
||||||
|
return QIcon(pm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 框选图标:虚线方框(选区)+ 右下角小箭头光标。描边随主题(次要文本色,两主题可见)。
|
||||||
|
QIcon makeMarqueeIcon() {
|
||||||
|
const QColor stroke = tokenColor("text/secondary");
|
||||||
|
QPainter p;
|
||||||
|
QPixmap pm = makeToolIconCanvas(p);
|
||||||
|
const qreal s = kToolIconPx;
|
||||||
|
// 虚线选区框(左上偏移,给右下角箭头让位)。
|
||||||
|
QPen dash(stroke, 1.3);
|
||||||
|
dash.setCapStyle(Qt::FlatCap);
|
||||||
|
dash.setJoinStyle(Qt::MiterJoin);
|
||||||
|
QVector<qreal> pattern{2.0, 1.6};
|
||||||
|
dash.setDashPattern(pattern);
|
||||||
|
p.setPen(dash);
|
||||||
|
p.setBrush(Qt::NoBrush);
|
||||||
|
p.drawRect(QRectF(2.0, 2.0, s - 6.0, s - 6.0));
|
||||||
|
// 右下角小箭头光标(实线填充,指向右下)。
|
||||||
|
QPainterPath cur;
|
||||||
|
const qreal ax = s - 5.0, ay = s - 5.0;
|
||||||
|
cur.moveTo(ax, ay);
|
||||||
|
cur.lineTo(ax + 4.6, ay + 1.8);
|
||||||
|
cur.lineTo(ax + 1.9, ay + 2.6);
|
||||||
|
cur.lineTo(ax + 1.1, ay + 4.9);
|
||||||
|
cur.closeSubpath();
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(stroke);
|
||||||
|
p.drawPath(cur);
|
||||||
|
p.end();
|
||||||
|
pm.setDevicePixelRatio(kToolIconScale);
|
||||||
|
return QIcon(pm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 把占位 QToolButton 配成图标按钮:清文字、设图标 + 固定尺寸(与工具条其它按钮一致高度)。
|
||||||
|
void styleToolIconButton(QToolButton* btn, const QIcon& icon) {
|
||||||
|
btn->setText(QString());
|
||||||
|
btn->setIcon(icon);
|
||||||
|
btn->setIconSize(QSize(kToolIconPx, kToolIconPx));
|
||||||
|
btn->setAutoRaise(true);
|
||||||
|
btn->setFixedSize(QSize(28, 28)); // 与下拉/按钮行高协调;不再过小或锯齿
|
||||||
|
btn->setCursor(Qt::PointingHandCursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void RawDataChartView::showNotImplemented(QWidget* anchor) {
|
||||||
|
// 轻提示:占位按钮/下拉点击 → 暂未实现(不阻塞,不弹窗)。
|
||||||
|
const QPoint pos = anchor ? anchor->mapToGlobal(QPoint(0, anchor->height()))
|
||||||
|
: QCursor::pos();
|
||||||
|
QToolTip::showText(pos, QStringLiteral("暂未实现"), anchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RawDataChartView::replotForAxis() {
|
||||||
|
// 本地换 x/y(无网络):按下拉 fieldCode 从备选列取数据,重设 scatter.x/.y 并重绘。
|
||||||
|
if (!xCombo_ || !yCombo_ || !scatterItem_) return;
|
||||||
|
const QString xCode = xCombo_->currentData().toString();
|
||||||
|
const QString yCode = yCombo_->currentData().toString();
|
||||||
|
|
||||||
|
if (xCode == QStringLiteral("horizontalDistance") && !data_.altXHorizontal.empty())
|
||||||
|
data_.scatter.x = data_.altXHorizontal;
|
||||||
|
else if (!data_.altXSlope.empty())
|
||||||
|
data_.scatter.x = data_.altXSlope; // 默认/斜距
|
||||||
|
|
||||||
|
if (yCode == QStringLiteral("elevationPseudoDepth") && !data_.altYElevationPseudo.empty())
|
||||||
|
data_.scatter.y = data_.altYElevationPseudo;
|
||||||
|
else if (yCode == QStringLiteral("pseudoDepth") && !data_.altYPseudo.empty())
|
||||||
|
data_.scatter.y = data_.altYPseudo;
|
||||||
|
// 层数(Layer No):数据为 null → 不改轴(保持当前),选项可选但 no-op。
|
||||||
|
|
||||||
|
scatterItem_->setData(data_.scatter, colorSvc_);
|
||||||
|
if (hoverTip_) hoverTip_->setField(&data_.scatter);
|
||||||
|
QRectF bbox = scatterItem_->boundingRect();
|
||||||
|
if (!bbox.isEmpty()) {
|
||||||
|
plot_->setAxisScale(QwtPlot::xTop, bbox.left(), bbox.right());
|
||||||
|
plot_->setAxisScale(QwtPlot::yLeft, bbox.top(), bbox.bottom());
|
||||||
|
}
|
||||||
|
plot_->updateAxes();
|
||||||
|
if (rescaler_) rescaler_->rescale();
|
||||||
|
plot_->replot();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RawDataChartView::buildMeasurementToolbar(const geopro::core::ScatterToolbarConf& conf) {
|
||||||
|
auto* toolbar = new QWidget(this);
|
||||||
|
auto* tbLay = new QHBoxLayout(toolbar);
|
||||||
|
tbLay->setContentsMargins(4, 4, 4, 4);
|
||||||
|
tbLay->setSpacing(4);
|
||||||
|
|
||||||
|
// [i] info + [▣] 框选:占位(暂未实现)。用 QPainter 画的线性图标(HiDPI 清晰,随主题)。
|
||||||
|
auto* btnInfo = new QToolButton(toolbar);
|
||||||
|
btnInfo->setToolTip(QStringLiteral("信息"));
|
||||||
|
styleToolIconButton(btnInfo, makeInfoIcon());
|
||||||
|
connect(btnInfo, &QToolButton::clicked, this, [this, btnInfo]() { showNotImplemented(btnInfo); });
|
||||||
|
auto* btnMarquee = new QToolButton(toolbar);
|
||||||
|
btnMarquee->setToolTip(QStringLiteral("框选"));
|
||||||
|
styleToolIconButton(btnMarquee, makeMarqueeIcon());
|
||||||
|
connect(btnMarquee, &QToolButton::clicked, this, [this, btnMarquee]() { showNotImplemented(btnMarquee); });
|
||||||
|
// 主题热切:重绘图标(info 锚定品牌蓝,marquee 描边随次要文本色)。
|
||||||
|
connect(&ThemeManager::instance(), &ThemeManager::changed, btnInfo,
|
||||||
|
[btnInfo]() { btnInfo->setIcon(makeInfoIcon()); });
|
||||||
|
connect(&ThemeManager::instance(), &ThemeManager::changed, btnMarquee,
|
||||||
|
[btnMarquee]() { btnMarquee->setIcon(makeMarqueeIcon()); });
|
||||||
|
|
||||||
|
// 显示 / 隐藏:功能性——切换全部数据方块可见性(电极保留)。
|
||||||
|
auto* btnShow = new QPushButton(QStringLiteral("显示"), toolbar);
|
||||||
|
auto* btnHide = new QPushButton(QStringLiteral("隐藏"), toolbar);
|
||||||
|
connect(btnShow, &QPushButton::clicked, this, [this]() {
|
||||||
|
if (scatterItem_) { scatterItem_->setScatterVisible(true); plot_->replot(); }
|
||||||
|
});
|
||||||
|
connect(btnHide, &QPushButton::clicked, this, [this]() {
|
||||||
|
if (scatterItem_) { scatterItem_->setScatterVisible(false); plot_->replot(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据过滤:占位(暂未实现)。
|
||||||
|
auto* btnFilter = new QPushButton(QStringLiteral("数据过滤"), toolbar);
|
||||||
|
connect(btnFilter, &QPushButton::clicked, this, [this, btnFilter]() { showNotImplemented(btnFilter); });
|
||||||
|
|
||||||
|
// x / y 下拉:功能性(本地换列重绘);v / method 下拉:视觉占位(选不同 v/method 提示暂未实现)。
|
||||||
|
xCombo_ = new QComboBox(toolbar);
|
||||||
|
fillCombo(xCombo_, conf.x, conf.defaultX, QString());
|
||||||
|
yCombo_ = new QComboBox(toolbar);
|
||||||
|
fillCombo(yCombo_, conf.y, conf.defaultY, QString());
|
||||||
|
auto* vCombo = new QComboBox(toolbar);
|
||||||
|
fillCombo(vCombo, conf.v, conf.defaultV, QString());
|
||||||
|
auto* methodCombo = new QComboBox(toolbar);
|
||||||
|
fillCombo(methodCombo, conf.method, QString(), conf.defaultMethod);
|
||||||
|
|
||||||
|
connect(xCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||||
|
[this](int) { replotForAxis(); });
|
||||||
|
connect(yCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||||
|
[this](int) { replotForAxis(); });
|
||||||
|
// v / method:换选项 → 暂未实现(需重新请求散点/色阶,属重交互,本轮不做)。
|
||||||
|
connect(vCombo, QOverload<int>::of(&QComboBox::activated), this,
|
||||||
|
[this, vCombo](int) { showNotImplemented(vCombo); });
|
||||||
|
connect(methodCombo, QOverload<int>::of(&QComboBox::activated), this,
|
||||||
|
[this, methodCombo](int) { showNotImplemented(methodCombo); });
|
||||||
|
|
||||||
|
// 色阶配置:占位(暂未实现)。
|
||||||
|
auto* btnColorScale = new QPushButton(QStringLiteral("色阶配置"), toolbar);
|
||||||
|
connect(btnColorScale, &QPushButton::clicked, this, [this, btnColorScale]() { showNotImplemented(btnColorScale); });
|
||||||
|
|
||||||
|
// 右侧主操作(蓝色):生成视电阻率数据 / 反演运算 / 另存为 —— 占位(暂未实现)。
|
||||||
|
auto* btnGen = new QPushButton(QStringLiteral("生成视电阻率数据"), toolbar);
|
||||||
|
auto* btnInvert = new QPushButton(QStringLiteral("反演运算"), toolbar);
|
||||||
|
auto* btnSaveAs = new QPushButton(QStringLiteral("另存为"), toolbar);
|
||||||
|
for (auto* b : {btnGen, btnInvert, btnSaveAs}) {
|
||||||
|
b->setObjectName(QStringLiteral("primaryBtn")); // 蓝色主按钮(下方 QSS)
|
||||||
|
connect(b, &QPushButton::clicked, this, [this, b]() { showNotImplemented(b); });
|
||||||
|
}
|
||||||
|
|
||||||
|
tbLay->addWidget(btnInfo);
|
||||||
|
tbLay->addWidget(btnMarquee);
|
||||||
|
tbLay->addWidget(btnShow);
|
||||||
|
tbLay->addWidget(btnHide);
|
||||||
|
tbLay->addWidget(btnFilter);
|
||||||
|
tbLay->addWidget(xCombo_);
|
||||||
|
tbLay->addWidget(yCombo_);
|
||||||
|
tbLay->addWidget(vCombo);
|
||||||
|
tbLay->addWidget(methodCombo);
|
||||||
|
tbLay->addWidget(btnColorScale);
|
||||||
|
tbLay->addStretch(); // 把主操作推到右侧
|
||||||
|
tbLay->addWidget(btnGen);
|
||||||
|
tbLay->addWidget(btnInvert);
|
||||||
|
tbLay->addWidget(btnSaveAs);
|
||||||
|
|
||||||
|
// 蓝色主按钮样式(随主题;普通按钮/下拉走全局 QSS 已支持明暗)。
|
||||||
|
applyTokenizedStyleSheet(
|
||||||
|
toolbar,
|
||||||
|
QStringLiteral(
|
||||||
|
"QPushButton#primaryBtn { background: {{accent/primary}}; color: {{text/on-primary}};"
|
||||||
|
" border: 1px solid {{accent/primary}}; border-radius: 6px; padding: 6px 14px; }"
|
||||||
|
"QPushButton#primaryBtn:hover { background: {{accent/primary-hover}}; border-color: {{accent/primary-hover}}; }"
|
||||||
|
"QPushButton#primaryBtn:pressed { background: {{accent/primary-pressed}}; }"));
|
||||||
|
|
||||||
|
// 替换 ctor 建的 inversion 工具条(同一顶层布局首位)。
|
||||||
|
rootLay_->replaceWidget(toolbar_, toolbar);
|
||||||
|
toolbar_->deleteLater();
|
||||||
|
toolbar_ = toolbar;
|
||||||
|
chartTypeCombo_ = nullptr; // 已随旧工具条移除
|
||||||
|
measurementToolbar_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RawDataChartView::setPayload(const QVariant& payload) {
|
||||||
|
if (!payload.canConvert<geopro::core::ScatterPayload>()) {
|
||||||
|
// 坏/空 variant:保持空态(不渲染、不崩)。E2+ 可在此显式提示「渲染数据格式错误」。
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setData(payload.value<geopro::core::ScatterPayload>());
|
||||||
|
}
|
||||||
|
|
||||||
|
void RawDataChartView::setData(const geopro::core::ScatterPayload& p) {
|
||||||
|
data_ = p;
|
||||||
if (hoverTip_) hoverTip_->setField(&data_.scatter); // 显式重绑(地址稳定,消除隐式依赖)
|
if (hoverTip_) hoverTip_->setField(&data_.scatter); // 显式重绑(地址稳定,消除隐式依赖)
|
||||||
|
|
||||||
if (d.scatterScale.empty()) return;
|
// measurement 载荷(toolbar 非空):首次到来时建并替换工具条(视觉 1:1)。反演留空 → 不动。
|
||||||
|
if (!p.toolbar.empty() && !measurementToolbar_) buildMeasurementToolbar(p.toolbar);
|
||||||
|
|
||||||
|
if (p.scale.empty()) return;
|
||||||
|
|
||||||
// 重建 ColorMapService(旧的 scatterItem 已被 QwtPlot detach/delete)
|
// 重建 ColorMapService(旧的 scatterItem 已被 QwtPlot detach/delete)
|
||||||
delete colorSvc_;
|
delete colorSvc_;
|
||||||
colorSvc_ = new ColorMapService(d.scatterScale);
|
colorSvc_ = new ColorMapService(p.scale);
|
||||||
|
|
||||||
// 散点颜色归一化对齐原版 Plotly(cmin/cmax 未设 → cauto=数据 min/max):
|
// 散点颜色归一化对齐原版 Plotly(cmin/cmax 未设 → cauto=数据 min/max),按 vlist 有限值
|
||||||
// 按 vlist 有限值的 min/max 设数据范围,使整段色阶铺满数据实际范围(而非压进 colorBar 全程一小段)。
|
// 的 min/max 设数据范围,使整段色阶铺满数据实际范围。measurement 与反演原数据同此路径:
|
||||||
|
// v 含负异常值(如 -1066),故 cmin<0,中段视电阻率归一化到色阶中部(深品红/紫)。
|
||||||
|
{
|
||||||
double vMin = std::numeric_limits<double>::max();
|
double vMin = std::numeric_limits<double>::max();
|
||||||
double vMax = std::numeric_limits<double>::lowest();
|
double vMax = std::numeric_limits<double>::lowest();
|
||||||
for (double v : d.scatter.v) {
|
for (double v : p.scatter.v) {
|
||||||
if (!std::isfinite(v)) continue; // 跳过 NaN/±Inf(脏数据),否则数据范围被污染→全图 NaN 取色
|
if (!std::isfinite(v)) continue; // 跳过 NaN/±Inf(脏数据),否则数据范围被污染→全图 NaN 取色
|
||||||
if (v < vMin) vMin = v;
|
if (v < vMin) vMin = v;
|
||||||
if (v > vMax) vMax = v;
|
if (v > vMax) vMax = v;
|
||||||
}
|
}
|
||||||
if (vMin <= vMax) colorSvc_->setDataRange(vMin, vMax);
|
if (vMin <= vMax) colorSvc_->setDataRange(vMin, vMax);
|
||||||
|
}
|
||||||
|
|
||||||
// 卸载旧散点项:QwtPlot 默认 autoDelete=true(析构时 delete 仍在 dict 的 item)。
|
// 卸载旧散点项:QwtPlot 默认 autoDelete=true(析构时 delete 仍在 dict 的 item)。
|
||||||
// 必须先 detach()(从 dict 移除)再 delete,否则 QwtPlot 析构时会 double-free。
|
// 必须先 detach()(从 dict 移除)再 delete,否则 QwtPlot 析构时会 double-free。
|
||||||
|
|
@ -165,7 +440,7 @@ void RawDataChartView::setData(
|
||||||
|
|
||||||
// 新建散点项并挂到 plot
|
// 新建散点项并挂到 plot
|
||||||
scatterItem_ = new ScatterPlotItem();
|
scatterItem_ = new ScatterPlotItem();
|
||||||
scatterItem_->setData(d.scatter, colorSvc_);
|
scatterItem_->setData(p.scatter, colorSvc_);
|
||||||
scatterItem_->attach(plot_);
|
scatterItem_->attach(plot_);
|
||||||
|
|
||||||
// 按数据包围盒设置轴范围
|
// 按数据包围盒设置轴范围
|
||||||
|
|
@ -178,8 +453,16 @@ void RawDataChartView::setData(
|
||||||
if (rescaler_) rescaler_->rescale(); // 应用真实比尺
|
if (rescaler_) rescaler_->rescale(); // 应用真实比尺
|
||||||
plot_->replot();
|
plot_->replot();
|
||||||
|
|
||||||
// 更新色阶条
|
// 更新色阶条:measurement 用右侧竖条,反演原数据用底部横条(二选一显示)。
|
||||||
colorBar_->setColorScale(d.scatterScale);
|
if (p.verticalLegend) {
|
||||||
|
colorBarV_->setColorScale(p.scale);
|
||||||
|
colorBarV_->setVisible(true);
|
||||||
|
colorBar_->setVisible(false);
|
||||||
|
} else {
|
||||||
|
colorBar_->setColorScale(p.scale);
|
||||||
|
colorBar_->setVisible(true);
|
||||||
|
colorBarV_->setVisible(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include "DatasetDetailController.hpp"
|
#include "model/detail/DetailPayloads.hpp"
|
||||||
#include "panels/chart/ColorMapService.hpp"
|
#include "panels/chart/ColorMapService.hpp"
|
||||||
|
#include "panels/chart/IDetailView.hpp"
|
||||||
|
|
||||||
class QComboBox;
|
class QComboBox;
|
||||||
|
class QVBoxLayout;
|
||||||
class QwtPlot;
|
class QwtPlot;
|
||||||
class QwtPlotRescaler;
|
class QwtPlotRescaler;
|
||||||
|
|
||||||
|
|
@ -14,23 +16,43 @@ class ScatterPlotItem;
|
||||||
class ScatterHoverTip;
|
class ScatterHoverTip;
|
||||||
|
|
||||||
// 原数据图表视图:工具条 + QwtPlot(x 轴顶部、Panner/Magnifier)+ 独立色阶条。
|
// 原数据图表视图:工具条 + QwtPlot(x 轴顶部、Panner/Magnifier)+ 独立色阶条。
|
||||||
class RawDataChartView : public QWidget {
|
class RawDataChartView : public QWidget, public IDetailView {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit RawDataChartView(QWidget* parent = nullptr);
|
explicit RawDataChartView(QWidget* parent = nullptr);
|
||||||
~RawDataChartView() override;
|
~RawDataChartView() override;
|
||||||
|
|
||||||
void setData(const geopro::controller::DatasetDetailController::ChartData& d);
|
// 渲染散点载荷(解包后调内部渲染;渲染代码不变)。
|
||||||
|
void setData(const geopro::core::ScatterPayload& p);
|
||||||
|
|
||||||
|
// IDetailView:壳/工厂统一入口。坏/空 variant → 保持空态不崩。
|
||||||
|
QWidget* widget() override { return this; }
|
||||||
|
void setPayload(const QVariant& payload) override;
|
||||||
|
|
||||||
// 供外部访问(已不再是占位,保留兼容接口返回 plot_)
|
// 供外部访问(已不再是占位,保留兼容接口返回 plot_)
|
||||||
QWidget* plotArea() const;
|
QWidget* plotArea() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
geopro::controller::DatasetDetailController::ChartData data_;
|
// 工具条按载荷类型二选一:反演原数据 = ctor 默认建的 inversion 工具条;measurement =
|
||||||
|
// 首个非空 ScatterToolbarConf 到来时建一次并替换(视觉 1:1)。建好后缓存,后续 setData 复用。
|
||||||
|
void buildMeasurementToolbar(const geopro::core::ScatterToolbarConf& conf);
|
||||||
|
// 按当前 x/y 下拉选择,从备选列重绘散点(本地,无网络)。code 为下拉项 fieldCode。
|
||||||
|
void replotForAxis();
|
||||||
|
// “暂未实现”轻提示(占位按钮/下拉点击)。
|
||||||
|
void showNotImplemented(QWidget* anchor);
|
||||||
|
|
||||||
|
geopro::core::ScatterPayload data_;
|
||||||
QwtPlot* plot_;
|
QwtPlot* plot_;
|
||||||
QwtPlotRescaler* rescaler_ = nullptr; // 锁定 x:y 真实比尺
|
QwtPlotRescaler* rescaler_ = nullptr; // 锁定 x:y 真实比尺
|
||||||
ColorBarWidget* colorBar_;
|
ColorBarWidget* colorBar_; // 底部横条(反演原数据)
|
||||||
QComboBox* chartTypeCombo_;
|
ColorBarWidget* colorBarV_; // 右侧竖条(measurement)
|
||||||
|
QComboBox* chartTypeCombo_ = nullptr;
|
||||||
|
|
||||||
|
QVBoxLayout* rootLay_ = nullptr; // 顶层竖向布局(用于替换工具条)
|
||||||
|
QWidget* toolbar_ = nullptr; // 当前工具条(inversion 或 measurement)
|
||||||
|
bool measurementToolbar_ = false; // 已建 measurement 工具条
|
||||||
|
QComboBox* xCombo_ = nullptr; // measurement x 下拉
|
||||||
|
QComboBox* yCombo_ = nullptr; // measurement y 下拉
|
||||||
|
|
||||||
// 使用 unique_ptr 管理生命周期;attach 后 QwtPlot 接管绘制,但我们仍持有指针
|
// 使用 unique_ptr 管理生命周期;attach 后 QwtPlot 接管绘制,但我们仍持有指针
|
||||||
ColorMapService* colorSvc_ = nullptr; // heap,由 setData 重建
|
ColorMapService* colorSvc_ = nullptr; // heap,由 setData 重建
|
||||||
|
|
|
||||||
|
|
@ -38,15 +38,44 @@ bool ScatterHoverTip::eventFilter(QObject* obj, QEvent* ev) {
|
||||||
const auto& ys = field_->y;
|
const auto& ys = field_->y;
|
||||||
const auto& vs = field_->v;
|
const auto& vs = field_->v;
|
||||||
const std::size_t n = std::min(xs.size(), ys.size());
|
const std::size_t n = std::min(xs.size(), ys.size());
|
||||||
if (n == 0) {
|
|
||||||
QToolTip::hideText();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在像素空间找最近散点(与 ScatterPlotItem 同用 canvasMap)。
|
// 在像素空间找最近散点(与 ScatterPlotItem 同用 canvasMap)。
|
||||||
const QwtScaleMap xMap = plot_->canvasMap(xAxis_);
|
const QwtScaleMap xMap = plot_->canvasMap(xAxis_);
|
||||||
const QwtScaleMap yMap = plot_->canvasMap(yAxis_);
|
const QwtScaleMap yMap = plot_->canvasMap(yAxis_);
|
||||||
const QPointF mp = me->position();
|
const QPointF mp = me->position();
|
||||||
|
|
||||||
|
// 电极 hover(y=0 行,与数据点 hover 分开):电极 marker 略大于数据方块,
|
||||||
|
// 命中半径稍大。命中则显示 x/y/num 浮动框(measurement 专有,反演 electrodeX 空→跳过)。
|
||||||
|
{
|
||||||
|
const auto& ex = field_->electrodeX;
|
||||||
|
const auto& eno = field_->electrodeNo;
|
||||||
|
const double ey0 = yMap.transform(0.0);
|
||||||
|
double eBestD2 = std::numeric_limits<double>::max();
|
||||||
|
std::size_t eBestI = 0;
|
||||||
|
for (std::size_t i = 0; i < ex.size(); ++i) {
|
||||||
|
const double dx = xMap.transform(ex[i]) - mp.x();
|
||||||
|
const double dy = ey0 - mp.y();
|
||||||
|
const double d2 = dx * dx + dy * dy;
|
||||||
|
if (d2 < eBestD2) {
|
||||||
|
eBestD2 = d2;
|
||||||
|
eBestI = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (eBestD2 <= kElectrodeHitRadiusPx * kElectrodeHitRadiusPx) {
|
||||||
|
const int num = (eBestI < eno.size())
|
||||||
|
? static_cast<int>(eno[eBestI])
|
||||||
|
: static_cast<int>(eBestI) + 1;
|
||||||
|
QToolTip::showText(me->globalPosition().toPoint(),
|
||||||
|
electrodeHoverText(ex[eBestI], num), plot_->canvas());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n == 0) {
|
||||||
|
QToolTip::hideText();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
double bestD2 = std::numeric_limits<double>::max();
|
double bestD2 = std::numeric_limits<double>::max();
|
||||||
std::size_t bestI = 0;
|
std::size_t bestI = 0;
|
||||||
for (std::size_t i = 0; i < n; ++i) {
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
|
@ -61,8 +90,17 @@ bool ScatterHoverTip::eventFilter(QObject* obj, QEvent* ev) {
|
||||||
|
|
||||||
if (bestD2 <= kHitRadiusPx * kHitRadiusPx) {
|
if (bestD2 <= kHitRadiusPx * kHitRadiusPx) {
|
||||||
const double v = (bestI < vs.size()) ? vs[bestI] : 0.0;
|
const double v = (bestI < vs.size()) ? vs[bestI] : 0.0;
|
||||||
QToolTip::showText(me->globalPosition().toPoint(),
|
// 有 A/B/M/N 元数据(measurement)→ 浮动框含 X/Y/Value/a/b/m/n;否则回退 X/Y/值(反演原数据)。
|
||||||
scatterHoverText(xs[bestI], ys[bestI], v), plot_->canvas());
|
const auto& a = field_->a;
|
||||||
|
QString text;
|
||||||
|
if (bestI < a.size() && bestI < field_->b.size() && bestI < field_->m.size() &&
|
||||||
|
bestI < field_->n.size()) {
|
||||||
|
text = measurementHoverText(xs[bestI], ys[bestI], v, a[bestI], field_->b[bestI],
|
||||||
|
field_->m[bestI], field_->n[bestI]);
|
||||||
|
} else {
|
||||||
|
text = scatterHoverText(xs[bestI], ys[bestI], v);
|
||||||
|
}
|
||||||
|
QToolTip::showText(me->globalPosition().toPoint(), text, plot_->canvas());
|
||||||
} else {
|
} else {
|
||||||
QToolTip::hideText();
|
QToolTip::hideText();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,31 @@ inline QString scatterHoverText(double x, double y, double v) {
|
||||||
.arg(v, 0, 'f', 3);
|
.arg(v, 0, 'f', 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// measurement 散点 hover 浮动框:对齐原版 Plotly 浮动框多行
|
||||||
|
// X / Y / Value(视电阻率 col16) / a / b / m / n。
|
||||||
|
inline QString measurementHoverText(double x, double y, double v,
|
||||||
|
double a, double b, double m, double n) {
|
||||||
|
return QStringLiteral(
|
||||||
|
"<b>X:</b> %1<br><b>Y:</b> %2<br><b>Value:</b> %3"
|
||||||
|
"<br><b>a:</b> %4<br><b>b:</b> %5<br><b>m:</b> %6<br><b>n:</b> %7")
|
||||||
|
.arg(x, 0, 'f', 3)
|
||||||
|
.arg(y, 0, 'f', 3)
|
||||||
|
.arg(v, 0, 'f', 3)
|
||||||
|
.arg(a, 0, 'g', 6)
|
||||||
|
.arg(b, 0, 'g', 6)
|
||||||
|
.arg(m, 0, 'g', 6)
|
||||||
|
.arg(n, 0, 'g', 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 电极 hover 浮动框:对齐原版 Plotly 电极 trace 浮动框(与数据点 hover 分开),
|
||||||
|
// x: {slopeDistance}<br>y: 0<br>num: {electrodeNo}
|
||||||
|
// x 用 6 位有效数字(对齐原版,如 5.123837),num 为电极编号(1-based 整数)。
|
||||||
|
inline QString electrodeHoverText(double x, int num) {
|
||||||
|
return QStringLiteral("x: %1<br>y: 0<br>num: %2")
|
||||||
|
.arg(x, 0, 'g', 7)
|
||||||
|
.arg(num);
|
||||||
|
}
|
||||||
|
|
||||||
// 散点 hover 提示:监听画布鼠标移动(无按键时),找最近散点,
|
// 散点 hover 提示:监听画布鼠标移动(无按键时),找最近散点,
|
||||||
// 命中半径内用 QToolTip 显示 X/Y/值(对齐原版 Plotly 悬浮)。
|
// 命中半径内用 QToolTip 显示 X/Y/值(对齐原版 Plotly 悬浮)。
|
||||||
// 不消费事件,与 LivePanner(左键平移/滚轮缩放)共存。
|
// 不消费事件,与 LivePanner(左键平移/滚轮缩放)共存。
|
||||||
|
|
@ -38,6 +63,7 @@ private:
|
||||||
const core::ScatterField* field_ = nullptr;
|
const core::ScatterField* field_ = nullptr;
|
||||||
|
|
||||||
static constexpr double kHitRadiusPx = 6.0; // 命中半径(像素),对齐方块 marker 尺寸
|
static constexpr double kHitRadiusPx = 6.0; // 命中半径(像素),对齐方块 marker 尺寸
|
||||||
|
static constexpr double kElectrodeHitRadiusPx = 7.0; // 电极菱形略大,命中半径稍大
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
#include "panels/chart/ScatterPlotItem.hpp"
|
#include "panels/chart/ScatterPlotItem.hpp"
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
#include <QPolygonF>
|
||||||
#include <qwt_plot.h>
|
#include <qwt_plot.h>
|
||||||
#include <qwt_scale_map.h>
|
#include <qwt_scale_map.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
@ -30,6 +32,9 @@ void ScatterPlotItem::setData(const core::ScatterField& field, ColorMapService*
|
||||||
double xMax = *std::max_element(field_.x.begin(), field_.x.end());
|
double xMax = *std::max_element(field_.x.begin(), field_.x.end());
|
||||||
double yMin = *std::min_element(field_.y.begin(), field_.y.end());
|
double yMin = *std::min_element(field_.y.begin(), field_.y.end());
|
||||||
double yMax = *std::max_element(field_.y.begin(), field_.y.end());
|
double yMax = *std::max_element(field_.y.begin(), field_.y.end());
|
||||||
|
// 电极 marker 在 y=0、x=electrodeX 处:纳入包围盒,避免电极被截。
|
||||||
|
for (double ex : field_.electrodeX) { xMin = std::min(xMin, ex); xMax = std::max(xMax, ex); }
|
||||||
|
if (!field_.electrodeX.empty()) { yMin = std::min(yMin, 0.0); yMax = std::max(yMax, 0.0); }
|
||||||
// 加少量 margin,避免边界点被截
|
// 加少量 margin,避免边界点被截
|
||||||
double xM = (xMax - xMin) * 0.03 + 0.1;
|
double xM = (xMax - xMin) * 0.03 + 0.1;
|
||||||
double yM = (yMax - yMin) * 0.03 + 0.1;
|
double yM = (yMax - yMin) * 0.03 + 0.1;
|
||||||
|
|
@ -56,12 +61,35 @@ void ScatterPlotItem::draw(QPainter* painter,
|
||||||
|
|
||||||
painter->save();
|
painter->save();
|
||||||
painter->setRenderHint(QPainter::Antialiasing, false);
|
painter->setRenderHint(QPainter::Antialiasing, false);
|
||||||
painter->setPen(QPen(Qt::white, kPenWidth));
|
|
||||||
|
|
||||||
|
// 电极位置:y=0 处灰色菱形(先画,置于散点下层)。
|
||||||
|
// 原版 Plotly 电极 marker:实心 #BEBEBE(190,190,190) 填充 + 白色 2px 描边,
|
||||||
|
// 菱形比数据方块略大(size 16 vs 12)。
|
||||||
|
if (!field_.electrodeX.empty()) {
|
||||||
|
const double y0 = yMap.transform(0.0);
|
||||||
|
painter->setPen(QPen(Qt::white, kElectrodePenWidth));
|
||||||
|
painter->setBrush(QColor(190, 190, 190));
|
||||||
|
const double h = kElectrodeHalfSide;
|
||||||
|
for (double ex : field_.electrodeX) {
|
||||||
|
const double px = xMap.transform(ex);
|
||||||
|
QPolygonF dia;
|
||||||
|
dia << QPointF(px, y0 - h) << QPointF(px + h, y0)
|
||||||
|
<< QPointF(px, y0 + h) << QPointF(px - h, y0);
|
||||||
|
painter->drawPolygon(dia);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏数据方块:电极已绘,直接收尾(保留电极菱形)。
|
||||||
|
if (!scatterVisible_) { painter->restore(); return; }
|
||||||
|
|
||||||
|
painter->setPen(QPen(Qt::white, kPenWidth));
|
||||||
for (std::size_t i = 0; i < n; ++i) {
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
double px = xMap.transform(xs[i]);
|
double px = xMap.transform(xs[i]);
|
||||||
double py = yMap.transform(ys[i]);
|
double py = yMap.transform(ys[i]);
|
||||||
double val = (i < vs.size()) ? vs[i] : 0.0;
|
double val = (i < vs.size()) ? vs[i] : 0.0;
|
||||||
|
// 非有限值(NaN/±Inf,可能来自降级后端的脏数据):跳过该点,不绘制。
|
||||||
|
// 与 RawDataChartView 的 setDataRange 的 isfinite 跳过一致。
|
||||||
|
if (!std::isfinite(val)) continue;
|
||||||
auto c = colorSvc_->colorAtContinuous(val);
|
auto c = colorSvc_->colorAtContinuous(val);
|
||||||
painter->setBrush(QColor(c.r, c.g, c.b, c.a));
|
painter->setBrush(QColor(c.r, c.g, c.b, c.a));
|
||||||
painter->drawRect(QRectF(px - kHalfSide, py - kHalfSide,
|
painter->drawRect(QRectF(px - kHalfSide, py - kHalfSide,
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,17 @@ namespace geopro::app {
|
||||||
|
|
||||||
// QwtPlotItem:把 ScatterField 数据渲染为彩色方块散点。
|
// QwtPlotItem:把 ScatterField 数据渲染为彩色方块散点。
|
||||||
// 每个点用固定像素边长方块绘制(不随缩放变大),白色描边,
|
// 每个点用固定像素边长方块绘制(不随缩放变大),白色描边,
|
||||||
// 颜色由 ColorMapService 连续插值决定(与原版 Plotly 一致)。
|
// 颜色由 ColorMapService 连续插值决定(cauto,与原版 Plotly 一致;measurement/反演同路径)。
|
||||||
class ScatterPlotItem : public QwtPlotItem {
|
class ScatterPlotItem : public QwtPlotItem {
|
||||||
public:
|
public:
|
||||||
ScatterPlotItem();
|
ScatterPlotItem();
|
||||||
|
|
||||||
|
// 按 ColorMapService 连续插值上色(cauto);数据范围由调用方在 svc 上 setDataRange。
|
||||||
void setData(const core::ScatterField& field, ColorMapService* svc);
|
void setData(const core::ScatterField& field, ColorMapService* svc);
|
||||||
|
|
||||||
|
// 显示/隐藏数据方块(measurement 工具条“显示/隐藏”)。电极菱形不受影响,始终绘制。
|
||||||
|
void setScatterVisible(bool on) { scatterVisible_ = on; }
|
||||||
|
|
||||||
int rtti() const override { return QwtPlotItem::Rtti_PlotUserItem; }
|
int rtti() const override { return QwtPlotItem::Rtti_PlotUserItem; }
|
||||||
|
|
||||||
QRectF boundingRect() const override;
|
QRectF boundingRect() const override;
|
||||||
|
|
@ -31,9 +35,15 @@ private:
|
||||||
core::ScatterField field_;
|
core::ScatterField field_;
|
||||||
ColorMapService* colorSvc_ = nullptr; // 不拥有,由 RawDataChartView 持有
|
ColorMapService* colorSvc_ = nullptr; // 不拥有,由 RawDataChartView 持有
|
||||||
QRectF bounding_;
|
QRectF bounding_;
|
||||||
|
bool scatterVisible_ = true; // false:仅画电极,隐藏数据方块
|
||||||
|
|
||||||
static constexpr double kHalfSide = 3.5; // 方块半边长(像素)
|
// 数据方块:原版 Plotly marker.size 12px(绝对像素直径)→ 半边长 6px,真 1:1。
|
||||||
|
static constexpr double kHalfSide = 6.0; // 方块半边长(像素,全宽 12px)
|
||||||
static constexpr double kPenWidth = 1.0; // 白色描边宽度(像素)
|
static constexpr double kPenWidth = 1.0; // 白色描边宽度(像素)
|
||||||
|
// 电极菱形:原版 Plotly marker.size 16px(绝对像素直径)→ 半对角 8px,真 1:1。
|
||||||
|
// 相对数据方块 ≈1.33×(16 vs 12),实心 #BEBEBE 填充 + 白色 2px 描边。
|
||||||
|
static constexpr double kElectrodeHalfSide = 8.0; // 半对角(像素,全宽 16px)
|
||||||
|
static constexpr double kElectrodePenWidth = 2.0; // 电极白色描边宽度(像素)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::app
|
} // namespace geopro::app
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "DatasetDetailController.hpp"
|
#include "DatasetDetailController.hpp"
|
||||||
|
#include <exception>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include "repo/IAsyncDatasetRepository.hpp"
|
#include "repo/IAsyncDatasetRepository.hpp"
|
||||||
#include "api/DatasetLoadHandles.hpp"
|
#include "api/DatasetLoadHandles.hpp"
|
||||||
|
|
@ -6,70 +7,68 @@ namespace geopro::controller {
|
||||||
|
|
||||||
DatasetDetailController::DatasetDetailController(data::IAsyncDatasetRepository& repo,
|
DatasetDetailController::DatasetDetailController(data::IAsyncDatasetRepository& repo,
|
||||||
ChartStrategyRegistry& registry, QObject* parent)
|
ChartStrategyRegistry& registry, QObject* parent)
|
||||||
: QObject(parent), repo_(repo), registry_(registry) {}
|
: QObject(parent), repo_(repo), registry_(registry) {
|
||||||
|
// QSignalSpy / 队列连接对自定义类型需注册(页签集随 datasetOpened 传递)。
|
||||||
|
qRegisterMetaType<std::vector<controller::TabSpec>>("std::vector<controller::TabSpec>");
|
||||||
|
}
|
||||||
|
|
||||||
DatasetDetailController::~DatasetDetailController() {
|
DatasetDetailController::~DatasetDetailController() {
|
||||||
if (chartLoad_) chartLoad_->abort(); // 退出契约:abort 在飞句柄,不依赖外部析构顺序兜底
|
// 退出契约:abort 全部在飞句柄,不依赖外部析构顺序兜底。
|
||||||
if (gridLoad_) gridLoad_->abort();
|
for (auto& load : inflight_)
|
||||||
|
if (load) load->abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatasetDetailController::openDataset(const QString& dsId, const QString& ddCode,
|
void DatasetDetailController::openDataset(const QString& dsId, const QString& ddCode,
|
||||||
const QString& dsName) {
|
const QString& dsName) {
|
||||||
qInfo("[detail] openDataset id=%s ddCode=%s name=%s", qUtf8Printable(dsId),
|
qInfo("[detail] openDataset id=%s ddCode=%s name=%s", qUtf8Printable(dsId),
|
||||||
qUtf8Printable(ddCode), qUtf8Printable(dsName));
|
qUtf8Printable(ddCode), qUtf8Printable(dsName));
|
||||||
if (!registry_.supports(ddCode.toStdString())) { // 未注册策略 → 优雅降级
|
auto* s = registry_.find(ddCode.toStdString());
|
||||||
|
if (!s) { // 未注册策略 → 优雅降级
|
||||||
qWarning("[detail] 未注册策略 ddCode=%s → 降级提示", qUtf8Printable(ddCode));
|
qWarning("[detail] 未注册策略 ddCode=%s → 降级提示", qUtf8Printable(ddCode));
|
||||||
emit loadFailed(dsId, QStringLiteral("暂不支持该数据类型的预览"));
|
emit loadFailed(dsId, QStringLiteral("暂不支持该数据类型的预览"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (chartLoad_) chartLoad_->abort(); // abort-and-replace
|
const std::vector<controller::TabSpec> tabs = s->tabs();
|
||||||
data::ChartLoad* load = repo_.loadChartAsync(dsId.toStdString());
|
emit datasetOpened(dsId, ddCode, dsName, tabs);
|
||||||
chartLoad_ = load;
|
for (int i = 0; i < static_cast<int>(tabs.size()); ++i)
|
||||||
emit loadStarted(dsId, LoadPhase::Chart);
|
if (!tabs[static_cast<size_t>(i)].lazy) loadTab(dsId, ddCode, i);
|
||||||
QObject::connect(load, &data::ChartLoad::done, this,
|
|
||||||
[this, load, dsId, ddCode, dsName](const data::ChartParts& parts) {
|
|
||||||
if (load != chartLoad_) return; // §5.0 句柄身份比对:丢弃迟到信号
|
|
||||||
chartLoad_.clear();
|
|
||||||
ChartData d;
|
|
||||||
d.dsId = dsId;
|
|
||||||
d.ddCode = ddCode;
|
|
||||||
d.dsName = dsName;
|
|
||||||
d.scatter = parts.scatter;
|
|
||||||
d.scatterScale = parts.scatterScale;
|
|
||||||
emit chartReady(d);
|
|
||||||
});
|
|
||||||
QObject::connect(load, &data::ChartLoad::failed, this,
|
|
||||||
[this, load, dsId](const QString& msg) {
|
|
||||||
if (load != chartLoad_) return;
|
|
||||||
chartLoad_.clear();
|
|
||||||
qWarning("[detail] 原数据加载失败 id=%s: %s", qUtf8Printable(dsId), qUtf8Printable(msg));
|
|
||||||
emit loadFailed(dsId, msg);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatasetDetailController::loadGridData(const QString& dsId, const QString& ddCode) {
|
void DatasetDetailController::loadTab(const QString& dsId, const QString& ddCode, int tabIndex) {
|
||||||
auto* strategy = registry_.find(ddCode.toStdString());
|
auto* s = registry_.find(ddCode.toStdString());
|
||||||
if (!strategy || !strategy->hasGridPhase()) return; // 仅有网格阶段的类型加载网格数据
|
if (!s) return; // 策略消失(不应发生):静默不加载
|
||||||
if (gridLoad_) gridLoad_->abort(); // abort-and-replace
|
const std::vector<controller::TabSpec> tabs = s->tabs();
|
||||||
data::GridLoad* load = repo_.loadGridAsync(dsId.toStdString());
|
if (tabIndex < 0 || tabIndex >= static_cast<int>(tabs.size())) return;
|
||||||
gridLoad_ = load;
|
const controller::TabSpec& spec = tabs[static_cast<size_t>(tabIndex)];
|
||||||
emit loadStarted(dsId, LoadPhase::Grid);
|
|
||||||
QObject::connect(load, &data::GridLoad::done, this,
|
// loadAsync 对未知 loaderKey 抛 std::runtime_error;若逃逸槽函数会被 GuardedApplication
|
||||||
[this, load, dsId](const data::GridParts& parts) {
|
// 吞掉、遮罩永久悬挂(文档化的崩溃/挂起类)。就地兜底为 loadFailed,且不留半注册的在飞句柄。
|
||||||
if (load != gridLoad_) return; // §5.0 句柄身份比对:丢弃迟到信号
|
data::DetailLoad* load = nullptr;
|
||||||
gridLoad_.clear();
|
try {
|
||||||
GridData d;
|
load = repo_.loadAsync(spec.loaderKey.toStdString(), dsId.toStdString());
|
||||||
d.dsId = dsId;
|
} catch (const std::exception& e) {
|
||||||
d.grid = parts.grid;
|
qWarning("[detail] loadAsync 失败 id=%s tab=%d key=%s: %s", qUtf8Printable(dsId), tabIndex,
|
||||||
d.gridScale = parts.gridScale;
|
qUtf8Printable(spec.loaderKey), e.what());
|
||||||
d.anomalies = parts.anomalies;
|
emit loadFailed(dsId, QStringLiteral("暂不支持该数据类型的预览"));
|
||||||
emit gridReady(d);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto& prev = inflight_[tabIndex]) prev->abort(); // abort-and-replace 该槽位
|
||||||
|
inflight_[tabIndex] = load;
|
||||||
|
emit tabLoadStarted(dsId, tabIndex);
|
||||||
|
|
||||||
|
QObject::connect(load, &data::DetailLoad::done, this,
|
||||||
|
[this, load, dsId, tabIndex](const QVariant& payload) {
|
||||||
|
if (load != inflight_.value(tabIndex)) return; // §5.0 句柄身份比对:丢弃迟到信号
|
||||||
|
inflight_.remove(tabIndex);
|
||||||
|
emit tabReady(dsId, tabIndex, payload);
|
||||||
});
|
});
|
||||||
QObject::connect(load, &data::GridLoad::failed, this,
|
QObject::connect(load, &data::DetailLoad::failed, this,
|
||||||
[this, load, dsId](const QString& msg) {
|
[this, load, dsId, tabIndex](const QString& msg) {
|
||||||
if (load != gridLoad_) return;
|
if (load != inflight_.value(tabIndex)) return;
|
||||||
gridLoad_.clear();
|
inflight_.remove(tabIndex);
|
||||||
qWarning("[detail] 网格数据加载失败 id=%s: %s", qUtf8Printable(dsId), qUtf8Printable(msg));
|
qWarning("[detail] 页签加载失败 id=%s tab=%d: %s", qUtf8Printable(dsId), tabIndex,
|
||||||
|
qUtf8Printable(msg));
|
||||||
emit loadFailed(dsId, msg);
|
emit loadFailed(dsId, msg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,40 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <QMap>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include "model/Field.hpp"
|
#include <QVariant>
|
||||||
#include "model/ColorScale.hpp"
|
#include "DatasetDetailTab.hpp"
|
||||||
#include "model/Anomaly.hpp"
|
|
||||||
#include "IDatasetChartStrategy.hpp"
|
#include "IDatasetChartStrategy.hpp"
|
||||||
namespace geopro::data { class IAsyncDatasetRepository; class ChartLoad; class GridLoad; }
|
namespace geopro::data { class IAsyncDatasetRepository; class DetailLoad; }
|
||||||
namespace geopro::controller {
|
namespace geopro::controller {
|
||||||
|
|
||||||
// 数据详情编排:双击/网格页签 → 异步拉 散点/网格/色阶/异常 → 发信号给详情面板。被动视图。
|
// 数据详情通用 tab 引擎编排:双击/页签激活 → 按 loaderKey 异步拉载荷(QVariant) → 发信号给详情面板。
|
||||||
|
// 无 per-ddCode 分支:页签集由策略 tabs() 描述,载荷经 QVariant 类型擦除。被动视图。
|
||||||
class DatasetDetailController : public QObject {
|
class DatasetDetailController : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
enum class LoadPhase { Chart, Grid };
|
|
||||||
Q_ENUM(LoadPhase)
|
|
||||||
|
|
||||||
struct ChartData {
|
|
||||||
QString dsId, ddCode, dsName; // dsName:页签标题用(空则回退 dsId)
|
|
||||||
geopro::core::ScatterField scatter;
|
|
||||||
geopro::core::ColorScale scatterScale;
|
|
||||||
geopro::core::Grid grid{1, 1}; // Grid 无默认构造;以占位值初始化,openDataset 会覆盖
|
|
||||||
geopro::core::ColorScale gridScale;
|
|
||||||
std::vector<geopro::core::Anomaly> anomalies;
|
|
||||||
};
|
|
||||||
// 网格数据(inversion/rows + 色阶 type2 + 异常):随「网格数据」页签首次激活按需懒加载。
|
|
||||||
struct GridData {
|
|
||||||
QString dsId;
|
|
||||||
geopro::core::Grid grid{1, 1}; // Grid 无默认构造;占位值初始化,loadGridData 会覆盖
|
|
||||||
geopro::core::ColorScale gridScale;
|
|
||||||
std::vector<geopro::core::Anomaly> anomalies;
|
|
||||||
};
|
|
||||||
|
|
||||||
DatasetDetailController(data::IAsyncDatasetRepository& repo,
|
DatasetDetailController(data::IAsyncDatasetRepository& repo,
|
||||||
ChartStrategyRegistry& registry, QObject* parent = nullptr);
|
ChartStrategyRegistry& registry, QObject* parent = nullptr);
|
||||||
~DatasetDetailController() override; // 退出契约(spec §7):abort 在飞句柄,避免迟到信号打到已析构 this
|
~DatasetDetailController() override; // 退出契约(spec §7):abort 全部在飞句柄,避免迟到信号打到已析构 this
|
||||||
public slots:
|
public slots:
|
||||||
|
// 打开数据集:查策略 → datasetOpened(页签集) → 对每个非 lazy 页签发起 loadTab。
|
||||||
void openDataset(const QString& dsId, const QString& ddCode, const QString& dsName = QString());
|
void openDataset(const QString& dsId, const QString& ddCode, const QString& dsName = QString());
|
||||||
|
// 加载某页签(lazy 页签首次激活时由壳触发;非 lazy 由 openDataset 自动触发)。
|
||||||
|
void loadTab(const QString& dsId, const QString& ddCode, int tabIndex);
|
||||||
void focusDataset(const QString& dsId);
|
void focusDataset(const QString& dsId);
|
||||||
void loadGridData(const QString& dsId, const QString& ddCode);
|
|
||||||
signals:
|
signals:
|
||||||
void loadStarted(const QString& dsId, LoadPhase phase);
|
void datasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName,
|
||||||
void chartReady(const ChartData& data);
|
const std::vector<controller::TabSpec>& tabs);
|
||||||
void gridReady(const GridData& data);
|
void tabLoadStarted(const QString& dsId, int tabIndex);
|
||||||
void focusRequested(const QString& dsId);
|
void tabReady(const QString& dsId, int tabIndex, const QVariant& payload);
|
||||||
void loadFailed(const QString& dsId, const QString& message);
|
void loadFailed(const QString& dsId, const QString& message);
|
||||||
|
void focusRequested(const QString& dsId);
|
||||||
private:
|
private:
|
||||||
data::IAsyncDatasetRepository& repo_;
|
data::IAsyncDatasetRepository& repo_;
|
||||||
ChartStrategyRegistry& registry_;
|
ChartStrategyRegistry& registry_;
|
||||||
QPointer<data::ChartLoad> chartLoad_;
|
QMap<int, QPointer<data::DetailLoad>> inflight_; // 按页签槽位的在飞句柄(§5.0 身份比对)
|
||||||
QPointer<data::GridLoad> gridLoad_;
|
|
||||||
};
|
};
|
||||||
} // namespace geopro::controller
|
} // namespace geopro::controller
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
#include <QMetaType>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace geopro::controller {
|
||||||
|
|
||||||
|
// 详情页签的渲染 kind 全集(Image/GISMap 待 GPR/radar 有活样本再加,YAGNI)。
|
||||||
|
enum class ViewKind { Scatter, FilledContour, Bar, LineProfile, PolylineMap, Table };
|
||||||
|
|
||||||
|
// 页签描述符:策略声明每个 dd 类型的页签集(标题/kind/加载键/惰性/分页)。
|
||||||
|
struct TabSpec {
|
||||||
|
QString title;
|
||||||
|
ViewKind kind;
|
||||||
|
QString loaderKey;
|
||||||
|
bool lazy = false;
|
||||||
|
bool paginated = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::controller
|
||||||
|
|
||||||
|
// QSignalSpy 对 datasetOpened(..., std::vector<TabSpec>) 需注册的元类型。
|
||||||
|
Q_DECLARE_METATYPE(geopro::controller::TabSpec)
|
||||||
|
Q_DECLARE_METATYPE(std::vector<geopro::controller::TabSpec>)
|
||||||
|
|
@ -2,15 +2,17 @@
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "DatasetDetailTab.hpp"
|
||||||
namespace geopro::controller {
|
namespace geopro::controller {
|
||||||
|
|
||||||
// dd 类型驱动的图表策略:决定某 ddCode 的详情页如何加载/渲染。
|
// dd 类型驱动的图表策略:决定某 ddCode 的详情页如何加载/渲染。
|
||||||
struct IDatasetChartStrategy {
|
struct IDatasetChartStrategy {
|
||||||
virtual ~IDatasetChartStrategy() = default;
|
virtual ~IDatasetChartStrategy() = default;
|
||||||
virtual std::string ddCode() const = 0;
|
virtual std::string ddCode() const = 0;
|
||||||
// 该类型是否有「网格数据」加载阶段(ERT 反演=true;纯散点/折线/图像类=false)。
|
// 该类型的页签集(标题/render kind/加载键/惰性/分页)。控制器据此建页签 + 驱动加载,
|
||||||
// 控制器据此决定是否允许 loadGridData,替代硬编码 ddCode 判断。
|
// 取代硬编码的 hasGridPhase()/ddCode 判断。
|
||||||
virtual bool hasGridPhase() const = 0;
|
virtual std::vector<TabSpec> tabs() const = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ChartStrategyRegistry {
|
class ChartStrategyRegistry {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,12 @@ private:
|
||||||
struct ScatterField {
|
struct ScatterField {
|
||||||
std::vector<double> x, y, z, v; // 对应样本 xlist/ylist/(hlist)/vlist
|
std::vector<double> x, y, z, v; // 对应样本 xlist/ylist/(hlist)/vlist
|
||||||
std::vector<double> projX, projY; // GIS 平面坐标
|
std::vector<double> projX, projY; // GIS 平面坐标
|
||||||
|
// 可选元数据(measurement 散点用,反演留空)。a/b/m/n 与各点一一对应(hover 显示
|
||||||
|
// A/B/M/N),electrodeX 为电极沿测线位置(y=0 处灰菱形 marker),electrodeNo 为对应
|
||||||
|
// 电极编号(1-based,hover 显示 num,与 electrodeX 一一对应)。
|
||||||
|
std::vector<double> a, b, m, n;
|
||||||
|
std::vector<double> electrodeX;
|
||||||
|
std::vector<double> electrodeNo;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::core
|
} // namespace geopro::core
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
#include <QMetaType>
|
||||||
|
#include <QString>
|
||||||
|
#include "model/Field.hpp"
|
||||||
|
#include "model/ColorScale.hpp"
|
||||||
|
#include "model/Anomaly.hpp"
|
||||||
|
|
||||||
|
// 详情渲染载荷(纯数据,跨 QVariant 类型擦除传递)。无 Qt-widget 依赖。
|
||||||
|
// 命名空间 geopro::core(与同目录 Field.hpp/ColorScale.hpp 一致)。
|
||||||
|
namespace geopro::core {
|
||||||
|
|
||||||
|
// 下拉项:字段码 + 显示名(measurement 工具条 x/y/v/method 下拉驱动;code 可空如 method)。
|
||||||
|
struct FieldOption {
|
||||||
|
QString code;
|
||||||
|
QString name;
|
||||||
|
};
|
||||||
|
|
||||||
|
// measurement 散点工具条配置(来自服务端 scatterGraphConf;反演留空 → 视图渲染反演工具条)。
|
||||||
|
// x/y/v/method 为各下拉的可选项(含 fieldCode+name);defaultX/Y/V/Method 为默认选中项的
|
||||||
|
// fieldCode(method 的 fieldCode 全为 null,故 defaultMethod 用 name)。empty() 判定走 x 是否为空。
|
||||||
|
struct ScatterToolbarConf {
|
||||||
|
std::vector<FieldOption> x, y, v, method;
|
||||||
|
QString defaultX, defaultY, defaultV, defaultMethod;
|
||||||
|
bool empty() const { return x.empty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 散点载荷:反演原数据 / measurement 散点共用(≈ data::ChartParts)。
|
||||||
|
// 两者上色一致——按数据 min/max 连续插值(Plotly cauto,含负异常值,RawDataChartView 据 v
|
||||||
|
// 有限值 min/max 设 setDataRange)。verticalLegend=true(measurement)时色阶图例画在右侧竖条
|
||||||
|
// (离散带,1323→0 自上而下,对齐原版);false(默认,反演原数据)时画在底部横条。
|
||||||
|
// toolbar 非空(measurement)时视图渲染 measurement 工具条;altX*/altY* 为 x/y 下拉本地重绘
|
||||||
|
// 用的备选列(平距/斜距、伪深度/伪深度+高程),与 scatter.v/.a... 同序、一一对应,避免再发请求。
|
||||||
|
struct ScatterPayload {
|
||||||
|
geopro::core::ScatterField scatter;
|
||||||
|
geopro::core::ColorScale scale;
|
||||||
|
bool verticalLegend = false;
|
||||||
|
ScatterToolbarConf toolbar;
|
||||||
|
std::vector<double> altXHorizontal, altXSlope; // x 下拉:平距 / 斜距
|
||||||
|
std::vector<double> altYPseudo, altYElevationPseudo; // y 下拉:伪深度 / 伪深度+高程
|
||||||
|
};
|
||||||
|
|
||||||
|
// 等值面载荷:grid(rows) + 色阶 + 异常(≈ data::GridParts)。
|
||||||
|
// Grid 无默认构造,给占位初始化以满足 QVariant 对默认可构造的要求。
|
||||||
|
struct ContourPayload {
|
||||||
|
geopro::core::Grid grid{1, 1};
|
||||||
|
geopro::core::ColorScale scale;
|
||||||
|
std::vector<geopro::core::Anomaly> anomalies;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 列渲染种类:Text=预格式化文本(默认);Toggle=每行开关(蓝色药丸开关,ON=可见)。
|
||||||
|
enum class TableColumnKind { Text, Toggle };
|
||||||
|
|
||||||
|
// 通用表格列定义。
|
||||||
|
struct TableColumn {
|
||||||
|
QString code;
|
||||||
|
QString title;
|
||||||
|
int width = 0;
|
||||||
|
int sort = 0;
|
||||||
|
TableColumnKind kind = TableColumnKind::Text;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通用表格载荷:列定义 + 预格式化的行(每格 QString)+ 总数(分页用)。
|
||||||
|
struct TablePayload {
|
||||||
|
std::vector<TableColumn> columns;
|
||||||
|
std::vector<std::vector<QString>> rows;
|
||||||
|
int total = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::core
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(geopro::core::ScatterPayload)
|
||||||
|
Q_DECLARE_METATYPE(geopro::core::ContourPayload)
|
||||||
|
Q_DECLARE_METATYPE(geopro::core::TablePayload)
|
||||||
|
|
@ -5,6 +5,7 @@ add_library(geopro_data STATIC
|
||||||
repo/LocalSampleRepository.cpp
|
repo/LocalSampleRepository.cpp
|
||||||
dto/NavDto.cpp
|
dto/NavDto.cpp
|
||||||
dto/DatasetChartDto.cpp
|
dto/DatasetChartDto.cpp
|
||||||
|
dto/MeasurementDto.cpp
|
||||||
api/ApiProjectRepository.cpp
|
api/ApiProjectRepository.cpp
|
||||||
api/ApiDatasetRepository.cpp
|
api/ApiDatasetRepository.cpp
|
||||||
api/DatasetLoadHandles.cpp
|
api/DatasetLoadHandles.cpp
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
#include "api/ApiDatasetRepository.hpp"
|
#include "api/ApiDatasetRepository.hpp"
|
||||||
|
#include <stdexcept>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
#include <QVariant>
|
||||||
#include "ApiClient.hpp"
|
#include "ApiClient.hpp"
|
||||||
#include "ApiBatch.hpp"
|
#include "ApiBatch.hpp"
|
||||||
#include "api/DatasetLoadHandles.hpp"
|
#include "api/DatasetLoadHandles.hpp"
|
||||||
#include "dto/DatasetChartDto.hpp"
|
#include "dto/DatasetChartDto.hpp"
|
||||||
|
#include "dto/MeasurementDto.hpp"
|
||||||
|
#include "model/detail/DetailPayloads.hpp"
|
||||||
|
|
||||||
namespace geopro::data {
|
namespace geopro::data {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
@ -14,6 +18,19 @@ QString enc(const std::string& s) {
|
||||||
return QString::fromUtf8(QUrl::toPercentEncoding(QString::fromStdString(s)));
|
return QString::fromUtf8(QUrl::toPercentEncoding(QString::fromStdString(s)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析中间态(仅本 .cpp 内部使用,故置匿名命名空间,避免对外双份表示)。
|
||||||
|
// 原数据加载结果:scatter + 散点色阶(type1)。
|
||||||
|
struct ChartParts {
|
||||||
|
geopro::core::ScatterField scatter;
|
||||||
|
geopro::core::ColorScale scatterScale;
|
||||||
|
};
|
||||||
|
// 网格数据加载结果:grid(rows) + 网格色阶(type2) + 异常。
|
||||||
|
struct GridParts {
|
||||||
|
geopro::core::Grid grid{1, 1}; // Grid 无默认构造;占位
|
||||||
|
geopro::core::ColorScale gridScale;
|
||||||
|
std::vector<geopro::core::Anomaly> anomalies;
|
||||||
|
};
|
||||||
|
|
||||||
// 失败判定(原 must() 口径):业务码 != 200 或传输错误。
|
// 失败判定(原 must() 口径):业务码 != 200 或传输错误。
|
||||||
bool isFailure(const geopro::net::ApiResponse& r) {
|
bool isFailure(const geopro::net::ApiResponse& r) {
|
||||||
return r.code != 200 || !r.rawError.isEmpty();
|
return r.code != 200 || !r.rawError.isEmpty();
|
||||||
|
|
@ -23,39 +40,99 @@ QJsonObject colorBody(const std::string& dsId, int type) {
|
||||||
return QJsonObject{{"dsObjectId", QString::fromStdString(dsId)}, {"businessCode", ""}, {"type", type}};
|
return QJsonObject{{"dsObjectId", QString::fromStdString(dsId)}, {"businessCode", ""}, {"type", type}};
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
// ── 共享批次构造:唯一端点定义处,old/new 路径复用,避免双份解析逻辑。 ──
|
||||||
|
// 反演原数据:index 0 = scatter(GET),1 = 散点色阶 type1(POST)。
|
||||||
ApiDatasetRepository::ApiDatasetRepository(net::ApiClient& api) : api_(api) {}
|
net::ApiBatch* inversionScatterBatch(net::ApiClient& api, const std::string& dsId) {
|
||||||
|
|
||||||
ChartLoad* ApiDatasetRepository::loadChartAsync(const std::string& dsId) {
|
|
||||||
// index 0 = scatter(GET),index 1 = 散点色阶 type1(POST)
|
|
||||||
QList<net::IApiCall*> calls{
|
QList<net::IApiCall*> calls{
|
||||||
api_.getAsync(QStringLiteral("/business/dd/ert/inversion/getErtRawDataScatterGraph/%1").arg(enc(dsId))),
|
api.getAsync(QStringLiteral("/business/dd/ert/inversion/getErtRawDataScatterGraph/%1").arg(enc(dsId))),
|
||||||
api_.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"), colorBody(dsId, 1)),
|
api.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"), colorBody(dsId, 1)),
|
||||||
};
|
};
|
||||||
auto* batch = new net::ApiBatch(calls, &isFailure);
|
return new net::ApiBatch(calls, &isFailure);
|
||||||
return new ApiChartLoad(batch, [](const QList<net::ApiResponse>& r) {
|
}
|
||||||
|
|
||||||
|
ChartParts parseScatterParts(const QList<net::ApiResponse>& r) {
|
||||||
ChartParts p;
|
ChartParts p;
|
||||||
p.scatter = dto::parseScatterGraph(r[0].data);
|
p.scatter = dto::parseScatterGraph(r[0].data);
|
||||||
p.scatterScale = dto::parseColorBar(r[1].data);
|
p.scatterScale = dto::parseColorBar(r[1].data);
|
||||||
return p;
|
return p;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GridLoad* ApiDatasetRepository::loadGridAsync(const std::string& dsId) {
|
// 反演网格:index 0 = rows(GET,慢),1 = 色阶 type2(POST),2 = 异常(GET)。
|
||||||
// index 0 = rows(GET,慢),1 = 色阶 type2(POST),2 = 异常(GET)
|
net::ApiBatch* inversionGridBatch(net::ApiClient& api, const std::string& dsId) {
|
||||||
QList<net::IApiCall*> calls{
|
QList<net::IApiCall*> calls{
|
||||||
api_.getAsync(QStringLiteral("/business/dd/ert/inversion/rows/%1").arg(enc(dsId))),
|
api.getAsync(QStringLiteral("/business/dd/ert/inversion/rows/%1").arg(enc(dsId))),
|
||||||
api_.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"), colorBody(dsId, 2)),
|
api.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"), colorBody(dsId, 2)),
|
||||||
api_.getAsync(QStringLiteral("/business/exception/queryException/%1").arg(enc(dsId))),
|
api.getAsync(QStringLiteral("/business/exception/queryException/%1").arg(enc(dsId))),
|
||||||
};
|
};
|
||||||
auto* batch = new net::ApiBatch(calls, &isFailure);
|
return new net::ApiBatch(calls, &isFailure);
|
||||||
return new ApiGridLoad(batch, [](const QList<net::ApiResponse>& r) {
|
}
|
||||||
|
|
||||||
|
GridParts parseGridParts(const QList<net::ApiResponse>& r) {
|
||||||
GridParts p;
|
GridParts p;
|
||||||
p.grid = dto::parseInversionGrid(r[0].data);
|
p.grid = dto::parseInversionGrid(r[0].data);
|
||||||
p.gridScale = dto::parseColorBar(r[1].data);
|
p.gridScale = dto::parseColorBar(r[1].data);
|
||||||
p.anomalies = dto::parseDatasetAnomalies(r[2].data.value(QStringLiteral("value")).toArray());
|
p.anomalies = dto::parseDatasetAnomalies(r[2].data.value(QStringLiteral("value")).toArray());
|
||||||
return p;
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// measurement 散点:index 0 = scatter/graph(GET,query 参数),1 = 色阶 type3(POST,businessCode=R0)。
|
||||||
|
net::ApiBatch* measurementScatterBatch(net::ApiClient& api, const std::string& dsId) {
|
||||||
|
const QString did = enc(dsId);
|
||||||
|
QList<net::IApiCall*> calls{
|
||||||
|
api.getAsync(QStringLiteral("/business/dd/ert/measurement/scatter/graph?dsObjectId=%1&vFieldCode=").arg(did)),
|
||||||
|
api.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"),
|
||||||
|
QJsonObject{{"dsObjectId", QString::fromStdString(dsId)},
|
||||||
|
{"businessCode", "R0"},
|
||||||
|
{"type", 3}}),
|
||||||
|
};
|
||||||
|
return new net::ApiBatch(calls, &isFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
// measurement 列表:单请求 measurement/rows(GET,query 参数)。
|
||||||
|
net::ApiBatch* measurementRowsBatch(net::ApiClient& api, const std::string& dsId) {
|
||||||
|
QList<net::IApiCall*> calls{
|
||||||
|
api.getAsync(QStringLiteral("/business/dd/ert/measurement/rows?dsObjectId=%1").arg(enc(dsId))),
|
||||||
|
};
|
||||||
|
return new net::ApiBatch(calls, &isFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ApiDatasetRepository::ApiDatasetRepository(net::ApiClient& api) : api_(api) {}
|
||||||
|
|
||||||
|
DetailLoad* ApiDatasetRepository::loadAsync(const std::string& loaderKey, const std::string& dsId) {
|
||||||
|
if (loaderKey == "inversion.scatter") return makeInversionScatter(dsId);
|
||||||
|
if (loaderKey == "inversion.grid") return makeInversionGrid(dsId);
|
||||||
|
if (loaderKey == "ert_measurement.scatter") return makeMeasurementScatter(dsId);
|
||||||
|
if (loaderKey == "ert_measurement.rows") return makeMeasurementRows(dsId);
|
||||||
|
throw std::runtime_error("unknown loaderKey: " + loaderKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
DetailLoad* ApiDatasetRepository::makeInversionScatter(const std::string& dsId) {
|
||||||
|
// 复用同一批次 + 解析器,再映射为 ScatterPayload(不复制 JSON 解析逻辑)。
|
||||||
|
return new ApiDetailLoad(inversionScatterBatch(api_, dsId), [](const QList<net::ApiResponse>& r) {
|
||||||
|
ChartParts p = parseScatterParts(r);
|
||||||
|
return QVariant::fromValue(core::ScatterPayload{p.scatter, p.scatterScale});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
DetailLoad* ApiDatasetRepository::makeInversionGrid(const std::string& dsId) {
|
||||||
|
return new ApiDetailLoad(inversionGridBatch(api_, dsId), [](const QList<net::ApiResponse>& r) {
|
||||||
|
GridParts p = parseGridParts(r);
|
||||||
|
return QVariant::fromValue(core::ContourPayload{p.grid, p.gridScale, p.anomalies});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
DetailLoad* ApiDatasetRepository::makeMeasurementScatter(const std::string& dsId) {
|
||||||
|
// index 0 = scatter/graph, 1 = colorBar(type3) → 离散上色的 ScatterPayload。
|
||||||
|
return new ApiDetailLoad(measurementScatterBatch(api_, dsId), [](const QList<net::ApiResponse>& r) {
|
||||||
|
return QVariant::fromValue(dto::parseMeasurementScatter(r[0].data, r[1].data));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
DetailLoad* ApiDatasetRepository::makeMeasurementRows(const std::string& dsId) {
|
||||||
|
return new ApiDetailLoad(measurementRowsBatch(api_, dsId), [](const QList<net::ApiResponse>& r) {
|
||||||
|
return QVariant::fromValue(dto::parseMeasurementTable(r[0].data));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,12 @@ namespace geopro::data {
|
||||||
class ApiDatasetRepository : public IAsyncDatasetRepository {
|
class ApiDatasetRepository : public IAsyncDatasetRepository {
|
||||||
public:
|
public:
|
||||||
explicit ApiDatasetRepository(net::ApiClient& api);
|
explicit ApiDatasetRepository(net::ApiClient& api);
|
||||||
ChartLoad* loadChartAsync(const std::string& dsId) override;
|
DetailLoad* loadAsync(const std::string& loaderKey, const std::string& dsId) override;
|
||||||
GridLoad* loadGridAsync(const std::string& dsId) override;
|
|
||||||
private:
|
private:
|
||||||
|
DetailLoad* makeInversionScatter(const std::string& dsId);
|
||||||
|
DetailLoad* makeInversionGrid(const std::string& dsId);
|
||||||
|
DetailLoad* makeMeasurementScatter(const std::string& dsId);
|
||||||
|
DetailLoad* makeMeasurementRows(const std::string& dsId);
|
||||||
net::ApiClient& api_;
|
net::ApiClient& api_;
|
||||||
};
|
};
|
||||||
} // namespace geopro::data
|
} // namespace geopro::data
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ QString reasonOf(const geopro::net::ApiResponse& r) {
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ApiChartLoad::ApiChartLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent)
|
ApiDetailLoad::ApiDetailLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent)
|
||||||
: ChartLoad(parent), batch_(batch), parse_(std::move(parse)) {
|
: DetailLoad(parent), batch_(batch), parse_(std::move(parse)) {
|
||||||
QObject::connect(batch, &geopro::net::ApiBatch::succeeded, this,
|
QObject::connect(batch, &geopro::net::ApiBatch::succeeded, this,
|
||||||
[this](const QList<geopro::net::ApiResponse>& resps) {
|
[this](const QList<geopro::net::ApiResponse>& resps) {
|
||||||
if (aborted_) return; // §5.0
|
if (aborted_) return; // §5.0
|
||||||
ChartParts parts;
|
QVariant payload;
|
||||||
try {
|
try {
|
||||||
parts = parse_(resps); // 仅解析在 try 内:下游 done 处理器抛出不应误报为解析失败
|
payload = parse_(resps); // 仅解析在 try 内:下游 done 处理器抛出不应误报为解析失败
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
emit failed(QString::fromUtf8(e.what()));
|
emit failed(QString::fromUtf8(e.what()));
|
||||||
deleteLater();
|
deleteLater();
|
||||||
|
|
@ -26,7 +26,7 @@ ApiChartLoad::ApiChartLoad(geopro::net::ApiBatch* batch, Parser parse, QObject*
|
||||||
deleteLater();
|
deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emit done(parts);
|
emit done(payload);
|
||||||
deleteLater();
|
deleteLater();
|
||||||
});
|
});
|
||||||
QObject::connect(batch, &geopro::net::ApiBatch::failed, this,
|
QObject::connect(batch, &geopro::net::ApiBatch::failed, this,
|
||||||
|
|
@ -37,42 +37,7 @@ ApiChartLoad::ApiChartLoad(geopro::net::ApiBatch* batch, Parser parse, QObject*
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiChartLoad::abort() {
|
void ApiDetailLoad::abort() {
|
||||||
if (aborted_) return;
|
|
||||||
aborted_ = true;
|
|
||||||
if (batch_) batch_->abort();
|
|
||||||
deleteLater();
|
|
||||||
}
|
|
||||||
|
|
||||||
ApiGridLoad::ApiGridLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent)
|
|
||||||
: GridLoad(parent), batch_(batch), parse_(std::move(parse)) {
|
|
||||||
QObject::connect(batch, &geopro::net::ApiBatch::succeeded, this,
|
|
||||||
[this](const QList<geopro::net::ApiResponse>& resps) {
|
|
||||||
if (aborted_) return;
|
|
||||||
GridParts parts;
|
|
||||||
try {
|
|
||||||
parts = parse_(resps); // 仅解析在 try 内:下游 done 处理器抛出不应误报为解析失败
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
emit failed(QString::fromUtf8(e.what()));
|
|
||||||
deleteLater();
|
|
||||||
return;
|
|
||||||
} catch (...) {
|
|
||||||
emit failed(QStringLiteral("解析失败:未知异常"));
|
|
||||||
deleteLater();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emit done(parts);
|
|
||||||
deleteLater();
|
|
||||||
});
|
|
||||||
QObject::connect(batch, &geopro::net::ApiBatch::failed, this,
|
|
||||||
[this](int, const geopro::net::ApiResponse& r) {
|
|
||||||
if (aborted_) return;
|
|
||||||
emit failed(reasonOf(r));
|
|
||||||
deleteLater();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ApiGridLoad::abort() {
|
|
||||||
if (aborted_) return;
|
if (aborted_) return;
|
||||||
aborted_ = true;
|
aborted_ = true;
|
||||||
if (batch_) batch_->abort();
|
if (batch_) batch_->abort();
|
||||||
|
|
|
||||||
|
|
@ -4,52 +4,30 @@
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QVariant>
|
||||||
#include "ApiBatch.hpp"
|
#include "ApiBatch.hpp"
|
||||||
#include "DatasetLoads.hpp"
|
|
||||||
|
|
||||||
namespace geopro::data {
|
namespace geopro::data {
|
||||||
|
|
||||||
// ── 抽象句柄(可测试缝,类比 IApiCall):仓储返回基类指针,控制器/测试只依赖它 ──
|
// ── 通用详情句柄(tab 引擎):载荷经 QVariant 类型擦除,单一 done(QVariant)。 ──
|
||||||
class ChartLoad : public QObject {
|
// 可测试缝(类比 IApiCall):仓储返回基类指针,控制器/测试只依赖它。
|
||||||
|
class DetailLoad : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
using QObject::QObject;
|
using QObject::QObject;
|
||||||
~ChartLoad() override = default;
|
~DetailLoad() override = default;
|
||||||
virtual void abort() = 0;
|
virtual void abort() = 0;
|
||||||
signals:
|
signals:
|
||||||
void done(const geopro::data::ChartParts& parts);
|
void done(const QVariant& payload);
|
||||||
void failed(const QString& message);
|
void failed(const QString& message);
|
||||||
};
|
};
|
||||||
|
|
||||||
class GridLoad : public QObject {
|
// Api 实现:包 ApiBatch + 注入解析器(返回 QVariant 载荷)。逻辑与 ApiChartLoad 等价。
|
||||||
|
class ApiDetailLoad : public DetailLoad {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
using QObject::QObject;
|
using Parser = std::function<QVariant(const QList<geopro::net::ApiResponse>&)>;
|
||||||
~GridLoad() override = default;
|
ApiDetailLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent = nullptr);
|
||||||
virtual void abort() = 0;
|
|
||||||
signals:
|
|
||||||
void done(const geopro::data::GridParts& parts);
|
|
||||||
void failed(const QString& message);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Api 实现:包一个 ApiBatch + 注入的解析函数。batch.succeeded→解析→done;failed→failed ──
|
|
||||||
class ApiChartLoad : public ChartLoad {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
using Parser = std::function<ChartParts(const QList<geopro::net::ApiResponse>&)>;
|
|
||||||
ApiChartLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent = nullptr);
|
|
||||||
void abort() override;
|
|
||||||
private:
|
|
||||||
QPointer<geopro::net::ApiBatch> batch_;
|
|
||||||
Parser parse_;
|
|
||||||
bool aborted_ = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ApiGridLoad : public GridLoad {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
using Parser = std::function<GridParts(const QList<geopro::net::ApiResponse>&)>;
|
|
||||||
ApiGridLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent = nullptr);
|
|
||||||
void abort() override;
|
void abort() override;
|
||||||
private:
|
private:
|
||||||
QPointer<geopro::net::ApiBatch> batch_;
|
QPointer<geopro::net::ApiBatch> batch_;
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include <vector>
|
|
||||||
#include "model/Field.hpp"
|
|
||||||
#include "model/ColorScale.hpp"
|
|
||||||
#include "model/Anomaly.hpp"
|
|
||||||
|
|
||||||
namespace geopro::data {
|
|
||||||
|
|
||||||
// 原数据加载结果:scatter + 散点色阶(type1)。
|
|
||||||
struct ChartParts {
|
|
||||||
geopro::core::ScatterField scatter;
|
|
||||||
geopro::core::ColorScale scatterScale;
|
|
||||||
};
|
|
||||||
// 网格数据加载结果:grid(rows) + 网格色阶(type2) + 异常。
|
|
||||||
struct GridParts {
|
|
||||||
geopro::core::Grid grid{1, 1}; // Grid 无默认构造;占位
|
|
||||||
geopro::core::ColorScale gridScale;
|
|
||||||
std::vector<geopro::core::Anomaly> anomalies;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace geopro::data
|
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
#include "dto/MeasurementDto.hpp"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonValue>
|
||||||
|
#include "dto/DatasetChartDto.hpp" // parseColorBar
|
||||||
|
|
||||||
|
namespace geopro::data::dto {
|
||||||
|
using namespace geopro::core;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// 位置数组列序(rows[i] 为 17 列定位数组)。
|
||||||
|
constexpr int kColHorizontalDistance = 0; // x 备选:平距
|
||||||
|
constexpr int kColSlopeDistance = 1; // x:斜距(默认)
|
||||||
|
constexpr int kColPseudoDepth = 3; // y:伪深度(负,向下,默认)
|
||||||
|
constexpr int kColElevationPseudoDepth = 9; // y 备选:伪深度+高程
|
||||||
|
constexpr int kColA = 10;
|
||||||
|
constexpr int kColB = 11;
|
||||||
|
constexpr int kColM = 12;
|
||||||
|
constexpr int kColN = 13;
|
||||||
|
constexpr int kColValue = 16; // 色值:选中 v(默认视电阻率 R0)
|
||||||
|
|
||||||
|
double numAt(const QJsonArray& arr, int i) {
|
||||||
|
if (i < 0 || i >= arr.size()) return 0.0;
|
||||||
|
const QJsonValue v = arr.at(i);
|
||||||
|
if (v.isDouble()) return v.toDouble();
|
||||||
|
if (v.isString()) { bool ok = false; double d = v.toString().toDouble(&ok); return ok ? d : 0.0; }
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 把 JSON 值预格式化为单元格 QString(数字按原值,null/缺省→空串)。
|
||||||
|
QString cellText(const QJsonValue& v) {
|
||||||
|
if (v.isDouble()) {
|
||||||
|
const double d = v.toDouble();
|
||||||
|
// 整数值不带小数点(与列表观感一致);非整数保留有效位。
|
||||||
|
if (d == static_cast<double>(static_cast<long long>(d))) return QString::number(static_cast<long long>(d));
|
||||||
|
return QString::number(d, 'g', 10);
|
||||||
|
}
|
||||||
|
if (v.isString()) return v.toString();
|
||||||
|
if (v.isBool()) return v.toBool() ? QStringLiteral("true") : QStringLiteral("false");
|
||||||
|
return QString(); // null/undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// scatterGraphConf 的某一组下拉项 → FieldOption 列表(保留服务端顺序)。
|
||||||
|
std::vector<FieldOption> parseOptions(const QJsonArray& arr) {
|
||||||
|
std::vector<FieldOption> out;
|
||||||
|
out.reserve(arr.size());
|
||||||
|
for (const auto& e : arr) {
|
||||||
|
const QJsonObject o = e.toObject();
|
||||||
|
out.push_back({o.value(QStringLiteral("fieldCode")).toString(),
|
||||||
|
o.value(QStringLiteral("name")).toString()});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ScatterPayload parseMeasurementScatter(const QJsonObject& scatterData, const QJsonObject& colorBarData) {
|
||||||
|
ScatterPayload p;
|
||||||
|
// 上色与反演原数据同路径:连续插值(cauto)。色值 col16 原样保留(含负异常值),
|
||||||
|
// RawDataChartView 据 v 有限值 min/max 设 setDataRange → cmin<0,与原版 Plotly 一致。
|
||||||
|
// 图例与反演统一为底部横条(verticalLegend 保持默认 false)。
|
||||||
|
|
||||||
|
const QJsonArray rows = scatterData.value(QStringLiteral("scatterGraphData"))
|
||||||
|
.toObject()
|
||||||
|
.value(QStringLiteral("rows"))
|
||||||
|
.toArray();
|
||||||
|
auto& s = p.scatter;
|
||||||
|
for (const auto& e : rows) {
|
||||||
|
const QJsonArray row = e.toArray();
|
||||||
|
s.x.push_back(numAt(row, kColSlopeDistance));
|
||||||
|
s.y.push_back(numAt(row, kColPseudoDepth));
|
||||||
|
s.v.push_back(numAt(row, kColValue));
|
||||||
|
s.a.push_back(numAt(row, kColA));
|
||||||
|
s.b.push_back(numAt(row, kColB));
|
||||||
|
s.m.push_back(numAt(row, kColM));
|
||||||
|
s.n.push_back(numAt(row, kColN));
|
||||||
|
// x/y 下拉本地重绘备选列(与上面 push 同序、一一对应 → 视图换 x/y 无需再请求)。
|
||||||
|
p.altXHorizontal.push_back(numAt(row, kColHorizontalDistance));
|
||||||
|
p.altXSlope.push_back(numAt(row, kColSlopeDistance));
|
||||||
|
p.altYPseudo.push_back(numAt(row, kColPseudoDepth));
|
||||||
|
p.altYElevationPseudo.push_back(numAt(row, kColElevationPseudoDepth));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具条下拉配置(scatterGraphConf):驱动 measurement 工具条 x/y/v/method 下拉与默认选中。
|
||||||
|
// x/y/method 默认取首项(斜距 / 伪深度 / 线性);v 默认取“视电阻率”(R0),回退末-… 改为首项。
|
||||||
|
const QJsonObject conf = scatterData.value(QStringLiteral("scatterGraphConf")).toObject();
|
||||||
|
auto& tb = p.toolbar;
|
||||||
|
tb.x = parseOptions(conf.value(QStringLiteral("x")).toArray());
|
||||||
|
tb.y = parseOptions(conf.value(QStringLiteral("y")).toArray());
|
||||||
|
tb.v = parseOptions(conf.value(QStringLiteral("v")).toArray());
|
||||||
|
tb.method = parseOptions(conf.value(QStringLiteral("vcalculationMethod")).toArray());
|
||||||
|
// 默认选中:x=斜距(slopeDistance)、y=伪深度(pseudoDepth)、v=视电阻率(R0)、method=线性。
|
||||||
|
tb.defaultX = QStringLiteral("slopeDistance");
|
||||||
|
tb.defaultY = QStringLiteral("pseudoDepth");
|
||||||
|
tb.defaultV = QStringLiteral("R0");
|
||||||
|
if (!tb.method.empty()) tb.defaultMethod = tb.method.front().name; // 线性
|
||||||
|
|
||||||
|
// 电极沿测线位置(y=0 处灰菱形):取 slopeDistance 与 x(斜距)同量纲;
|
||||||
|
// electrodeNo 为电极编号(1-based),与 electrodeX 一一对应,供 hover 显示 num。
|
||||||
|
for (const auto& e : scatterData.value(QStringLiteral("electrodeList")).toArray()) {
|
||||||
|
const QJsonObject eo = e.toObject();
|
||||||
|
s.electrodeX.push_back(eo.value(QStringLiteral("slopeDistance")).toDouble());
|
||||||
|
s.electrodeNo.push_back(eo.value(QStringLiteral("electrodeNo")).toDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
p.scale = parseColorBar(colorBarData); // 复用既有混合格式解析器(AlphaScale::Unit)
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
TablePayload parseMeasurementTable(const QJsonObject& data) {
|
||||||
|
TablePayload t;
|
||||||
|
|
||||||
|
// 列定义:优先 filedList,其次 gridHeaderDisplay(grid/trajectory 列表用同形状)。
|
||||||
|
QJsonArray cols = data.value(QStringLiteral("filedList")).toArray();
|
||||||
|
if (cols.isEmpty()) cols = data.value(QStringLiteral("gridHeaderDisplay")).toArray();
|
||||||
|
for (const auto& e : cols) {
|
||||||
|
const QJsonObject c = e.toObject();
|
||||||
|
TableColumn col;
|
||||||
|
col.code = c.value(QStringLiteral("fieldCode")).toString();
|
||||||
|
col.title = c.value(QStringLiteral("name")).toString();
|
||||||
|
if (col.title.isEmpty()) col.title = col.code;
|
||||||
|
t.columns.push_back(col);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 末尾追加“隐藏/显示”开关列(原版每行右侧蓝色药丸开关,全部 ON=可见)。
|
||||||
|
// 仅 measurement 列表有此列(filedList 驱动时);grid/trajectory 走 gridHeaderDisplay 不追加。
|
||||||
|
const bool hasToggleCol = !data.value(QStringLiteral("filedList")).toArray().isEmpty();
|
||||||
|
if (hasToggleCol) {
|
||||||
|
TableColumn toggle;
|
||||||
|
toggle.code = QStringLiteral("displayStatus");
|
||||||
|
toggle.title = QStringLiteral("隐藏/显示");
|
||||||
|
toggle.kind = TableColumnKind::Toggle;
|
||||||
|
t.columns.push_back(toggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 行:每格按列码先查 vmap,再查行对象顶层。
|
||||||
|
// Toggle 列:displayStatus==0 ⇒ ON/可见("1"),否则 OFF("0")。
|
||||||
|
const QJsonArray rowList = data.value(QStringLiteral("rowList")).toArray();
|
||||||
|
for (const auto& e : rowList) {
|
||||||
|
const QJsonObject obj = e.toObject();
|
||||||
|
const QJsonObject vmap = obj.value(QStringLiteral("vmap")).toObject();
|
||||||
|
std::vector<QString> cells;
|
||||||
|
cells.reserve(t.columns.size());
|
||||||
|
for (const auto& col : t.columns) {
|
||||||
|
if (col.kind == TableColumnKind::Toggle) {
|
||||||
|
const int status = obj.value(QStringLiteral("displayStatus")).toInt(0);
|
||||||
|
cells.push_back(status == 0 ? QStringLiteral("1") : QStringLiteral("0"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QJsonValue v = vmap.contains(col.code) ? vmap.value(col.code) : obj.value(col.code);
|
||||||
|
cells.push_back(cellText(v));
|
||||||
|
}
|
||||||
|
t.rows.push_back(std::move(cells));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 总数(分页用):measurement 用 __rowListTotal,回退到本批行数。
|
||||||
|
t.total = data.value(QStringLiteral("__rowListTotal")).toInt(static_cast<int>(t.rows.size()));
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::data::dto
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include "model/detail/DetailPayloads.hpp"
|
||||||
|
|
||||||
|
namespace geopro::data::dto {
|
||||||
|
|
||||||
|
// dd_ert_measurement_data 散点伪剖面:
|
||||||
|
// scatterData = scatter/graph 的 data{ electrodeList[], scatterGraphData{rows[位置数组]} }
|
||||||
|
// colorBarData = colorGradation/getDetail{type:3} 的 data{ properties.colorBar }
|
||||||
|
// 位置数组列序:[1]斜距(x) [3]伪深度(y,负向下) [10]a [11]b [12]m [13]n [16]视电阻率(色值)。
|
||||||
|
// 上色与反演原数据同路径:连续插值(cauto),色值含负异常值不过滤;色阶复用
|
||||||
|
// DatasetChartDto::parseColorBar。图例画在右侧竖条(verticalLegend=true)。
|
||||||
|
geopro::core::ScatterPayload parseMeasurementScatter(const QJsonObject& scatterData,
|
||||||
|
const QJsonObject& colorBarData);
|
||||||
|
|
||||||
|
// 通用列表:data{ filedList[列定义] | gridHeaderDisplay, rowList[键控对象,含 vmap] } → TablePayload。
|
||||||
|
// 列码命中 vmap 时从 vmap 取值,否则从行对象顶层取;值预格式化为 QString。可复用于 grid/trajectory 列表。
|
||||||
|
geopro::core::TablePayload parseMeasurementTable(const QJsonObject& data);
|
||||||
|
|
||||||
|
} // namespace geopro::data::dto
|
||||||
|
|
@ -3,15 +3,14 @@
|
||||||
|
|
||||||
namespace geopro::data {
|
namespace geopro::data {
|
||||||
|
|
||||||
class ChartLoad;
|
class DetailLoad;
|
||||||
class GridLoad;
|
|
||||||
|
|
||||||
// 数据集详情异步仓储抽象。返回自管理句柄(完成/失败后 deleteLater)。
|
// 数据集详情异步仓储抽象。返回自管理句柄(完成/失败后 deleteLater)。
|
||||||
class IAsyncDatasetRepository {
|
class IAsyncDatasetRepository {
|
||||||
public:
|
public:
|
||||||
virtual ~IAsyncDatasetRepository() = default;
|
virtual ~IAsyncDatasetRepository() = default;
|
||||||
virtual ChartLoad* loadChartAsync(const std::string& dsId) = 0; // scatter + 散点色阶(type1)
|
// 通用页签加载(tab 引擎):按 loaderKey 分派,载荷经 QVariant 类型擦除。
|
||||||
virtual GridLoad* loadGridAsync(const std::string& dsId) = 0; // grid(rows) + 色阶(type2) + 异常
|
virtual DetailLoad* loadAsync(const std::string& loaderKey, const std::string& dsId) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace geopro::data
|
} // namespace geopro::data
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,10 @@ target_sources(geopro_tests PRIVATE data/test_parsers.cpp)
|
||||||
target_sources(geopro_tests PRIVATE data/test_local_repo.cpp)
|
target_sources(geopro_tests PRIVATE data/test_local_repo.cpp)
|
||||||
target_sources(geopro_tests PRIVATE data/test_nav_dto.cpp)
|
target_sources(geopro_tests PRIVATE data/test_nav_dto.cpp)
|
||||||
target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp)
|
target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp)
|
||||||
|
target_sources(geopro_tests PRIVATE data/test_measurement_dto.cpp)
|
||||||
target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp)
|
target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp)
|
||||||
|
# 通用仓储分派离线单测(loadAsync 分派 + QVariant payload round-trip)。
|
||||||
|
target_sources(geopro_tests PRIVATE data/test_async_repo_dispatch.cpp)
|
||||||
# NavRequest 离线单测(QVariant payload: done/failed/abort 闸门)。
|
# NavRequest 离线单测(QVariant payload: done/failed/abort 闸门)。
|
||||||
target_sources(geopro_tests PRIVATE data/test_nav_request.cpp)
|
target_sources(geopro_tests PRIVATE data/test_nav_request.cpp)
|
||||||
target_link_libraries(geopro_tests PRIVATE geopro_data)
|
target_link_libraries(geopro_tests PRIVATE geopro_data)
|
||||||
|
|
@ -107,7 +110,7 @@ target_sources(geopro_tests PRIVATE
|
||||||
# 散点 hover 文本格式(inline 纯函数,无需链 qwt/实例化 ScatterHoverTip)。
|
# 散点 hover 文本格式(inline 纯函数,无需链 qwt/实例化 ScatterHoverTip)。
|
||||||
target_sources(geopro_tests PRIVATE app/test_scatter_hover.cpp)
|
target_sources(geopro_tests PRIVATE app/test_scatter_hover.cpp)
|
||||||
|
|
||||||
# controller 层:DatasetDetailController 编排集成测试(QSignalSpy 验证 chartReady/loadFailed)。
|
# controller 层:DatasetDetailController 编排集成测试(QSignalSpy 验证 datasetOpened/tabReady/loadFailed)。
|
||||||
find_package(Qt6 COMPONENTS Test REQUIRED)
|
find_package(Qt6 COMPONENTS Test REQUIRED)
|
||||||
target_sources(geopro_tests PRIVATE controller/test_dataset_detail_controller.cpp)
|
target_sources(geopro_tests PRIVATE controller/test_dataset_detail_controller.cpp)
|
||||||
target_sources(geopro_tests PRIVATE controller/test_workbench_nav_controller.cpp)
|
target_sources(geopro_tests PRIVATE controller/test_workbench_nav_controller.cpp)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
#include "DatasetDetailTab.hpp"
|
||||||
#include "IDatasetChartStrategy.hpp" // geopro::controller(控制器层)
|
#include "IDatasetChartStrategy.hpp" // geopro::controller(控制器层)
|
||||||
|
#include "panels/chart/MeasurementStrategy.hpp"
|
||||||
using namespace geopro::controller;
|
using namespace geopro::controller;
|
||||||
namespace {
|
namespace {
|
||||||
struct Fake : IDatasetChartStrategy {
|
struct Fake : IDatasetChartStrategy {
|
||||||
std::string ddCode() const override { return "dd_inversion_data"; }
|
std::string ddCode() const override { return "dd_inversion_data"; }
|
||||||
bool hasGridPhase() const override { return true; }
|
std::vector<TabSpec> tabs() const override {
|
||||||
|
return {
|
||||||
|
{QStringLiteral("原数据"), ViewKind::Scatter,
|
||||||
|
QStringLiteral("inversion.scatter"), /*lazy*/ false, /*paginated*/ false},
|
||||||
|
{QStringLiteral("网格数据"), ViewKind::FilledContour,
|
||||||
|
QStringLiteral("inversion.grid"), /*lazy*/ true, /*paginated*/ false},
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
TEST(ChartStrategyRegistry, FindsRegisteredAndDegradesUnknown) {
|
TEST(ChartStrategyRegistry, FindsRegisteredAndDegradesUnknown) {
|
||||||
|
|
@ -15,10 +24,31 @@ TEST(ChartStrategyRegistry, FindsRegisteredAndDegradesUnknown) {
|
||||||
EXPECT_FALSE(reg.supports("dd_unknown"));
|
EXPECT_FALSE(reg.supports("dd_unknown"));
|
||||||
EXPECT_EQ(reg.find("dd_unknown"), nullptr);
|
EXPECT_EQ(reg.find("dd_unknown"), nullptr);
|
||||||
}
|
}
|
||||||
TEST(ChartStrategyRegistry, ReportsHasGridPhase) {
|
TEST(ChartStrategyRegistry, ExposesTabSpecsFromStrategy) {
|
||||||
ChartStrategyRegistry reg;
|
ChartStrategyRegistry reg;
|
||||||
reg.add(std::make_unique<Fake>());
|
reg.add(std::make_unique<Fake>());
|
||||||
auto* s = reg.find("dd_inversion_data");
|
auto* s = reg.find("dd_inversion_data");
|
||||||
ASSERT_NE(s, nullptr);
|
ASSERT_NE(s, nullptr);
|
||||||
EXPECT_TRUE(s->hasGridPhase());
|
const auto tabs = s->tabs();
|
||||||
|
ASSERT_EQ(tabs.size(), 2u);
|
||||||
|
EXPECT_FALSE(tabs[0].lazy); // 原数据非 lazy
|
||||||
|
EXPECT_TRUE(tabs[1].lazy); // 网格数据 lazy
|
||||||
|
EXPECT_EQ(tabs[1].kind, ViewKind::FilledContour);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MeasurementStrategy, DrivesTwoTabsScatterAndTable) {
|
||||||
|
geopro::app::MeasurementStrategy s;
|
||||||
|
EXPECT_EQ(s.ddCode(), "dd_ert_measurement_data");
|
||||||
|
const auto tabs = s.tabs();
|
||||||
|
ASSERT_EQ(tabs.size(), 2u);
|
||||||
|
// 散点图:Scatter,非 lazy。
|
||||||
|
EXPECT_EQ(tabs[0].title.toStdString(), std::string("散点图"));
|
||||||
|
EXPECT_EQ(tabs[0].kind, ViewKind::Scatter);
|
||||||
|
EXPECT_FALSE(tabs[0].lazy);
|
||||||
|
EXPECT_EQ(tabs[0].loaderKey.toStdString(), "ert_measurement.scatter");
|
||||||
|
// 数据列表:Table,lazy。
|
||||||
|
EXPECT_EQ(tabs[1].title.toStdString(), std::string("数据列表"));
|
||||||
|
EXPECT_EQ(tabs[1].kind, ViewKind::Table);
|
||||||
|
EXPECT_TRUE(tabs[1].lazy);
|
||||||
|
EXPECT_EQ(tabs[1].loaderKey.toStdString(), "ert_measurement.rows");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include "panels/chart/ScatterHoverTip.hpp"
|
#include "panels/chart/ScatterHoverTip.hpp"
|
||||||
|
|
||||||
|
using geopro::app::electrodeHoverText;
|
||||||
|
using geopro::app::measurementHoverText;
|
||||||
using geopro::app::scatterHoverText;
|
using geopro::app::scatterHoverText;
|
||||||
|
|
||||||
// 对齐原版 Plotly hovertemplate:
|
// 对齐原版 Plotly hovertemplate:
|
||||||
|
|
@ -15,3 +17,16 @@ TEST(ScatterHoverTip, RoundsAndPadsToFixed3) {
|
||||||
EXPECT_EQ(scatterHoverText(-1.0, 0.5, 1.0),
|
EXPECT_EQ(scatterHoverText(-1.0, 0.5, 1.0),
|
||||||
QStringLiteral("<b>X:</b> -1.000<br><b>Y:</b> 0.500<br><b>值:</b> 1.000"));
|
QStringLiteral("<b>X:</b> -1.000<br><b>Y:</b> 0.500<br><b>值:</b> 1.000"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// measurement 浮动框:X/Y/Value/a/b/m/n 多行(对齐原版 Plotly 浮动框)。
|
||||||
|
TEST(ScatterHoverTip, MeasurementFloatingTipShowsXYValueABMN) {
|
||||||
|
const QString t = measurementHoverText(2.283, -1.2, 242.952988, 1, 4, 2, 3);
|
||||||
|
EXPECT_EQ(t, QStringLiteral("<b>X:</b> 2.283<br><b>Y:</b> -1.200<br><b>Value:</b> 242.953"
|
||||||
|
"<br><b>a:</b> 1<br><b>b:</b> 4<br><b>m:</b> 2<br><b>n:</b> 3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 电极浮动框:x(7 位有效数字)/ y: 0 / num(电极编号),对齐原版 Plotly 电极 trace。
|
||||||
|
TEST(ScatterHoverTip, ElectrodeFloatingTipShowsXYNum) {
|
||||||
|
const QString t = electrodeHoverText(5.123837209632222, 4);
|
||||||
|
EXPECT_EQ(t, QStringLiteral("x: 5.123837<br>y: 0<br>num: 4"));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,33 @@
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include <QSignalSpy>
|
#include <QSignalSpy>
|
||||||
|
#include <QVariant>
|
||||||
#include "DatasetDetailController.hpp"
|
#include "DatasetDetailController.hpp"
|
||||||
|
#include "DatasetDetailTab.hpp"
|
||||||
#include "IDatasetChartStrategy.hpp"
|
#include "IDatasetChartStrategy.hpp"
|
||||||
#include "repo/IAsyncDatasetRepository.hpp"
|
#include "repo/IAsyncDatasetRepository.hpp"
|
||||||
#include "api/DatasetLoadHandles.hpp"
|
#include "api/DatasetLoadHandles.hpp"
|
||||||
using namespace geopro;
|
using namespace geopro;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// 反演策略桩:散点 + 网格两阶段。
|
// 反演策略桩:散点(非 lazy) + 网格(lazy) 两页签。
|
||||||
struct InversionStrategy : controller::IDatasetChartStrategy {
|
struct InversionStrategy : controller::IDatasetChartStrategy {
|
||||||
std::string ddCode() const override { return "dd_inversion_data"; }
|
std::string ddCode() const override { return "dd_inversion_data"; }
|
||||||
bool hasGridPhase() const override { return true; }
|
std::vector<controller::TabSpec> tabs() const override {
|
||||||
|
return {
|
||||||
|
{QStringLiteral("原数据"), controller::ViewKind::Scatter,
|
||||||
|
QStringLiteral("inversion.scatter"), /*lazy*/ false, /*paginated*/ false},
|
||||||
|
{QStringLiteral("网格数据"), controller::ViewKind::FilledContour,
|
||||||
|
QStringLiteral("inversion.grid"), /*lazy*/ true, /*paginated*/ false},
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// 无网格阶段策略桩:仅散点(如纯散点类型)。
|
// 单页签策略桩(仅一个非 lazy 页签)。
|
||||||
struct NoGridStrategy : controller::IDatasetChartStrategy {
|
struct SingleTabStrategy : controller::IDatasetChartStrategy {
|
||||||
std::string ddCode() const override { return "dd_scatter_only"; }
|
std::string ddCode() const override { return "dd_scatter_only"; }
|
||||||
bool hasGridPhase() const override { return false; }
|
std::vector<controller::TabSpec> tabs() const override {
|
||||||
|
return {{QStringLiteral("散点"), controller::ViewKind::Scatter,
|
||||||
|
QStringLiteral("scatter.only"), /*lazy*/ false, /*paginated*/ false}};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// 注册了反演策略的注册表(多数用例复用)。
|
// 注册了反演策略的注册表(多数用例复用)。
|
||||||
controller::ChartStrategyRegistry makeInversionRegistry() {
|
controller::ChartStrategyRegistry makeInversionRegistry() {
|
||||||
|
|
@ -24,39 +36,56 @@ controller::ChartStrategyRegistry makeInversionRegistry() {
|
||||||
return reg;
|
return reg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 桩句柄:不声明 Q_OBJECT —— 发射继承自 data::ChartLoad/GridLoad 的信号、override abort。
|
// 桩详情句柄:不声明 Q_OBJECT —— 发射继承自 data::DetailLoad 的信号、override abort。
|
||||||
struct StubChartLoad : data::ChartLoad {
|
struct StubDetailLoad : data::DetailLoad {
|
||||||
bool aborted = false;
|
bool aborted = false;
|
||||||
void abort() override { aborted = true; }
|
void abort() override { aborted = true; }
|
||||||
void fireDone() { emit done(data::ChartParts{}); }
|
void fireDone() { emit done(QVariant{}); }
|
||||||
void fireFailed() { emit failed(QStringLiteral("x")); }
|
|
||||||
};
|
|
||||||
struct StubGridLoad : data::GridLoad {
|
|
||||||
bool aborted = false;
|
|
||||||
void abort() override { aborted = true; }
|
|
||||||
void fireDone() { emit done(data::GridParts{}); }
|
|
||||||
void fireFailed() { emit failed(QStringLiteral("x")); }
|
void fireFailed() { emit failed(QStringLiteral("x")); }
|
||||||
};
|
};
|
||||||
|
// 桩仓储:每个 loaderKey 都造一个新句柄,记录最近一个用于 fire。
|
||||||
struct StubAsyncRepo : data::IAsyncDatasetRepository {
|
struct StubAsyncRepo : data::IAsyncDatasetRepository {
|
||||||
StubChartLoad* lastChart = nullptr;
|
StubDetailLoad* last = nullptr;
|
||||||
StubGridLoad* lastGrid = nullptr;
|
data::DetailLoad* loadAsync(const std::string&, const std::string&) override {
|
||||||
data::ChartLoad* loadChartAsync(const std::string&) override {
|
last = new StubDetailLoad;
|
||||||
lastChart = new StubChartLoad; return lastChart;
|
return last;
|
||||||
}
|
|
||||||
data::GridLoad* loadGridAsync(const std::string&) override {
|
|
||||||
lastGrid = new StubGridLoad; return lastGrid;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
} // namespace
|
||||||
|
|
||||||
TEST(DatasetDetailController, EmitsChartReadyOnDone) {
|
TEST(DatasetDetailController, OpenEmitsDatasetOpenedWithTabsAndDdCode) {
|
||||||
StubAsyncRepo repo;
|
StubAsyncRepo repo;
|
||||||
auto reg = makeInversionRegistry();
|
auto reg = makeInversionRegistry();
|
||||||
controller::DatasetDetailController c(repo, reg);
|
controller::DatasetDetailController c(repo, reg);
|
||||||
QSignalSpy spy(&c, &controller::DatasetDetailController::chartReady);
|
QSignalSpy spy(&c, &controller::DatasetDetailController::datasetOpened);
|
||||||
|
c.openDataset("ds1", "dd_inversion_data", "名称");
|
||||||
|
ASSERT_EQ(spy.count(), 1);
|
||||||
|
const auto args = spy.takeFirst();
|
||||||
|
EXPECT_EQ(args.at(0).toString(), QStringLiteral("ds1"));
|
||||||
|
EXPECT_EQ(args.at(1).toString(), QStringLiteral("dd_inversion_data")); // ddCode 透传
|
||||||
|
EXPECT_EQ(args.at(2).toString(), QStringLiteral("名称"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DatasetDetailController, OpenLoadsNonLazyTabsOnly) {
|
||||||
|
StubAsyncRepo repo;
|
||||||
|
auto reg = makeInversionRegistry();
|
||||||
|
controller::DatasetDetailController c(repo, reg);
|
||||||
|
QSignalSpy started(&c, &controller::DatasetDetailController::tabLoadStarted);
|
||||||
c.openDataset("ds1", "dd_inversion_data");
|
c.openDataset("ds1", "dd_inversion_data");
|
||||||
repo.lastChart->fireDone();
|
// 反演:tab0 非 lazy 自动加载,tab1 lazy 不加载。
|
||||||
EXPECT_EQ(spy.count(), 1);
|
ASSERT_EQ(started.count(), 1);
|
||||||
|
EXPECT_EQ(started.takeFirst().at(1).toInt(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DatasetDetailController, EmitsTabReadyOnDone) {
|
||||||
|
StubAsyncRepo repo;
|
||||||
|
auto reg = makeInversionRegistry();
|
||||||
|
controller::DatasetDetailController c(repo, reg);
|
||||||
|
QSignalSpy spy(&c, &controller::DatasetDetailController::tabReady);
|
||||||
|
c.openDataset("ds1", "dd_inversion_data");
|
||||||
|
repo.last->fireDone();
|
||||||
|
ASSERT_EQ(spy.count(), 1);
|
||||||
|
EXPECT_EQ(spy.takeFirst().at(1).toInt(), 0); // tabIndex 0
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DatasetDetailController, EmitsLoadFailedOnFailed) {
|
TEST(DatasetDetailController, EmitsLoadFailedOnFailed) {
|
||||||
|
|
@ -65,7 +94,7 @@ TEST(DatasetDetailController, EmitsLoadFailedOnFailed) {
|
||||||
controller::DatasetDetailController c(repo, reg);
|
controller::DatasetDetailController c(repo, reg);
|
||||||
QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed);
|
QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed);
|
||||||
c.openDataset("ds1", "dd_inversion_data");
|
c.openDataset("ds1", "dd_inversion_data");
|
||||||
repo.lastChart->fireFailed();
|
repo.last->fireFailed();
|
||||||
EXPECT_EQ(spy.count(), 1);
|
EXPECT_EQ(spy.count(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +105,7 @@ TEST(DatasetDetailController, UnsupportedTypeFailsImmediately) {
|
||||||
QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed);
|
QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed);
|
||||||
c.openDataset("ds1", "dd_other");
|
c.openDataset("ds1", "dd_other");
|
||||||
EXPECT_EQ(spy.count(), 1);
|
EXPECT_EQ(spy.count(), 1);
|
||||||
EXPECT_EQ(repo.lastChart, nullptr); // 未发起加载
|
EXPECT_EQ(repo.last, nullptr); // 未发起加载
|
||||||
}
|
}
|
||||||
|
|
||||||
// 空注册表 → 任意 ddCode 都不支持 → loadFailed,不发起加载。
|
// 空注册表 → 任意 ddCode 都不支持 → loadFailed,不发起加载。
|
||||||
|
|
@ -87,26 +116,38 @@ TEST(DatasetDetailController, EmptyRegistryFailsAnyType) {
|
||||||
QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed);
|
QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed);
|
||||||
c.openDataset("ds1", "dd_inversion_data");
|
c.openDataset("ds1", "dd_inversion_data");
|
||||||
EXPECT_EQ(spy.count(), 1);
|
EXPECT_EQ(spy.count(), 1);
|
||||||
EXPECT_EQ(repo.lastChart, nullptr);
|
EXPECT_EQ(repo.last, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无网格阶段的策略 → loadGridData 不发起网格加载。
|
// 越界 tabIndex → loadTab 静默不加载。
|
||||||
TEST(DatasetDetailController, NoGridPhaseStrategySkipsGridLoad) {
|
TEST(DatasetDetailController, LoadTabOutOfRangeDoesNothing) {
|
||||||
StubAsyncRepo repo;
|
|
||||||
controller::ChartStrategyRegistry reg;
|
|
||||||
reg.add(std::make_unique<NoGridStrategy>());
|
|
||||||
controller::DatasetDetailController c(repo, reg);
|
|
||||||
c.loadGridData("ds1", "dd_scatter_only");
|
|
||||||
EXPECT_EQ(repo.lastGrid, nullptr); // hasGridPhase()==false → 未发起
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(DatasetDetailController, AbortsPreviousOnReopen) {
|
|
||||||
StubAsyncRepo repo;
|
StubAsyncRepo repo;
|
||||||
auto reg = makeInversionRegistry();
|
auto reg = makeInversionRegistry();
|
||||||
controller::DatasetDetailController c(repo, reg);
|
controller::DatasetDetailController c(repo, reg);
|
||||||
c.openDataset("dsA", "dd_inversion_data");
|
c.loadTab("ds1", "dd_inversion_data", 99);
|
||||||
StubChartLoad* a = repo.lastChart;
|
EXPECT_EQ(repo.last, nullptr);
|
||||||
c.openDataset("dsB", "dd_inversion_data"); // 替换
|
}
|
||||||
|
|
||||||
|
// lazy 页签经 loadTab 触发后加载。
|
||||||
|
TEST(DatasetDetailController, LoadTabLazyTabStartsLoad) {
|
||||||
|
StubAsyncRepo repo;
|
||||||
|
auto reg = makeInversionRegistry();
|
||||||
|
controller::DatasetDetailController c(repo, reg);
|
||||||
|
QSignalSpy ready(&c, &controller::DatasetDetailController::tabReady);
|
||||||
|
c.loadTab("ds1", "dd_inversion_data", 1); // lazy 网格页
|
||||||
|
ASSERT_NE(repo.last, nullptr);
|
||||||
|
repo.last->fireDone();
|
||||||
|
ASSERT_EQ(ready.count(), 1);
|
||||||
|
EXPECT_EQ(ready.takeFirst().at(1).toInt(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DatasetDetailController, AbortsPreviousOnSameSlotReload) {
|
||||||
|
StubAsyncRepo repo;
|
||||||
|
auto reg = makeInversionRegistry();
|
||||||
|
controller::DatasetDetailController c(repo, reg);
|
||||||
|
c.loadTab("dsA", "dd_inversion_data", 0);
|
||||||
|
StubDetailLoad* a = repo.last;
|
||||||
|
c.loadTab("dsB", "dd_inversion_data", 0); // 同槽位替换
|
||||||
EXPECT_TRUE(a->aborted); // 旧句柄被 abort
|
EXPECT_TRUE(a->aborted); // 旧句柄被 abort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,49 +155,23 @@ TEST(DatasetDetailController, DropsLateSignalFromAbortedLoad) {
|
||||||
StubAsyncRepo repo;
|
StubAsyncRepo repo;
|
||||||
auto reg = makeInversionRegistry();
|
auto reg = makeInversionRegistry();
|
||||||
controller::DatasetDetailController c(repo, reg);
|
controller::DatasetDetailController c(repo, reg);
|
||||||
QSignalSpy spy(&c, &controller::DatasetDetailController::chartReady);
|
QSignalSpy spy(&c, &controller::DatasetDetailController::tabReady);
|
||||||
c.openDataset("dsA", "dd_inversion_data");
|
c.loadTab("dsA", "dd_inversion_data", 0);
|
||||||
StubChartLoad* a = repo.lastChart;
|
StubDetailLoad* a = repo.last;
|
||||||
c.openDataset("dsB", "dd_inversion_data");
|
c.loadTab("dsB", "dd_inversion_data", 0); // 同槽位替换
|
||||||
StubChartLoad* b = repo.lastChart;
|
StubDetailLoad* b = repo.last;
|
||||||
a->fireDone(); // 旧句柄迟到 → 身份比对丢弃
|
a->fireDone(); // 旧句柄迟到 → 身份比对丢弃
|
||||||
EXPECT_EQ(spy.count(), 0);
|
EXPECT_EQ(spy.count(), 0);
|
||||||
b->fireDone(); // 当前句柄 → 正常
|
b->fireDone(); // 当前句柄 → 正常
|
||||||
EXPECT_EQ(spy.count(), 1);
|
EXPECT_EQ(spy.count(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DatasetDetailController, EmitsGridReadyOnDone) {
|
TEST(DatasetDetailController, FocusEmitsFocusRequested) {
|
||||||
StubAsyncRepo repo;
|
StubAsyncRepo repo;
|
||||||
auto reg = makeInversionRegistry();
|
auto reg = makeInversionRegistry();
|
||||||
controller::DatasetDetailController c(repo, reg);
|
controller::DatasetDetailController c(repo, reg);
|
||||||
QSignalSpy spy(&c, &controller::DatasetDetailController::gridReady);
|
QSignalSpy spy(&c, &controller::DatasetDetailController::focusRequested);
|
||||||
c.loadGridData("ds1", "dd_inversion_data");
|
c.focusDataset("ds1");
|
||||||
repo.lastGrid->fireDone();
|
ASSERT_EQ(spy.count(), 1);
|
||||||
EXPECT_EQ(spy.count(), 1);
|
EXPECT_EQ(spy.takeFirst().at(0).toString(), QStringLiteral("ds1"));
|
||||||
}
|
|
||||||
|
|
||||||
TEST(DatasetDetailController, EmitsLoadFailedOnGridFailed) {
|
|
||||||
StubAsyncRepo repo;
|
|
||||||
auto reg = makeInversionRegistry();
|
|
||||||
controller::DatasetDetailController c(repo, reg);
|
|
||||||
QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed);
|
|
||||||
c.loadGridData("ds1", "dd_inversion_data");
|
|
||||||
repo.lastGrid->fireFailed();
|
|
||||||
EXPECT_EQ(spy.count(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(DatasetDetailController, DropsLateGridSignalFromAbortedLoad) {
|
|
||||||
StubAsyncRepo repo;
|
|
||||||
auto reg = makeInversionRegistry();
|
|
||||||
controller::DatasetDetailController c(repo, reg);
|
|
||||||
QSignalSpy spy(&c, &controller::DatasetDetailController::gridReady);
|
|
||||||
c.loadGridData("dsA", "dd_inversion_data");
|
|
||||||
StubGridLoad* a = repo.lastGrid;
|
|
||||||
c.loadGridData("dsB", "dd_inversion_data"); // 替换 → 旧句柄被 abort
|
|
||||||
StubGridLoad* b = repo.lastGrid;
|
|
||||||
EXPECT_TRUE(a->aborted);
|
|
||||||
a->fireDone(); // 旧句柄迟到 → 身份比对丢弃
|
|
||||||
EXPECT_EQ(spy.count(), 0);
|
|
||||||
b->fireDone(); // 当前句柄 → 正常
|
|
||||||
EXPECT_EQ(spy.count(), 1);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QVariant>
|
||||||
|
#include "ApiClient.hpp"
|
||||||
|
#include "api/ApiDatasetRepository.hpp"
|
||||||
|
#include "api/DatasetLoadHandles.hpp"
|
||||||
|
#include "model/detail/DetailPayloads.hpp"
|
||||||
|
|
||||||
|
using geopro::data::ApiDatasetRepository;
|
||||||
|
using geopro::data::DetailLoad;
|
||||||
|
using geopro::core::ContourPayload;
|
||||||
|
using geopro::core::ScatterPayload;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// ApiClient 构造 + getAsync/postJsonAsync 创建网络句柄需 QCoreApplication(QNAM 事件循环)。
|
||||||
|
QCoreApplication* ensureApp() {
|
||||||
|
if (!QCoreApplication::instance()) {
|
||||||
|
static int argc = 0;
|
||||||
|
static char** argv = nullptr;
|
||||||
|
new QCoreApplication(argc, argv);
|
||||||
|
}
|
||||||
|
return QCoreApplication::instance();
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// 已知 loaderKey 返回非空句柄;返回后立即 abort 取消在飞请求(不依赖网络可达)。
|
||||||
|
TEST(AsyncRepoDispatch, KnownKeysReturnNonNullHandle) {
|
||||||
|
ensureApp();
|
||||||
|
geopro::net::ApiClient api(QStringLiteral("http://tenant.geomative.cn/pop-api"));
|
||||||
|
ApiDatasetRepository repo(api);
|
||||||
|
|
||||||
|
DetailLoad* scatter = repo.loadAsync("inversion.scatter", "ds1");
|
||||||
|
ASSERT_NE(scatter, nullptr);
|
||||||
|
scatter->abort();
|
||||||
|
|
||||||
|
DetailLoad* grid = repo.loadAsync("inversion.grid", "ds1");
|
||||||
|
ASSERT_NE(grid, nullptr);
|
||||||
|
grid->abort();
|
||||||
|
|
||||||
|
DetailLoad* measScatter = repo.loadAsync("ert_measurement.scatter", "ds1");
|
||||||
|
ASSERT_NE(measScatter, nullptr);
|
||||||
|
measScatter->abort();
|
||||||
|
|
||||||
|
DetailLoad* measRows = repo.loadAsync("ert_measurement.rows", "ds1");
|
||||||
|
ASSERT_NE(measRows, nullptr);
|
||||||
|
measRows->abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未知 loaderKey 抛 std::runtime_error。
|
||||||
|
TEST(AsyncRepoDispatch, UnknownKeyThrows) {
|
||||||
|
ensureApp();
|
||||||
|
geopro::net::ApiClient api(QStringLiteral("http://tenant.geomative.cn/pop-api"));
|
||||||
|
ApiDatasetRepository repo(api);
|
||||||
|
EXPECT_THROW(repo.loadAsync("bogus", "ds1"), std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// payload 经 QVariant 类型擦除往返还原(不触网)。
|
||||||
|
TEST(AsyncRepoDispatch, ScatterPayloadRoundTrips) {
|
||||||
|
ScatterPayload in;
|
||||||
|
in.scatter.x = {1.0, 2.0, 3.0};
|
||||||
|
in.scatter.v = {10.0, 20.0, 30.0};
|
||||||
|
QVariant v = QVariant::fromValue(in);
|
||||||
|
ASSERT_TRUE(v.canConvert<ScatterPayload>());
|
||||||
|
ScatterPayload out = v.value<ScatterPayload>();
|
||||||
|
ASSERT_EQ(out.scatter.x.size(), 3u);
|
||||||
|
EXPECT_DOUBLE_EQ(out.scatter.v[2], 30.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AsyncRepoDispatch, ContourPayloadRoundTrips) {
|
||||||
|
ContourPayload in; // Grid{1,1} 占位 + 空异常
|
||||||
|
in.grid.vmin = 1.5;
|
||||||
|
in.grid.vmax = 9.5;
|
||||||
|
QVariant v = QVariant::fromValue(in);
|
||||||
|
ASSERT_TRUE(v.canConvert<ContourPayload>());
|
||||||
|
ContourPayload out = v.value<ContourPayload>();
|
||||||
|
EXPECT_DOUBLE_EQ(out.grid.vmin, 1.5);
|
||||||
|
EXPECT_DOUBLE_EQ(out.grid.vmax, 9.5);
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <QSignalSpy>
|
#include <QSignalSpy>
|
||||||
|
#include <QVariant>
|
||||||
#include "api/DatasetLoadHandles.hpp"
|
#include "api/DatasetLoadHandles.hpp"
|
||||||
|
#include "model/detail/DetailPayloads.hpp"
|
||||||
#include "net/FakeApiCall.hpp"
|
#include "net/FakeApiCall.hpp"
|
||||||
|
|
||||||
using namespace geopro::data;
|
using namespace geopro::data;
|
||||||
using geopro::net::ApiBatch;
|
using geopro::net::ApiBatch;
|
||||||
using geopro::net::ApiResponse;
|
using geopro::net::ApiResponse;
|
||||||
using geopro::net::test::FakeApiCall;
|
using geopro::net::test::FakeApiCall;
|
||||||
|
using geopro::core::ScatterPayload;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
ApiResponse ok() { ApiResponse r; r.code = 200; r.httpStatus = 200; return r; }
|
ApiResponse ok() { ApiResponse r; r.code = 200; r.httpStatus = 200; return r; }
|
||||||
|
|
@ -15,51 +18,54 @@ ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QSt
|
||||||
auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); };
|
auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); };
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DatasetLoadHandles, ChartLoadEmitsDoneOnSuccess) {
|
// ── 通用详情句柄 ApiDetailLoad(tab 引擎):done(QVariant) / failed / abort 闸门 ──
|
||||||
|
|
||||||
|
TEST(DatasetLoadHandles, DetailLoadEmitsDoneWithPayloadOnSuccess) {
|
||||||
auto* a = new FakeApiCall;
|
auto* a = new FakeApiCall;
|
||||||
auto* b = new FakeApiCall;
|
auto* b = new FakeApiCall;
|
||||||
auto* batch = new ApiBatch({a, b}, isFailure);
|
auto* batch = new ApiBatch({a, b}, isFailure);
|
||||||
bool parsed = false;
|
auto* load = new ApiDetailLoad(batch, [](const QList<ApiResponse>& resps) {
|
||||||
auto* load = new ApiChartLoad(batch, [&](const QList<ApiResponse>& resps) {
|
ScatterPayload p;
|
||||||
parsed = (resps.size() == 2);
|
p.scatter.v = std::vector<double>(static_cast<size_t>(resps.size()), 0.0); // 携带可校验状态
|
||||||
return ChartParts{};
|
return QVariant::fromValue(p);
|
||||||
});
|
});
|
||||||
QSignalSpy doneSpy(load, &ChartLoad::done);
|
QSignalSpy doneSpy(load, &DetailLoad::done);
|
||||||
a->fire(ok());
|
a->fire(ok());
|
||||||
b->fire(ok());
|
b->fire(ok());
|
||||||
EXPECT_EQ(doneSpy.count(), 1);
|
ASSERT_EQ(doneSpy.count(), 1);
|
||||||
EXPECT_TRUE(parsed);
|
QVariant payload = doneSpy.takeFirst().at(0).value<QVariant>();
|
||||||
|
ASSERT_TRUE(payload.canConvert<ScatterPayload>());
|
||||||
|
EXPECT_EQ(payload.value<ScatterPayload>().scatter.v.size(), 2u); // round-trip 还原字段
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DatasetLoadHandles, ChartLoadEmitsFailedOnBatchFailure) {
|
TEST(DatasetLoadHandles, DetailLoadEmitsFailedOnBatchFailure) {
|
||||||
auto* a = new FakeApiCall;
|
auto* a = new FakeApiCall;
|
||||||
auto* b = new FakeApiCall;
|
auto* batch = new ApiBatch({a}, isFailure);
|
||||||
auto* batch = new ApiBatch({a, b}, isFailure);
|
auto* load = new ApiDetailLoad(batch, [](const QList<ApiResponse>&) { return QVariant{}; });
|
||||||
auto* load = new ApiChartLoad(batch, [](const QList<ApiResponse>&) { return ChartParts{}; });
|
QSignalSpy failSpy(load, &DetailLoad::failed);
|
||||||
QSignalSpy failSpy(load, &ChartLoad::failed);
|
|
||||||
a->fire(bad());
|
a->fire(bad());
|
||||||
EXPECT_EQ(failSpy.count(), 1);
|
EXPECT_EQ(failSpy.count(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DatasetLoadHandles, ChartLoadEmitsFailedWhenParseThrows) {
|
TEST(DatasetLoadHandles, DetailLoadEmitsFailedWhenParseThrows) {
|
||||||
auto* a = new FakeApiCall;
|
auto* a = new FakeApiCall;
|
||||||
auto* batch = new ApiBatch({a}, isFailure);
|
auto* batch = new ApiBatch({a}, isFailure);
|
||||||
auto* load = new ApiChartLoad(batch, [](const QList<ApiResponse>&) -> ChartParts {
|
auto* load = new ApiDetailLoad(batch, [](const QList<ApiResponse>&) -> QVariant {
|
||||||
throw std::runtime_error("parse boom");
|
throw std::runtime_error("parse boom");
|
||||||
});
|
});
|
||||||
QSignalSpy doneSpy(load, &ChartLoad::done);
|
QSignalSpy doneSpy(load, &DetailLoad::done);
|
||||||
QSignalSpy failSpy(load, &ChartLoad::failed);
|
QSignalSpy failSpy(load, &DetailLoad::failed);
|
||||||
a->fire(ok()); // batch 成功 → parse 抛异常 → failed(emit done 已移出 try)
|
a->fire(ok()); // batch 成功 → parse 抛 → failed(done 已移出 try)
|
||||||
EXPECT_EQ(doneSpy.count(), 0);
|
EXPECT_EQ(doneSpy.count(), 0);
|
||||||
EXPECT_EQ(failSpy.count(), 1);
|
EXPECT_EQ(failSpy.count(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DatasetLoadHandles, GridLoadAbortSuppressesLateDone) {
|
TEST(DatasetLoadHandles, DetailLoadAbortSuppressesLateDone) {
|
||||||
auto* a = new FakeApiCall;
|
auto* a = new FakeApiCall;
|
||||||
auto* batch = new ApiBatch({a}, isFailure);
|
auto* batch = new ApiBatch({a}, isFailure);
|
||||||
auto* load = new ApiGridLoad(batch, [](const QList<ApiResponse>&) { return GridParts{}; });
|
auto* load = new ApiDetailLoad(batch, [](const QList<ApiResponse>&) { return QVariant{}; });
|
||||||
QSignalSpy doneSpy(load, &GridLoad::done);
|
QSignalSpy doneSpy(load, &DetailLoad::done);
|
||||||
load->abort();
|
load->abort();
|
||||||
a->fire(ok()); // 迟到
|
a->fire(ok()); // 迟到
|
||||||
EXPECT_EQ(doneSpy.count(), 0); // batch.aborted_ + load.aborted_ 双闸门
|
EXPECT_EQ(doneSpy.count(), 0); // 双闸门
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include "dto/MeasurementDto.hpp"
|
||||||
|
using namespace geopro::data::dto;
|
||||||
|
|
||||||
|
static QJsonObject obj(const char* json) {
|
||||||
|
return QJsonDocument::fromJson(json).object();
|
||||||
|
}
|
||||||
|
|
||||||
|
// scatter/graph 的 data:electrodeList + scatterGraphData.rows(17 列定位数组,取自真实夹具前 3 行)。
|
||||||
|
static const char* kScatterData = R"({
|
||||||
|
"electrodeList": [
|
||||||
|
{"electrodeNo":1,"horizontalDistance":0,"slopeDistance":0,"pseudoDepth":0},
|
||||||
|
{"electrodeNo":2,"horizontalDistance":1.08,"slopeDistance":1.2459494894830485,"pseudoDepth":0},
|
||||||
|
{"electrodeNo":3,"horizontalDistance":3.15,"slopeDistance":3.3194843662393243,"pseudoDepth":0}
|
||||||
|
],
|
||||||
|
"scatterGraphData": {
|
||||||
|
"hasCoordinate": true,
|
||||||
|
"rows": [
|
||||||
|
[2.115013167852418,2.2827169278611863,null,-1.2,242.952988,2.385,4.116,0,"1453611521843200",24.288,1,4,2,3,12.5664,1,242.952988],
|
||||||
|
[5.124875148343908,5.300065674086864,null,-2.4,-1066.592407,1.873,13.377,0,"1453611521843201",23.12,1,7,3,5,25.1327,2,-1066.592407],
|
||||||
|
[7.994418521491841,8.175900836015376,null,-3.6,100.239388,2.001,9.976,0,"1453611521843202",21.987,1,10,4,7,37.6991,3,100.239388]
|
||||||
|
],
|
||||||
|
"__rowsTotal": 576
|
||||||
|
}
|
||||||
|
})";
|
||||||
|
|
||||||
|
// colorGradation/getDetail{type:3} 的 data:与反演同构混合格式 colorBar(hex + rgba alpha 0–1)。
|
||||||
|
static const char* kColorBarData = R"json({
|
||||||
|
"properties": {
|
||||||
|
"lvlSchemeType": "normal",
|
||||||
|
"colorBar": [
|
||||||
|
["0.00", "#00008B"],
|
||||||
|
["1.51", "rgba(0, 0, 170, 1)"],
|
||||||
|
["208.40", "#FF0000"],
|
||||||
|
["1323.20", "rgba(48, 0, 48, 1)"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})json";
|
||||||
|
|
||||||
|
// measurement/rows 的 data:filedList(列定义)+ rowList(含 vmap 摊平),取自真实夹具前 2 行。
|
||||||
|
static const char* kRowsData = R"({
|
||||||
|
"filedList": [
|
||||||
|
{"fieldCode":"a","name":"A"},
|
||||||
|
{"fieldCode":"b","name":"B"},
|
||||||
|
{"fieldCode":"k","name":"K"},
|
||||||
|
{"fieldCode":"R","name":"电阻"},
|
||||||
|
{"fieldCode":"R0","name":"视电阻率"}
|
||||||
|
],
|
||||||
|
"rowList": [
|
||||||
|
{"id":"1453611521843200","rowNo":1,"displayStatus":0,"a":1,"b":4,"k":12.5664,
|
||||||
|
"vmap":{"R":19.333542,"V":10223.44531,"I":528.793213,"R0_RD":0,"SP":127.640175,"R0":242.952988}},
|
||||||
|
{"id":"1453611521843201","rowNo":2,"displayStatus":0,"a":1,"b":7,"k":25.1327,
|
||||||
|
"vmap":{"R":2.974368,"V":1822.526123,"I":612.743958,"R0_RD":0,"SP":92.380089,"R0":74.753899}}
|
||||||
|
],
|
||||||
|
"__rowListTotal": 576
|
||||||
|
})";
|
||||||
|
|
||||||
|
TEST(MeasurementDto, ParsesScatterPositionalRows) {
|
||||||
|
auto p = parseMeasurementScatter(obj(kScatterData), obj(kColorBarData));
|
||||||
|
const auto& s = p.scatter;
|
||||||
|
|
||||||
|
// 3 个散点;连续上色(cauto,与反演原数据同路径,已无 discreteColor 字段)
|
||||||
|
// + 图例与反演统一为底部横条。
|
||||||
|
EXPECT_FALSE(p.verticalLegend); // measurement 图例改用底部横条(与反演一致)
|
||||||
|
ASSERT_EQ(s.x.size(), 3u);
|
||||||
|
ASSERT_EQ(s.y.size(), 3u);
|
||||||
|
ASSERT_EQ(s.v.size(), 3u);
|
||||||
|
|
||||||
|
// x = col1 斜距;y = col3 伪深度(负);色值 = col16。
|
||||||
|
EXPECT_DOUBLE_EQ(s.x[0], 2.2827169278611863);
|
||||||
|
EXPECT_DOUBLE_EQ(s.y[0], -1.2);
|
||||||
|
EXPECT_LT(s.y[0], 0.0); // 伪深度向下为负
|
||||||
|
EXPECT_DOUBLE_EQ(s.v[0], 242.952988);
|
||||||
|
EXPECT_DOUBLE_EQ(s.x[2], 8.175900836015376);
|
||||||
|
EXPECT_DOUBLE_EQ(s.y[2], -3.6);
|
||||||
|
|
||||||
|
// cauto 关键事实:负异常值(col16)原样保留——视图 setDataRange 据此得 cmin<0,
|
||||||
|
// 使中段视电阻率(≈25–250)归一化到色阶中部(深品红/紫),与原版 Plotly 一致。
|
||||||
|
EXPECT_DOUBLE_EQ(s.v[1], -1066.592407);
|
||||||
|
EXPECT_LT(s.v[1], 0.0); // 负异常值未被过滤
|
||||||
|
const double vMin = std::min({s.v[0], s.v[1], s.v[2]});
|
||||||
|
EXPECT_DOUBLE_EQ(vMin, -1066.592407); // 数据范围下界为负 → cmin<0
|
||||||
|
|
||||||
|
// A/B/M/N = col10-13。
|
||||||
|
ASSERT_EQ(s.a.size(), 3u);
|
||||||
|
EXPECT_DOUBLE_EQ(s.a[0], 1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(s.b[0], 4.0);
|
||||||
|
EXPECT_DOUBLE_EQ(s.m[0], 2.0);
|
||||||
|
EXPECT_DOUBLE_EQ(s.n[0], 3.0);
|
||||||
|
|
||||||
|
// 电极 X = electrodeList.slopeDistance。
|
||||||
|
ASSERT_EQ(s.electrodeX.size(), 3u);
|
||||||
|
EXPECT_DOUBLE_EQ(s.electrodeX[0], 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(s.electrodeX[1], 1.2459494894830485);
|
||||||
|
|
||||||
|
// 电极编号 = electrodeList.electrodeNo(1-based),与 electrodeX 一一对应(供 hover num)。
|
||||||
|
ASSERT_EQ(s.electrodeNo.size(), 3u);
|
||||||
|
EXPECT_DOUBLE_EQ(s.electrodeNo[0], 1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(s.electrodeNo[1], 2.0);
|
||||||
|
EXPECT_DOUBLE_EQ(s.electrodeNo[2], 3.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// scatter/graph 含 scatterGraphConf 时:工具条下拉项 + 默认选中 + x/y 本地重绘备选列。
|
||||||
|
static const char* kScatterDataWithConf = R"({
|
||||||
|
"electrodeList": [
|
||||||
|
{"electrodeNo":1,"horizontalDistance":0,"slopeDistance":0,"pseudoDepth":0}
|
||||||
|
],
|
||||||
|
"scatterGraphConf": {
|
||||||
|
"x": [
|
||||||
|
{"fieldCode":"horizontalDistance","name":"平距"},
|
||||||
|
{"fieldCode":"slopeDistance","name":"斜距"}
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
{"fieldCode":"Layer No","name":"层数"},
|
||||||
|
{"fieldCode":"pseudoDepth","name":"伪深度"},
|
||||||
|
{"fieldCode":"elevationPseudoDepth","name":"伪深度+高程"}
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
{"fieldCode":"I","name":"电流"},
|
||||||
|
{"fieldCode":"R0","name":"视电阻率"},
|
||||||
|
{"fieldCode":"SP","name":"自然电位"}
|
||||||
|
],
|
||||||
|
"vcalculationMethod": [
|
||||||
|
{"fieldCode":null,"name":"线性"},
|
||||||
|
{"fieldCode":null,"name":"对数"},
|
||||||
|
{"fieldCode":null,"name":"导数"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scatterGraphData": {
|
||||||
|
"rows": [
|
||||||
|
[2.115013167852418,2.2827169278611863,null,-1.2,242.952988,2.385,4.116,0,"id0",24.288,1,4,2,3,12.5664,1,242.952988]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})";
|
||||||
|
|
||||||
|
TEST(MeasurementDto, ParsesScatterToolbarConfAndDefaults) {
|
||||||
|
auto p = parseMeasurementScatter(obj(kScatterDataWithConf), obj(kColorBarData));
|
||||||
|
const auto& tb = p.toolbar;
|
||||||
|
|
||||||
|
EXPECT_FALSE(tb.empty());
|
||||||
|
ASSERT_EQ(tb.x.size(), 2u);
|
||||||
|
ASSERT_EQ(tb.y.size(), 3u);
|
||||||
|
ASSERT_EQ(tb.v.size(), 3u);
|
||||||
|
ASSERT_EQ(tb.method.size(), 3u);
|
||||||
|
|
||||||
|
// 选项名/码(顺序保留)。
|
||||||
|
EXPECT_EQ(tb.x[0].code.toStdString(), "horizontalDistance");
|
||||||
|
EXPECT_EQ(tb.x[0].name.toStdString(), std::string("平距"));
|
||||||
|
EXPECT_EQ(tb.x[1].code.toStdString(), "slopeDistance");
|
||||||
|
EXPECT_EQ(tb.method[0].name.toStdString(), std::string("线性"));
|
||||||
|
EXPECT_TRUE(tb.method[0].code.isEmpty()); // method 的 fieldCode 为 null
|
||||||
|
|
||||||
|
// 默认选中:斜距 / 伪深度 / 视电阻率 / 线性。
|
||||||
|
EXPECT_EQ(tb.defaultX.toStdString(), "slopeDistance");
|
||||||
|
EXPECT_EQ(tb.defaultY.toStdString(), "pseudoDepth");
|
||||||
|
EXPECT_EQ(tb.defaultV.toStdString(), "R0");
|
||||||
|
EXPECT_EQ(tb.defaultMethod.toStdString(), std::string("线性"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MeasurementDto, CarriesAltColumnsForLocalReplot) {
|
||||||
|
auto p = parseMeasurementScatter(obj(kScatterDataWithConf), obj(kColorBarData));
|
||||||
|
|
||||||
|
// x 备选:平距(col0) / 斜距(col1);y 备选:伪深度(col3) / 伪深度+高程(col9)。
|
||||||
|
ASSERT_EQ(p.altXHorizontal.size(), 1u);
|
||||||
|
ASSERT_EQ(p.altXSlope.size(), 1u);
|
||||||
|
ASSERT_EQ(p.altYPseudo.size(), 1u);
|
||||||
|
ASSERT_EQ(p.altYElevationPseudo.size(), 1u);
|
||||||
|
EXPECT_DOUBLE_EQ(p.altXHorizontal[0], 2.115013167852418);
|
||||||
|
EXPECT_DOUBLE_EQ(p.altXSlope[0], 2.2827169278611863);
|
||||||
|
EXPECT_DOUBLE_EQ(p.altYPseudo[0], -1.2);
|
||||||
|
EXPECT_DOUBLE_EQ(p.altYElevationPseudo[0], 24.288);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MeasurementDto, ToolbarEmptyWhenNoConf) {
|
||||||
|
// 无 scatterGraphConf(反演原数据形状):工具条空 → 视图渲染反演工具条。
|
||||||
|
auto p = parseMeasurementScatter(obj(kScatterData), obj(kColorBarData));
|
||||||
|
EXPECT_TRUE(p.toolbar.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MeasurementDto, ParsesScatterColorBarOpaque) {
|
||||||
|
auto p = parseMeasurementScatter(obj(kScatterData), obj(kColorBarData));
|
||||||
|
auto stops = p.scale.stops();
|
||||||
|
ASSERT_EQ(stops.size(), 4u);
|
||||||
|
// 首/末值。
|
||||||
|
EXPECT_DOUBLE_EQ(stops.front().first, 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(stops.back().first, 1323.20);
|
||||||
|
// 首色 #00008B(深蓝),末色 rgba(48,0,48,1)(alpha=1 → 255,不透明)。
|
||||||
|
EXPECT_EQ(stops.front().second.r, 0);
|
||||||
|
EXPECT_EQ(stops.front().second.g, 0);
|
||||||
|
EXPECT_EQ(stops.front().second.b, 0x8B);
|
||||||
|
EXPECT_EQ(stops.front().second.a, 255);
|
||||||
|
EXPECT_EQ(stops.back().second.r, 48);
|
||||||
|
EXPECT_EQ(stops.back().second.g, 0);
|
||||||
|
EXPECT_EQ(stops.back().second.b, 48);
|
||||||
|
EXPECT_EQ(stops.back().second.a, 255); // 回归:alpha=1 须映射为 255 不透明
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MeasurementDto, ParsesTableColumnsAndVmapFlattened) {
|
||||||
|
auto t = parseMeasurementTable(obj(kRowsData));
|
||||||
|
|
||||||
|
// 列来自 filedList(5 列)+ 末尾追加“隐藏/显示”开关列 = 6 列。
|
||||||
|
ASSERT_EQ(t.columns.size(), 6u);
|
||||||
|
EXPECT_EQ(t.columns[0].code.toStdString(), "a");
|
||||||
|
EXPECT_EQ(t.columns[0].title.toStdString(), "A");
|
||||||
|
EXPECT_EQ(t.columns[4].code.toStdString(), "R0");
|
||||||
|
EXPECT_EQ(t.columns[4].title.toStdString(), std::string("视电阻率"));
|
||||||
|
|
||||||
|
// 2 行;total 来自 __rowListTotal。
|
||||||
|
ASSERT_EQ(t.rows.size(), 2u);
|
||||||
|
EXPECT_EQ(t.total, 576);
|
||||||
|
|
||||||
|
// 顶层列(a/b/k)从行对象取;vmap 列(R/R0)从 vmap 摊平。
|
||||||
|
ASSERT_EQ(t.rows[0].size(), 6u);
|
||||||
|
EXPECT_EQ(t.rows[0][0].toStdString(), "1"); // a(整数无小数点)
|
||||||
|
EXPECT_EQ(t.rows[0][1].toStdString(), "4"); // b
|
||||||
|
EXPECT_EQ(t.rows[0][3].toStdString(), "19.333542"); // R from vmap
|
||||||
|
EXPECT_EQ(t.rows[0][4].toStdString(), "242.952988"); // R0 from vmap
|
||||||
|
EXPECT_EQ(t.rows[1][4].toStdString(), "74.753899");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MeasurementDto, AppendsHideShowToggleColumn) {
|
||||||
|
auto t = parseMeasurementTable(obj(kRowsData));
|
||||||
|
|
||||||
|
// 末列为“隐藏/显示”,kind=Toggle,code=displayStatus。
|
||||||
|
ASSERT_EQ(t.columns.size(), 6u);
|
||||||
|
const auto& toggle = t.columns.back();
|
||||||
|
EXPECT_EQ(toggle.title.toStdString(), std::string("隐藏/显示"));
|
||||||
|
EXPECT_EQ(toggle.code.toStdString(), "displayStatus");
|
||||||
|
EXPECT_EQ(toggle.kind, geopro::core::TableColumnKind::Toggle);
|
||||||
|
|
||||||
|
// displayStatus==0 ⇒ ON/可见("1"),全部 ON。
|
||||||
|
ASSERT_EQ(t.rows.size(), 2u);
|
||||||
|
EXPECT_EQ(t.rows[0].back().toStdString(), "1");
|
||||||
|
EXPECT_EQ(t.rows[1].back().toStdString(), "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MeasurementDto, ToggleOffWhenDisplayStatusNonZero) {
|
||||||
|
// displayStatus!=0 ⇒ OFF("0")。
|
||||||
|
auto t = parseMeasurementTable(obj(R"({
|
||||||
|
"filedList": [{"fieldCode":"a","name":"A"}],
|
||||||
|
"rowList": [
|
||||||
|
{"id":"x","a":1,"displayStatus":0},
|
||||||
|
{"id":"y","a":2,"displayStatus":1}
|
||||||
|
]
|
||||||
|
})"));
|
||||||
|
ASSERT_EQ(t.columns.size(), 2u);
|
||||||
|
EXPECT_EQ(t.columns.back().kind, geopro::core::TableColumnKind::Toggle);
|
||||||
|
ASSERT_EQ(t.rows.size(), 2u);
|
||||||
|
EXPECT_EQ(t.rows[0].back().toStdString(), "1"); // displayStatus 0 → ON
|
||||||
|
EXPECT_EQ(t.rows[1].back().toStdString(), "0"); // displayStatus 1 → OFF
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "成功",
|
||||||
|
"data": {
|
||||||
|
"id": null,
|
||||||
|
"projectId": "1438889436225536",
|
||||||
|
"templateId": "1439864962138112",
|
||||||
|
"dsObjectId": null,
|
||||||
|
"tmObjectId": null,
|
||||||
|
"graphicArea": 0,
|
||||||
|
"properties": {
|
||||||
|
"lvlSchemeType": "normal",
|
||||||
|
"colorBar": [
|
||||||
|
["0.00", "#00008B"],
|
||||||
|
["1.51", "rgba(0, 0, 170, 1)"],
|
||||||
|
["2.27", "rgba(0, 0, 211, 1)"],
|
||||||
|
["3.43", "#0000FF"],
|
||||||
|
["5.17", "rgba(0, 128, 255, 1)"],
|
||||||
|
["7.79", "#00FFFF"],
|
||||||
|
["11.75", "rgba(0, 192, 128, 1)"],
|
||||||
|
["17.72", "#00FF00"],
|
||||||
|
["26.73", "rgba(0, 128, 0, 1)"],
|
||||||
|
["40.30", "rgba(128, 192, 0, 1)"],
|
||||||
|
["60.77", "#FFFF00"],
|
||||||
|
["91.64", "rgba(191, 128, 0, 1)"],
|
||||||
|
["138.20", "rgba(255, 128, 0, 1)"],
|
||||||
|
["208.40", "#FF0000"],
|
||||||
|
["314.20", "rgba(211, 0, 0, 1)"],
|
||||||
|
["473.80", "rgba(132, 0, 64, 1)"],
|
||||||
|
["714.50", "rgba(96, 0, 96, 1)"],
|
||||||
|
["1323.20", "rgba(48, 0, 48, 1)"]
|
||||||
|
],
|
||||||
|
"equalAreaLayerCount": 10,
|
||||||
|
"labelConfig": { "showLabels": true, "color": "#000000" },
|
||||||
|
"logLinesCount": 8,
|
||||||
|
"lineConfig": { "showLines": true, "color": "#000000", "lineType": "dashed" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,575 @@
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "成功",
|
||||||
|
"data": {
|
||||||
|
"filedList": [
|
||||||
|
{
|
||||||
|
"fieldCode": "a",
|
||||||
|
"name": "A",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"A"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "b",
|
||||||
|
"name": "B",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"B"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "m",
|
||||||
|
"name": "M",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"M"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "n",
|
||||||
|
"name": "N",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"N"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "k",
|
||||||
|
"name": "K",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"K"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "stacking",
|
||||||
|
"name": "Stacking",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"Stacking"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "I",
|
||||||
|
"name": "电流",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"I(mA)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "V",
|
||||||
|
"name": "电压",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"V(mV)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "R",
|
||||||
|
"name": "电阻",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"R(Ohm)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "R0",
|
||||||
|
"name": "视电阻率",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"R0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "SP",
|
||||||
|
"name": "自然电位",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"SP"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "R0_RD",
|
||||||
|
"name": "电阻误差",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"R0_RD"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rowList": [
|
||||||
|
{
|
||||||
|
"id": "1453611521843200",
|
||||||
|
"rowNo": 1,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 2.115013167852418,
|
||||||
|
"slopeDistance": 2.2827169278611863,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -1.2,
|
||||||
|
"elevationPseudoDepth": 24.288,
|
||||||
|
"n": 3,
|
||||||
|
"a": 1,
|
||||||
|
"b": 4,
|
||||||
|
"m": 2,
|
||||||
|
"k": 12.5664,
|
||||||
|
"vmap": {
|
||||||
|
"R": 19.333542,
|
||||||
|
"V": 10223.44531,
|
||||||
|
"I": 528.793213,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": 127.640175,
|
||||||
|
"R0": 242.952988
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521843201",
|
||||||
|
"rowNo": 2,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 5.124875148343908,
|
||||||
|
"slopeDistance": 5.300065674086864,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -2.4,
|
||||||
|
"elevationPseudoDepth": 23.12,
|
||||||
|
"n": 5,
|
||||||
|
"a": 1,
|
||||||
|
"b": 7,
|
||||||
|
"m": 3,
|
||||||
|
"k": 25.1327,
|
||||||
|
"vmap": {
|
||||||
|
"R": 2.974368,
|
||||||
|
"V": 1822.526123,
|
||||||
|
"I": 612.743958,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": 92.380089,
|
||||||
|
"R0": 74.753899
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521843202",
|
||||||
|
"rowNo": 3,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 7.994418521491841,
|
||||||
|
"slopeDistance": 8.175900836015376,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -3.6,
|
||||||
|
"elevationPseudoDepth": 21.987,
|
||||||
|
"n": 7,
|
||||||
|
"a": 1,
|
||||||
|
"b": 10,
|
||||||
|
"m": 4,
|
||||||
|
"k": 37.6991,
|
||||||
|
"vmap": {
|
||||||
|
"R": 2.658933,
|
||||||
|
"V": 886.042603,
|
||||||
|
"I": 333.232361,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": -186.630417,
|
||||||
|
"R0": 100.239388
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521843203",
|
||||||
|
"rowNo": 4,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 8.00601333013422,
|
||||||
|
"slopeDistance": 8.186794593291953,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -3.6,
|
||||||
|
"elevationPseudoDepth": 22.020999999999997,
|
||||||
|
"n": 6,
|
||||||
|
"a": 1,
|
||||||
|
"b": 10,
|
||||||
|
"m": 5,
|
||||||
|
"k": 125.663696,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.561675,
|
||||||
|
"V": 187.168427,
|
||||||
|
"I": 333.232361,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": -129.25145,
|
||||||
|
"R0": 70.582214
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521843204",
|
||||||
|
"rowNo": 5,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 11.066663821597649,
|
||||||
|
"slopeDistance": 11.272344909478555,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -4.8,
|
||||||
|
"elevationPseudoDepth": 20.9685,
|
||||||
|
"n": 9,
|
||||||
|
"a": 1,
|
||||||
|
"b": 13,
|
||||||
|
"m": 5,
|
||||||
|
"k": 50.265499,
|
||||||
|
"vmap": {
|
||||||
|
"R": 1.496101,
|
||||||
|
"V": 528.129211,
|
||||||
|
"I": 353.003784,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": -22.242311,
|
||||||
|
"R0": 75.202255
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521843205",
|
||||||
|
"rowNo": 6,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 11.00295159513125,
|
||||||
|
"slopeDistance": 11.184890199081117,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -4.8,
|
||||||
|
"elevationPseudoDepth": 20.758499999999998,
|
||||||
|
"n": 8,
|
||||||
|
"a": 1,
|
||||||
|
"b": 13,
|
||||||
|
"m": 6,
|
||||||
|
"k": 109.955704,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.941678,
|
||||||
|
"V": 332.415863,
|
||||||
|
"I": 353.003784,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": 288.221405,
|
||||||
|
"R0": 103.542862
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521859584",
|
||||||
|
"rowNo": 7,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 13.92101964767117,
|
||||||
|
"slopeDistance": 14.166540305149894,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -6,
|
||||||
|
"elevationPseudoDepth": 19.490499999999997,
|
||||||
|
"n": 11,
|
||||||
|
"a": 1,
|
||||||
|
"b": 16,
|
||||||
|
"m": 6,
|
||||||
|
"k": 62.831902,
|
||||||
|
"vmap": {
|
||||||
|
"R": 1.357018,
|
||||||
|
"V": 693.766174,
|
||||||
|
"I": 511.243073,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": 289.488678,
|
||||||
|
"R0": 85.264038
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521859585",
|
||||||
|
"rowNo": 8,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 14.000862630669717,
|
||||||
|
"slopeDistance": 14.247085672002468,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -6,
|
||||||
|
"elevationPseudoDepth": 19.456,
|
||||||
|
"n": 10,
|
||||||
|
"a": 1,
|
||||||
|
"b": 16,
|
||||||
|
"m": 7,
|
||||||
|
"k": 113.097298,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.557539,
|
||||||
|
"V": 285.037994,
|
||||||
|
"I": 511.243073,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": -35.634811,
|
||||||
|
"R0": 63.056168
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521859586",
|
||||||
|
"rowNo": 9,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 14.063602086594681,
|
||||||
|
"slopeDistance": 14.27044051526772,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -6,
|
||||||
|
"elevationPseudoDepth": 19.706,
|
||||||
|
"n": 9,
|
||||||
|
"a": 1,
|
||||||
|
"b": 16,
|
||||||
|
"m": 8,
|
||||||
|
"k": 351.858398,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.125825,
|
||||||
|
"V": 64.3274,
|
||||||
|
"I": 511.243073,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": -187.302017,
|
||||||
|
"R0": 44.272751
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521859587",
|
||||||
|
"rowNo": 10,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 16.987234838304097,
|
||||||
|
"slopeDistance": 17.233999253766825,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -7.2,
|
||||||
|
"elevationPseudoDepth": 18.296499999999998,
|
||||||
|
"n": 13,
|
||||||
|
"a": 1,
|
||||||
|
"b": 19,
|
||||||
|
"m": 7,
|
||||||
|
"k": 75.398201,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.889478,
|
||||||
|
"V": 318.904541,
|
||||||
|
"I": 358.53006,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": -63.969704,
|
||||||
|
"R0": 67.065033
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521859588",
|
||||||
|
"rowNo": 11,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 17.03278556632032,
|
||||||
|
"slopeDistance": 17.279602362635487,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -7.2,
|
||||||
|
"elevationPseudoDepth": 18.24,
|
||||||
|
"n": 12,
|
||||||
|
"a": 1,
|
||||||
|
"b": 19,
|
||||||
|
"m": 8,
|
||||||
|
"k": 120.951302,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.585498,
|
||||||
|
"V": 209.918564,
|
||||||
|
"I": 358.53006,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": -268.122803,
|
||||||
|
"R0": 70.816719
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521859589",
|
||||||
|
"rowNo": 12,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 16.9816701391346,
|
||||||
|
"slopeDistance": 17.2520906213365,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -7.2,
|
||||||
|
"elevationPseudoDepth": 18.438,
|
||||||
|
"n": 11,
|
||||||
|
"a": 1,
|
||||||
|
"b": 19,
|
||||||
|
"m": 9,
|
||||||
|
"k": 251.327393,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.166266,
|
||||||
|
"V": 59.611279,
|
||||||
|
"I": 358.53006,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": 187.193558,
|
||||||
|
"R0": 41.787144
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521859590",
|
||||||
|
"rowNo": 13,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 20.01061875986487,
|
||||||
|
"slopeDistance": 20.259070486101244,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -8.4,
|
||||||
|
"elevationPseudoDepth": 17.037,
|
||||||
|
"n": 15,
|
||||||
|
"a": 1,
|
||||||
|
"b": 22,
|
||||||
|
"m": 8,
|
||||||
|
"k": 87.9646,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.778783,
|
||||||
|
"V": 222.046936,
|
||||||
|
"I": 285.120575,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": -188.105652,
|
||||||
|
"R0": 68.505302
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521859591",
|
||||||
|
"rowNo": 14,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 19.924955341057455,
|
||||||
|
"slopeDistance": 20.196078557097973,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -8.4,
|
||||||
|
"elevationPseudoDepth": 17.301000000000002,
|
||||||
|
"n": 14,
|
||||||
|
"a": 1,
|
||||||
|
"b": 22,
|
||||||
|
"m": 9,
|
||||||
|
"k": 130.690308,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.586209,
|
||||||
|
"V": 167.140335,
|
||||||
|
"I": 285.120575,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": -30.588928,
|
||||||
|
"R0": 76.61187
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521859592",
|
||||||
|
"rowNo": 15,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 19.94235941950773,
|
||||||
|
"slopeDistance": 20.25312046337076,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -8.4,
|
||||||
|
"elevationPseudoDepth": 17.003500000000003,
|
||||||
|
"n": 13,
|
||||||
|
"a": 1,
|
||||||
|
"b": 22,
|
||||||
|
"m": 10,
|
||||||
|
"k": 226.194595,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.258264,
|
||||||
|
"V": 73.636406,
|
||||||
|
"I": 285.120575,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": -38.283382,
|
||||||
|
"R0": 58.417942
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521859593",
|
||||||
|
"rowNo": 16,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 19.95085361886024,
|
||||||
|
"slopeDistance": 20.261252468704264,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -8.4,
|
||||||
|
"elevationPseudoDepth": 16.972,
|
||||||
|
"n": 12,
|
||||||
|
"a": 1,
|
||||||
|
"b": 22,
|
||||||
|
"m": 11,
|
||||||
|
"k": 691.150391,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.219839,
|
||||||
|
"V": 62.776394,
|
||||||
|
"I": 285.55603,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": -264.453094,
|
||||||
|
"R0": 151.941925
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521859594",
|
||||||
|
"rowNo": 17,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 22.95679663366693,
|
||||||
|
"slopeDistance": 23.231100221926333,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -9.6,
|
||||||
|
"elevationPseudoDepth": 16.131,
|
||||||
|
"n": 17,
|
||||||
|
"a": 1,
|
||||||
|
"b": 25,
|
||||||
|
"m": 9,
|
||||||
|
"k": 100.530998,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.865843,
|
||||||
|
"V": 235.531586,
|
||||||
|
"I": 272.025665,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": 252.63089,
|
||||||
|
"R0": 87.04409
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521859595",
|
||||||
|
"rowNo": 18,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 22.822828222784292,
|
||||||
|
"slopeDistance": 23.136758664349642,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -9.6,
|
||||||
|
"elevationPseudoDepth": 15.8315,
|
||||||
|
"n": 16,
|
||||||
|
"a": 1,
|
||||||
|
"b": 25,
|
||||||
|
"m": 10,
|
||||||
|
"k": 141.371704,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.636254,
|
||||||
|
"V": 173.077347,
|
||||||
|
"I": 272.025665,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": 17.840714,
|
||||||
|
"R0": 89.94828
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521867776",
|
||||||
|
"rowNo": 19,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 22.928686812404784,
|
||||||
|
"slopeDistance": 23.24072059217002,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -9.6,
|
||||||
|
"elevationPseudoDepth": 15.769,
|
||||||
|
"n": 15,
|
||||||
|
"a": 1,
|
||||||
|
"b": 25,
|
||||||
|
"m": 11,
|
||||||
|
"k": 219.911407,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.112888,
|
||||||
|
"V": 30.708496,
|
||||||
|
"I": 272.025665,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": -189.315582,
|
||||||
|
"R0": 24.825411
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1453611521867777",
|
||||||
|
"rowNo": 20,
|
||||||
|
"displayStatus": 0,
|
||||||
|
"horizontalDistance": 22.8941388207831,
|
||||||
|
"slopeDistance": 23.205240404465737,
|
||||||
|
"layerNo": null,
|
||||||
|
"pseudoDepth": -9.6,
|
||||||
|
"elevationPseudoDepth": 15.835000000000003,
|
||||||
|
"n": 14,
|
||||||
|
"a": 1,
|
||||||
|
"b": 25,
|
||||||
|
"m": 12,
|
||||||
|
"k": 449.247803,
|
||||||
|
"vmap": {
|
||||||
|
"R": 0.086995,
|
||||||
|
"V": 23.698902,
|
||||||
|
"I": 272.416779,
|
||||||
|
"R0_RD": 0,
|
||||||
|
"SP": 45.706585,
|
||||||
|
"R0": 39.082321
|
||||||
|
},
|
||||||
|
"stacking": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"__rowListTotal": 576
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,830 @@
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "成功",
|
||||||
|
"data": {
|
||||||
|
"electrodeList": [
|
||||||
|
{
|
||||||
|
"electrodeNo": 1,
|
||||||
|
"horizontalDistance": 0,
|
||||||
|
"slopeDistance": 0,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 26.172
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 2,
|
||||||
|
"horizontalDistance": 1.0801616223246715,
|
||||||
|
"slopeDistance": 1.2459494894830485,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.551
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 3,
|
||||||
|
"horizontalDistance": 3.1498647133801647,
|
||||||
|
"slopeDistance": 3.3194843662393243,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.425
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 4,
|
||||||
|
"horizontalDistance": 4.943098993517602,
|
||||||
|
"slopeDistance": 5.123837209632222,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 5,
|
||||||
|
"horizontalDistance": 7.0998855833076515,
|
||||||
|
"slopeDistance": 7.280646981934403,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.615
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 6,
|
||||||
|
"horizontalDistance": 8.912141076960786,
|
||||||
|
"slopeDistance": 9.092942204649502,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.627
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 7,
|
||||||
|
"horizontalDistance": 11.045738049466081,
|
||||||
|
"slopeDistance": 11.227964462398532,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.549
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 8,
|
||||||
|
"horizontalDistance": 13.093762113301716,
|
||||||
|
"slopeDistance": 13.276838193512734,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.49
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 9,
|
||||||
|
"horizontalDistance": 15.033442059887646,
|
||||||
|
"slopeDistance": 15.264042837022707,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.922
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 10,
|
||||||
|
"horizontalDistance": 16.955987211873353,
|
||||||
|
"slopeDistance": 17.266206881606404,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.363
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 11,
|
||||||
|
"horizontalDistance": 18.929898218381553,
|
||||||
|
"slopeDistance": 19.24013840565029,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.354
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 12,
|
||||||
|
"horizontalDistance": 20.971809019338927,
|
||||||
|
"slopeDistance": 21.282366531758235,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.39
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 13,
|
||||||
|
"horizontalDistance": 22.92873162714211,
|
||||||
|
"slopeDistance": 23.240034045135115,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.444
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 14,
|
||||||
|
"horizontalDistance": 24.816468622227266,
|
||||||
|
"slopeDistance": 25.12811427717324,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 15,
|
||||||
|
"horizontalDistance": 26.92747540642802,
|
||||||
|
"slopeDistance": 27.241302778689754,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.384
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 16,
|
||||||
|
"horizontalDistance": 28.68966923369523,
|
||||||
|
"slopeDistance": 29.007310447092884,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 17,
|
||||||
|
"horizontalDistance": 30.880151207446215,
|
||||||
|
"slopeDistance": 31.19815760682996,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.54
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 18,
|
||||||
|
"horizontalDistance": 32.7659271758147,
|
||||||
|
"slopeDistance": 33.084258347213705,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.505
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 19,
|
||||||
|
"horizontalDistance": 34.71171810948364,
|
||||||
|
"slopeDistance": 35.03134421012479,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.576
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 20,
|
||||||
|
"horizontalDistance": 36.644544469129066,
|
||||||
|
"slopeDistance": 36.96419152342716,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.567
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 21,
|
||||||
|
"horizontalDistance": 38.31639860471849,
|
||||||
|
"slopeDistance": 38.640204724654254,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.685
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 22,
|
||||||
|
"horizontalDistance": 40.713354415497676,
|
||||||
|
"slopeDistance": 41.050580869331284,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.431
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 23,
|
||||||
|
"horizontalDistance": 42.28359724152586,
|
||||||
|
"slopeDistance": 42.626616253671685,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.296
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 24,
|
||||||
|
"horizontalDistance": 44.00336585439301,
|
||||||
|
"slopeDistance": 44.351366984010134,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.427
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 25,
|
||||||
|
"horizontalDistance": 45.95916673864363,
|
||||||
|
"slopeDistance": 46.30721797496919,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.413
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 26,
|
||||||
|
"horizontalDistance": 48.03509295173176,
|
||||||
|
"slopeDistance": 48.385051135303875,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.502
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 27,
|
||||||
|
"horizontalDistance": 50.00439250362521,
|
||||||
|
"slopeDistance": 50.35438140854088,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.513
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 28,
|
||||||
|
"horizontalDistance": 52.15587463692657,
|
||||||
|
"slopeDistance": 52.50823292847325,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.614
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 29,
|
||||||
|
"horizontalDistance": 54.31927360220433,
|
||||||
|
"slopeDistance": 54.671698685762905,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.597
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 30,
|
||||||
|
"horizontalDistance": 56.070653463392375,
|
||||||
|
"slopeDistance": 56.42330235616756,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.625
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 31,
|
||||||
|
"horizontalDistance": 58.3971778197452,
|
||||||
|
"slopeDistance": 58.820309778079924,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 26.202
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 32,
|
||||||
|
"horizontalDistance": 60.267675140394914,
|
||||||
|
"slopeDistance": 60.69681190346048,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 26.052
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 33,
|
||||||
|
"horizontalDistance": 62.3517973628095,
|
||||||
|
"slopeDistance": 62.78119537057067,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 26.085
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 34,
|
||||||
|
"horizontalDistance": 64.26099256115273,
|
||||||
|
"slopeDistance": 64.69219388061114,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 26.002
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 35,
|
||||||
|
"horizontalDistance": 66.23694132533387,
|
||||||
|
"slopeDistance": 66.66845259815523,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 25.967
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 36,
|
||||||
|
"horizontalDistance": 68.50618870757245,
|
||||||
|
"slopeDistance": 68.9401279010532,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 26.072
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 37,
|
||||||
|
"horizontalDistance": 70.12425130226687,
|
||||||
|
"slopeDistance": 70.55912498577858,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 26.127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 38,
|
||||||
|
"horizontalDistance": 72.45685720369549,
|
||||||
|
"slopeDistance": 72.90445786010146,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 26.371
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 39,
|
||||||
|
"horizontalDistance": 74.330372956236,
|
||||||
|
"slopeDistance": 74.85313876651416,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 26.907
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"electrodeNo": 40,
|
||||||
|
"horizontalDistance": 75.76722057945078,
|
||||||
|
"slopeDistance": 76.30859577114809,
|
||||||
|
"layerNo": 0,
|
||||||
|
"pseudoDepth": 0,
|
||||||
|
"elevationPseudoDepth": 27.139
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scatterGraphConf": {
|
||||||
|
"canFilter": null,
|
||||||
|
"x": [
|
||||||
|
{
|
||||||
|
"fieldCode": "horizontalDistance",
|
||||||
|
"name": "平距",
|
||||||
|
"sheetHeaderNameList": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "slopeDistance",
|
||||||
|
"name": "斜距",
|
||||||
|
"sheetHeaderNameList": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
{
|
||||||
|
"fieldCode": "I",
|
||||||
|
"name": "电流",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"I(mA)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "V",
|
||||||
|
"name": "电压",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"V(mV)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "R",
|
||||||
|
"name": "电阻",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"R(Ohm)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "R0",
|
||||||
|
"name": "视电阻率",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"R0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "SP",
|
||||||
|
"name": "自然电位",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"SP"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "R0_RD",
|
||||||
|
"name": "电阻误差",
|
||||||
|
"sheetHeaderNameList": [
|
||||||
|
"R0_RD"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
{
|
||||||
|
"fieldCode": "Layer No",
|
||||||
|
"name": "层数",
|
||||||
|
"sheetHeaderNameList": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "pseudoDepth",
|
||||||
|
"name": "伪深度",
|
||||||
|
"sheetHeaderNameList": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": "elevationPseudoDepth",
|
||||||
|
"name": "伪深度+高程",
|
||||||
|
"sheetHeaderNameList": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vcalculationMethod": [
|
||||||
|
{
|
||||||
|
"fieldCode": null,
|
||||||
|
"name": "线性",
|
||||||
|
"sheetHeaderNameList": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": null,
|
||||||
|
"name": "对数",
|
||||||
|
"sheetHeaderNameList": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldCode": null,
|
||||||
|
"name": "导数",
|
||||||
|
"sheetHeaderNameList": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scatterGraphData": {
|
||||||
|
"hasCoordinate": true,
|
||||||
|
"hasElevation": true,
|
||||||
|
"min": [
|
||||||
|
2.115013167852418,
|
||||||
|
2.2827169278611863,
|
||||||
|
0,
|
||||||
|
-15.6,
|
||||||
|
-1066.592407,
|
||||||
|
-1.1950568103363985,
|
||||||
|
-7538.067239559778,
|
||||||
|
0,
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
73.39361507996574,
|
||||||
|
73.87879831330781,
|
||||||
|
0,
|
||||||
|
-1.2,
|
||||||
|
1232.438965,
|
||||||
|
"NaN",
|
||||||
|
15669.56031213764,
|
||||||
|
0,
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
2.115013167852418,
|
||||||
|
2.2827169278611863,
|
||||||
|
null,
|
||||||
|
-1.2,
|
||||||
|
242.952988,
|
||||||
|
2.385522244678844,
|
||||||
|
4.116022643854044,
|
||||||
|
0,
|
||||||
|
"1453611521843200",
|
||||||
|
24.288,
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
12.5664,
|
||||||
|
1,
|
||||||
|
242.952988
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5.124875148343908,
|
||||||
|
5.300065674086864,
|
||||||
|
null,
|
||||||
|
-2.4,
|
||||||
|
74.753899,
|
||||||
|
1.8736338494382652,
|
||||||
|
13.377228657999497,
|
||||||
|
0,
|
||||||
|
"1453611521843201",
|
||||||
|
23.12,
|
||||||
|
1,
|
||||||
|
7,
|
||||||
|
3,
|
||||||
|
5,
|
||||||
|
25.1327,
|
||||||
|
2,
|
||||||
|
74.753899
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7.994418521491841,
|
||||||
|
8.175900836015376,
|
||||||
|
null,
|
||||||
|
-3.6,
|
||||||
|
100.239388,
|
||||||
|
2.001038406459415,
|
||||||
|
9.976118369757005,
|
||||||
|
0,
|
||||||
|
"1453611521843202",
|
||||||
|
21.987,
|
||||||
|
1,
|
||||||
|
10,
|
||||||
|
4,
|
||||||
|
7,
|
||||||
|
37.6991,
|
||||||
|
3,
|
||||||
|
100.239388
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.00601333013422,
|
||||||
|
8.186794593291953,
|
||||||
|
null,
|
||||||
|
-3.6,
|
||||||
|
70.582214,
|
||||||
|
1.8486952770460858,
|
||||||
|
14.16787521003521,
|
||||||
|
0,
|
||||||
|
"1453611521843203",
|
||||||
|
22.020999999999997,
|
||||||
|
1,
|
||||||
|
10,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
125.663696,
|
||||||
|
4,
|
||||||
|
70.582214
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11.066663821597649,
|
||||||
|
11.272344909478555,
|
||||||
|
null,
|
||||||
|
-4.8,
|
||||||
|
75.202255,
|
||||||
|
1.8762308634556513,
|
||||||
|
13.297473593045847,
|
||||||
|
0,
|
||||||
|
"1453611521843204",
|
||||||
|
20.9685,
|
||||||
|
1,
|
||||||
|
13,
|
||||||
|
5,
|
||||||
|
9,
|
||||||
|
50.265499,
|
||||||
|
5,
|
||||||
|
75.202255
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11.00295159513125,
|
||||||
|
11.184890199081117,
|
||||||
|
null,
|
||||||
|
-4.8,
|
||||||
|
103.542862,
|
||||||
|
2.015120165027033,
|
||||||
|
9.65783619154742,
|
||||||
|
0,
|
||||||
|
"1453611521843205",
|
||||||
|
20.758499999999998,
|
||||||
|
1,
|
||||||
|
13,
|
||||||
|
6,
|
||||||
|
8,
|
||||||
|
109.955704,
|
||||||
|
6,
|
||||||
|
103.542862
|
||||||
|
],
|
||||||
|
[
|
||||||
|
13.92101964767117,
|
||||||
|
14.166540305149894,
|
||||||
|
null,
|
||||||
|
-6,
|
||||||
|
85.264038,
|
||||||
|
1.9307658964508176,
|
||||||
|
11.728273999877885,
|
||||||
|
0,
|
||||||
|
"1453611521859584",
|
||||||
|
19.490499999999997,
|
||||||
|
1,
|
||||||
|
16,
|
||||||
|
6,
|
||||||
|
11,
|
||||||
|
62.831902,
|
||||||
|
7,
|
||||||
|
85.264038
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.000862630669717,
|
||||||
|
14.247085672002468,
|
||||||
|
null,
|
||||||
|
-6,
|
||||||
|
63.056168,
|
||||||
|
1.7997275746100803,
|
||||||
|
15.858876803297024,
|
||||||
|
0,
|
||||||
|
"1453611521859585",
|
||||||
|
19.456,
|
||||||
|
1,
|
||||||
|
16,
|
||||||
|
7,
|
||||||
|
10,
|
||||||
|
113.097298,
|
||||||
|
8,
|
||||||
|
63.056168
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.063602086594681,
|
||||||
|
14.27044051526772,
|
||||||
|
null,
|
||||||
|
-6,
|
||||||
|
44.272751,
|
||||||
|
1.6461365088096525,
|
||||||
|
22.587256888554318,
|
||||||
|
0,
|
||||||
|
"1453611521859586",
|
||||||
|
19.706,
|
||||||
|
1,
|
||||||
|
16,
|
||||||
|
8,
|
||||||
|
9,
|
||||||
|
351.858398,
|
||||||
|
9,
|
||||||
|
44.272751
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.987234838304097,
|
||||||
|
17.233999253766825,
|
||||||
|
null,
|
||||||
|
-7.2,
|
||||||
|
67.065033,
|
||||||
|
1.8264961426225779,
|
||||||
|
14.91089999165437,
|
||||||
|
0,
|
||||||
|
"1453611521859587",
|
||||||
|
18.296499999999998,
|
||||||
|
1,
|
||||||
|
19,
|
||||||
|
7,
|
||||||
|
13,
|
||||||
|
75.398201,
|
||||||
|
10,
|
||||||
|
67.065033
|
||||||
|
],
|
||||||
|
[
|
||||||
|
17.03278556632032,
|
||||||
|
17.279602362635487,
|
||||||
|
null,
|
||||||
|
-7.2,
|
||||||
|
70.816719,
|
||||||
|
1.85013580164825,
|
||||||
|
14.120959204562979,
|
||||||
|
0,
|
||||||
|
"1453611521859588",
|
||||||
|
18.24,
|
||||||
|
1,
|
||||||
|
19,
|
||||||
|
8,
|
||||||
|
12,
|
||||||
|
120.951302,
|
||||||
|
11,
|
||||||
|
70.816719
|
||||||
|
],
|
||||||
|
[
|
||||||
|
16.9816701391346,
|
||||||
|
17.2520906213365,
|
||||||
|
null,
|
||||||
|
-7.2,
|
||||||
|
41.787144,
|
||||||
|
1.6210426897024701,
|
||||||
|
23.930805129922256,
|
||||||
|
0,
|
||||||
|
"1453611521859589",
|
||||||
|
18.438,
|
||||||
|
1,
|
||||||
|
19,
|
||||||
|
9,
|
||||||
|
11,
|
||||||
|
251.327393,
|
||||||
|
12,
|
||||||
|
41.787144
|
||||||
|
],
|
||||||
|
[
|
||||||
|
20.01061875986487,
|
||||||
|
20.259070486101244,
|
||||||
|
null,
|
||||||
|
-8.4,
|
||||||
|
68.505302,
|
||||||
|
1.8357241852184725,
|
||||||
|
14.597410285119246,
|
||||||
|
0,
|
||||||
|
"1453611521859590",
|
||||||
|
17.037,
|
||||||
|
1,
|
||||||
|
22,
|
||||||
|
8,
|
||||||
|
15,
|
||||||
|
87.9646,
|
||||||
|
13,
|
||||||
|
68.505302
|
||||||
|
],
|
||||||
|
[
|
||||||
|
19.924955341057455,
|
||||||
|
20.196078557097973,
|
||||||
|
null,
|
||||||
|
-8.4,
|
||||||
|
76.61187,
|
||||||
|
1.8842960630545889,
|
||||||
|
13.05280761323278,
|
||||||
|
0,
|
||||||
|
"1453611521859591",
|
||||||
|
17.301000000000002,
|
||||||
|
1,
|
||||||
|
22,
|
||||||
|
9,
|
||||||
|
14,
|
||||||
|
130.690308,
|
||||||
|
14,
|
||||||
|
76.61187
|
||||||
|
],
|
||||||
|
[
|
||||||
|
19.94235941950773,
|
||||||
|
20.25312046337076,
|
||||||
|
null,
|
||||||
|
-8.4,
|
||||||
|
58.417942,
|
||||||
|
1.7665462531889349,
|
||||||
|
17.118028567319268,
|
||||||
|
0,
|
||||||
|
"1453611521859592",
|
||||||
|
17.003500000000003,
|
||||||
|
1,
|
||||||
|
22,
|
||||||
|
10,
|
||||||
|
13,
|
||||||
|
226.194595,
|
||||||
|
15,
|
||||||
|
58.417942
|
||||||
|
],
|
||||||
|
[
|
||||||
|
19.95085361886024,
|
||||||
|
20.261252468704264,
|
||||||
|
null,
|
||||||
|
-8.4,
|
||||||
|
151.941925,
|
||||||
|
2.1816776243164018,
|
||||||
|
6.581461963180999,
|
||||||
|
0,
|
||||||
|
"1453611521859593",
|
||||||
|
16.972,
|
||||||
|
1,
|
||||||
|
22,
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
691.150391,
|
||||||
|
16,
|
||||||
|
151.941925
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22.95679663366693,
|
||||||
|
23.231100221926333,
|
||||||
|
null,
|
||||||
|
-9.6,
|
||||||
|
87.04409,
|
||||||
|
1.9397392893244523,
|
||||||
|
11.488430748141546,
|
||||||
|
0,
|
||||||
|
"1453611521859594",
|
||||||
|
16.131,
|
||||||
|
1,
|
||||||
|
25,
|
||||||
|
9,
|
||||||
|
17,
|
||||||
|
100.530998,
|
||||||
|
17,
|
||||||
|
87.04409
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22.822828222784292,
|
||||||
|
23.136758664349642,
|
||||||
|
null,
|
||||||
|
-9.6,
|
||||||
|
89.94828,
|
||||||
|
1.9539928631384818,
|
||||||
|
11.117499967759251,
|
||||||
|
0,
|
||||||
|
"1453611521859595",
|
||||||
|
15.8315,
|
||||||
|
1,
|
||||||
|
25,
|
||||||
|
10,
|
||||||
|
16,
|
||||||
|
141.371704,
|
||||||
|
18,
|
||||||
|
89.94828
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22.928686812404784,
|
||||||
|
23.24072059217002,
|
||||||
|
null,
|
||||||
|
-9.6,
|
||||||
|
24.825411,
|
||||||
|
1.3948964472401078,
|
||||||
|
40.28130692378064,
|
||||||
|
0,
|
||||||
|
"1453611521867776",
|
||||||
|
15.769,
|
||||||
|
1,
|
||||||
|
25,
|
||||||
|
11,
|
||||||
|
15,
|
||||||
|
219.911407,
|
||||||
|
19,
|
||||||
|
24.825411
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22.8941388207831,
|
||||||
|
23.205240404465737,
|
||||||
|
null,
|
||||||
|
-9.6,
|
||||||
|
39.082321,
|
||||||
|
1.5919803474612875,
|
||||||
|
25.587016697396244,
|
||||||
|
0,
|
||||||
|
"1453611521867777",
|
||||||
|
15.835000000000003,
|
||||||
|
1,
|
||||||
|
25,
|
||||||
|
12,
|
||||||
|
14,
|
||||||
|
449.247803,
|
||||||
|
20,
|
||||||
|
39.082321
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"__rowsTotal": 576
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue