Compare commits

..

No commits in common. "main" and "feat/vtk-3d-view" have entirely different histories.

178 changed files with 1447 additions and 14335 deletions

3
.gitignore vendored
View File

@ -56,6 +56,3 @@ docs/剖面网格数据的色阶数据2等文件.tar
/installer/staging/
/installer/dist/
/installer/redist/
# ---- Radar sample data: keep .head/.cor (small text), ignore big .data binaries ----
samples/**/*.data

View File

@ -13,8 +13,6 @@ set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
if(MSVC)
# MSVC Release=/MDDebug=/MDdABI ENV_SETUP §9.2
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
add_compile_options(/utf-8 /MP /W4 /permissive-)
# PDB使 Release 使 minidump /
# /Zi /DEBUG PDB/OPT:REF,ICF /DEBUG
@ -33,61 +31,16 @@ endif()
# - Qt GDAL/PROJ/OpenSSL/Eigen/... vcpkg
# =====================================================================
# =====================================================================
#
# Qt 6.11.1 msvc2022_64 + VTK 9.6.x + VS2026-preview
# WARN FATAL VS2026-preview Qt v143 ABI
# docs/ENV_SETUP_Windows.md §4/§5/§9
# =====================================================================
if(WIN32)
# QT_ROOT MSVC Qt kit CMAKE_PREFIX_PATH
if(NOT DEFINED ENV{QT_ROOT} OR "$ENV{QT_ROOT}" STREQUAL ""
OR NOT EXISTS "$ENV{QT_ROOT}/lib/cmake/Qt6")
message(FATAL_ERROR
"QT_ROOT setx QT_ROOT \"<Qt>\\6.11.1\\msvc2022_64\" docs/ENV_SETUP_Windows.md §4")
endif()
endif()
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network Sql Concurrent)
# Qt major.minor 6.11 VTK/ADS ABI/
if(NOT Qt6_VERSION VERSION_LESS 6.11 AND Qt6_VERSION VERSION_LESS 6.12)
# 6.11.x
else()
message(FATAL_ERROR
"Qt ${Qt6_VERSION} 6.11.x VTK/ADS 6.11.1 msvc2022_64 QT_ROOT docs/ENV_SETUP_Windows.md §4")
endif()
# VTK friendlyfind_package REQUIRED
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/external/vtk-install/lib/cmake/vtk-9.6")
message(FATAL_ERROR
"VTK docs/ENV_SETUP_Windows.md §5.1 Release VTK 9.6.x external/vtk-install")
endif()
# VTK 9 COMPONENTS VTK_LIBRARIES VTK
# VTK_DIRexternal/vtk-installVolume/Filters
find_package(VTK REQUIRED COMPONENTS
GUISupportQt
RenderingOpenGL2
RenderingFreeType # gizmo 轴标签(vtkBillboardTextActor3D) FreeType
InteractionStyle
FiltersSources
)
# VTK major.minor 9.6VTK_DIR vtk-9.6/
if(VTK_VERSION VERSION_LESS 9.6 OR NOT VTK_VERSION VERSION_LESS 9.7)
message(FATAL_ERROR
"VTK ${VTK_VERSION} 9.6.x docs/ENV_SETUP_Windows.md §5.1 Qt/ external/vtk-install")
endif()
# WARN FATAL Qt msvc2022(v143)VTK app
# std::map/std::string ABI §10 VS2026-preview v143
if(MSVC AND NOT MSVC_TOOLSET_VERSION STREQUAL "143")
message(WARNING
"MSVC v${MSVC_TOOLSET_VERSION} v143 Qt msvc2022(v143) external/vtk-install VTK app ABI docs/ENV_SETUP_Windows.md §9.2")
endif()
# Qt vcpkg
# find_package(GDAL CONFIG REQUIRED)
# find_package(PROJ CONFIG REQUIRED)

View File

@ -12,7 +12,7 @@
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"VCPKG_TARGET_TRIPLET": "x64-windows",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"CMAKE_PREFIX_PATH": "$env{QT_ROOT}",
"CMAKE_PREFIX_PATH": "D:/Qt/6.11.1/msvc2022_64",
"VTK_DIR": "${sourceDir}/external/vtk-install/lib/cmake/vtk-9.6"
}
},

View File

@ -42,16 +42,6 @@ if not exist "%CMAKE%" ( echo [build] cmake not found: "%CMAKE%" & exit /b 1 )
REM --- activate MSVC environment (cl / link / include / lib) ---
call "%VCVARS%" >nul
REM --- environment guardrails (fail loud & early, before cmake configure) ---
if not defined QT_ROOT (
echo [build] QT_ROOT is not set. Run: setx QT_ROOT "D:\Qt\6.11.1\msvc2022_64" ^(see docs/ENV_SETUP_Windows.md^), then reopen the terminal.
exit /b 1
)
if not defined VCPKG_ROOT (
echo [build] VCPKG_ROOT is not set. Run: setx VCPKG_ROOT "C:\dev\vcpkg" ^(see docs/ENV_SETUP_Windows.md^), then reopen the terminal.
exit /b 1
)
set "CMD=%~1"
if "%CMD%"=="" set "CMD=app"

View File

@ -1,87 +1,106 @@
# Geopro 3.0 桌面客户端 — Windows 开发环境从零搭建指引
适用Windows 10 22H2 / Windows 1164 位,**MSVC 2022v143工具链**
目标:从一台干净的机器,搭到 `build.bat app` 能编出可运行、且**不因 ABI 不匹配崩溃**的 Qt6 + VTK9 桌面程序。
适用Windows 10 22H2 / Windows 1164 位,MSVC 2022 工具链
目标:从一台干净的机器,搭到 `cmake --build` 出可运行的 Qt6 + VTK9 桌面程序。
> 最后核对2026-07-01已对齐 `CMakePresets.json` / `vcpkg.json` / 根 `CMakeLists.txt` / `build.bat` 的真实配置。
> ⚠️ 若你手里有更早版本的本文档:**§6.1/§7/§9 曾残留「全 vcpkgQt/VTK 也走 vcpkg」的旧方案与现状矛盾——以本版为准。** 现状是「方案②-修订」:**官方预编译 Qt + 源码编 VTK + vcpkg 只管非 Qt 依赖**。
> 配套设计文档:`docs/superpowers/specs/2026-06-07-geopro-desktop-m1-design.md`
---
## 0. 总览(方案②-修订)
## 0. 总览
**单一 Qt = 官方 MSVC 预编译 Qt**`D:\Qt\6.11.1\msvc2022_64`)。**凡依赖 Qt 的组件都不走 vcpkg**vcpkg 的 Qt 端口会再编一份 qtbase = 双份 Qt 冲突):
> ⚠️ **构建方案已改定为「方案②-修订」**(经双专家评审 + 实机勘验)。本文档大部分步骤按此更新;**权威步骤以设计 §11 + `docs/superpowers/plans/2026-06-07-m1-phase0-spikes.md` 为准**。
>
> **方案②-修订要点**:单一 Qt = **官方 MSVC 预编译 Qt**`D:\Qt\6.11.1\msvc2022_64`)。**凡依赖 Qt 的组件(VTK/ADS/QtKeychain)都不走 vcpkg**vcpkg 的 Qt 依赖端口会再编一份 qtbase = 双份冲突VTK 用官方 Qt 源码编到 install 前缀ADS/QtKeychain 走 FetchContent仅非 Qt 依赖(GDAL/PROJ/OpenSSL/Eigen/...)走 vcpkg。
> **关键事实**:① 用户原装 `D:\Qt\6.11.1`**MinGW 版**(MSVC 不可链),须在 Qt 维护工具里补装 **MSVC 2022 64-bit** kit② VTK 无 MSVC 预编译,三方案都必须源码编;③ 本机 VS18 = MSVC 14.51,链官方 Qt(v143)属"新链旧"ABI 安全。
| 组件 | 来源 | 备注 |
|---|---|---|
| Qt 6.11.1Core/Gui/Widgets/Network/Sql/Concurrent/OpenGL + tools | **官方 MSVC 2022 64-bit kit**§4 | 路径由预设 `CMAKE_PREFIX_PATH` 指定 |
| VTK 9.6.xGUISupportQt/RenderingOpenGL2/RenderingFreeType/InteractionStyle/FiltersSources… | **源码 Release 编 → `external/vtk-install`**§5 | 必须对齐同一份 Qt + 同一工具集 + Release/`/MD` |
| ADSQt-Advanced-Docking-System 4.3.1 | **FetchContent 自动**(配置时拉,对接官方 Qt | 无需手动 |
| QtKeychain v0.14.0 | **FetchContent 自动**`BUILD_WITH_QT6` | 无需手动 |
| Qwt 6.2(二维科学图表) | **手工克隆到 `external/qwt-src`**§5.3gitignored | 缺失则详情页图表功能不编入 |
| vendored 3DGPRViewergeopro_gpr3dv | 仓库内 `external/gpr3dviewer`(随仓库) | 无需手动 |
| 非 Qt 依赖eigen3 / gdal / gtest / nlohmann-json / openssl / proj | **vcpkg manifest**`vcpkg.json`,有 baseline 锁版本) | 配置时自动拉 |
**ABI 铁律(否则运行时崩溃,见 §10**Qt(预编译 msvc2022) / VTK(你自己编) / app / 所有 FetchContent 组件,**必须同一工具集(v143)、同一运行时(`/MD`)、同一配置(全 Release)**。任何一处混 Debug/Release 或换工具集,`std::map`/`std::string` 跨界就崩。
四块:① 编译器VS18 / MSVC 14.51)② Git ③ vcpkg仅非 Qt 依赖)④ 官方 MSVC Qt + 源码 VTK。
---
## 1. Visual StudioMSVC v143 + CMake + Ninja
## 1. Visual Studio 2022MSVC + CMake + Ninja
1. 安装 **Visual Studio 2022 Community**(或更高)。`build.bat` 也兼容 VS2026 preview但**其工具集必须与预编译 Qt 的 msvc2022(v143) ABI 兼容**——若用 VS2026 的更新工具集,稳妥做法是**用同一工具集重编 VTK**§5别让 Qt(v143 预编译) 与 VTK/app(更新工具集) 混。
2. 勾选工作负载 **「使用 C++ 的桌面开发」**,确保含:
1. 安装 **Visual Studio 2022 Community**(或更高)。
2. 勾选工作负载 **「使用 C++ 的桌面开发」(Desktop development with C++)**,确保包含:
- MSVC v143 - VS 2022 C++ x64/x86 生成工具
- Windows 11 SDK或 Windows 10 SDK
- C++ CMake tools for Windows自带 CMake ≥3.21 + Ninja
- C++ AddressSanitizerDebug Sanitizer 用)
3. 验证开「x64 Native Tools Command Prompt for VS」`cl` / `cmake --version`≥3.21/ `ninja --version`
- C++ CMake tools for Windows自带 CMake + Ninja
- C++ AddressSanitizer用于 Debug Sanitizer规约 §10.2
3. 验证开「x64 Native Tools Command Prompt for VS 2022」运行
```
cl
cmake --version # 应 ≥ 3.21
ninja --version
```
> `build.bat` 会用 `vswhere` 自动定位 VS 并激活 MSVC 环境,所以**日常构建直接跑 `build.bat` 即可**,不必手动开 x64 命令行。但 `cmake/ninja/cl` **不在 PATH**,手动跑 cmake 前需先激活 vcvars64。
> 之后所有 cmake/vcpkg 命令都在 **x64 Native Tools 命令行** 里跑(已设好 MSVC 环境变量)
---
## 2. Git
1. 安装 [Git for Windows](https://git-scm.com/download/win)`git --version` 验证。
2. 本项目**已是 git 仓库**(无需 `git init`);正常 `git clone` 即可。
1. 安装 [Git for Windows](https://git-scm.com/download/win)。
2. 验证:`git --version`。
3. 本项目当前**尚未初始化 git 仓库**——首次提交前需 `git init`(见 §7
---
## 3. vcpkg仅非 Qt 依赖)
## 3. vcpkg依赖管理
```powershell
git clone https://github.com/microsoft/vcpkg C:\dev\vcpkg # 路径不含空格/中文
# 选一个不含空格/中文的路径,例如 C:\dev
git clone https://github.com/microsoft/vcpkg C:\dev\vcpkg
C:\dev\vcpkg\bootstrap-vcpkg.bat
setx VCPKG_ROOT "C:\dev\vcpkg" # 永久;新开终端生效
```
- **manifest 模式**:根 `vcpkg.json` 声明依赖 + `builtin-baseline` 锁版本CMake 配置时自动拉取,**无需手动 `vcpkg install`**。
- 实际依赖(勿加 Qt/VTK 进来):`eigen3, gdal, gtest, nlohmann-json, openssl, proj`。
- **不要随意 `vcpkg x-update-baseline`**——baseline 已锁,改动会漂移依赖版本、可能引入 ABI 不一致。
- 首次会编 GDAL/PROJ 等,较久;可选配 `VCPKG_BINARY_SOURCES` 二进制缓存加速。
设环境变量(系统环境变量或当前会话):
```powershell
$env:VCPKG_ROOT = "C:\dev\vcpkg"
setx VCPKG_ROOT "C:\dev\vcpkg" # 永久(新开终端生效)
```
> 本项目用 **vcpkg manifest 模式**`vcpkg.json`),不需要手动 `vcpkg install`CMake 配置时按清单自动拉取。
---
## 4. Qt官方 MSVC 2022 64-bit kit
## 4. Qt官方 MSVC 预编译 kit
**必须是 MSVC kit**——若你原装的是 `mingw_64`MSVC 下不可链:
**用官方安装器,但必须是 MSVC kit**(你原装的 `mingw_64` 在 MSVC 下不可用):
1. 用官方在线安装器(或已装则开 `D:\Qt\MaintenanceTool.exe`)→ Add or remove components → 登录 Qt 账号。
2. 展开 Qt → **Qt 6.11.1**,勾选 **MSVC 2022 64-bit**,安装。
3. 完成后应存在 `D:\Qt\6.11.1\msvc2022_64\lib\cmake\Qt6`
4. **设 `QT_ROOT` 环境变量指向该 kit**(与 `VCPKG_ROOT` 同一套路,预设 `CMAKE_PREFIX_PATH=$env{QT_ROOT}` 读它§6
```powershell
setx QT_ROOT "<你的路径>\6.11.1\msvc2022_64" # 永久;新开终端生效
```
**全链路只此一份 Qt。** 版本仍须是 **6.11.1**(换版本可能与源码编的 VTK/ADS 不匹配)。
1. `.\qt-online-installer-windows-x64-4.11.0.exe --mirror https://ftp.jaist.ac.jp/pub/qtproject`
2. 打开 `D:\Qt\MaintenanceTool.exe` → Add or remove components → 登录 Qt 账号。
3. 展开 Qt → Qt 6.11.1,勾选 **MSVC 2022 64-bit**,安装。
4. 完成后存在 `D:\Qt\6.11.1\msvc2022_64\lib\cmake\Qt6`(供 `find_package(Qt6)`)。
> 若你的 Qt 装在别处/别的版本:**改 `QT_ROOT` 指向你的 `msvc2022_64` 即可**,无需动预设;但版本仍须是 6.11.1。
> 未设 `QT_ROOT` 时:`build.bat` 会立即报 `[build] QT_ROOT is not set...` 并退出;直接跑 cmake 则配置期 `FATAL_ERROR: QT_ROOT 未设置或无效`
CMake 经 `CMAKE_PREFIX_PATH=D:/Qt/6.11.1/msvc2022_64` 找到它(见 §6 预设)。**全链路只此一份 Qt**。
---
## 5. 源码依赖(手工准备
## 5. 依赖来源(方案②-修订
### 5.1 VTK 9.6.x 源码编到 `external/vtk-install`(用官方 QtRelease
| 类别 | 组件 | 来源 |
|---|---|---|
| Qt | qtbase/widgets/network/sql/concurrent/opengl + tools | 官方 MSVC kit(§4) |
| VTK | vtk 9.3[qt,opengl]+gdal/proj 可选) | **源码编 → install 前缀**(§5.2) |
| Qt 依赖小件 | ADS、QtKeychain | **FetchContent 对接官方 Qt**(§6.2) |
| 非 Qt 依赖 | gdal/proj/openssl/eigen3/spdlog/fmt/nlohmann-json/gtest | **vcpkg**(下方 vcpkg.json) |
### 5.1 `vcpkg.json`(仅非 Qt 依赖)
```json
{
"name": "geopro-desktop",
"version": "0.1.0",
"dependencies": ["gdal","proj","eigen3","spdlog","fmt","nlohmann-json","openssl","gtest"]
}
```
- **凡依赖 Qt 的(vtk[qt]/qtkeychain/qt-advanced-docking-system)绝不放进 vcpkg**——否则 vcpkg 会再编一份 qtbase = 双份 Qt 冲突(已核 `ports/vtk/vcpkg.json`)。
- `vcpkg x-update-baseline --add-initial-baseline` 锁版本(规约 §5.4)。
- 先配 `VCPKG_BINARY_SOURCES` 二进制缓存(实测当前为空),省 GDAL/PROJ 重编。
### 5.2 VTK 源码编到 install 前缀(用官方 Qt
实机用 **VTK 9.6.2**(最新稳定,对 Qt 6.11 兼容最好),源码/构建全放 **D:**(C: 仅剩 ~1GB)。脚本见 `external/build_vtk.bat`(已 .gitignore),要点:
```bat
call "<VS>\VC\Auxiliary\Build\vcvars64.bat"
@ -95,32 +114,18 @@ cmake -S D:\dev\vtk-src -B D:\dev\vtk-build -G Ninja ^
-D CMAKE_INSTALL_PREFIX=D:/Git/lanbingtech/geopro/external/vtk-install
cmake --build D:\dev\vtk-build --target install
```
- 完成后 `external/vtk-install/lib/cmake/vtk-9.6``find_package(VTK)``CMakePresets.json` 里 `VTK_DIR` 已指向它)。
- **必须 Release + 与本项目相同的工具集(v143)编**;因此**本项目也用 `msvc-release` 构建**,避免 `/MD` vs `/MDd` 或 Debug/Release 混链(→ §10 崩溃)。需要调试 VTK 时另出一份 Debug VTK且此时 app 也须整套 Debug。
- 根 `CMakeLists.txt``find_package(VTK REQUIRED COMPONENTS ...)` **必须列组件**(否则 `VTK_LIBRARIES` 为空、链不到);当前组件:`GUISupportQt / RenderingOpenGL2 / RenderingFreeType / InteractionStyle / FiltersSources`(随渲染层增补)。
### 5.2 ADS / QtKeychainFetchContent自动
无需手动——根 `CMakeLists.txt``FetchContent`**ADS 4.3.1****QtKeychain v0.14.0**,对接同一份官方 Qt`BUILD_WITH_QT6=ON`)。首次配置会 clone需能访问 GitHub。
### 5.3 Qwt 6.2(手工克隆到 `external/qwt-src`
二维科学图表(数据集详情散点/等值线)依赖 Qwt。`external/qwt-src` 已 gitignore须自备
```powershell
# 克隆/解压 Qwt 6.2 源码到 external/qwt-src使 external/qwt-src/src 存在
git clone --branch qwt-6.2 https://git.code.sf.net/p/qwt/git external/qwt-src
```
- CMake 检测到 `external/qwt-src/src` 才 include `cmake/qwt.cmake` 编 Qwt**缺失则相关图表功能不编入**(不报错,但详情图表缺失)。
完成后 `external/vtk-install/lib/cmake/vtk-9.6``find_package(VTK)`(已在 `CMakePresets.json``VTK_DIR`)。
> 注:VTK 用 **Release** 编。因此**冒烟程序也用 `msvc-release` 预设构建**,以匹配 Release VTK + Release Qt(避免 `/MD` vs `/MDd` 混链)。需要 Debug 调试 VTK 时再出一份 Debug VTK。
---
## 6. CMake 接线(现状,勿照旧文档)
## 6. CMake 接线
### 6.1 `CMakePresets.json`真实内容
### 6.1 `CMakePresets.json`(项目根)
```json
{
"version": 3,
"configurePresets": [
{
"name": "msvc-debug",
@ -129,94 +134,112 @@ git clone --branch qwt-6.2 https://git.code.sf.net/p/qwt/git external/qwt-src
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"VCPKG_TARGET_TRIPLET": "x64-windows",
"CMAKE_PREFIX_PATH": "$env{QT_ROOT}",
"VTK_DIR": "${sourceDir}/external/vtk-install/lib/cmake/vtk-9.6"
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{ "name": "msvc-release", "inherits": "msvc-debug",
{
"name": "msvc-release",
"inherits": "msvc-debug",
"binaryDir": "${sourceDir}/build/release",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } }
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }
}
]
}
```
- **预设确实设 `CMAKE_PREFIX_PATH`(官方 Qt读 `$env{QT_ROOT}`+ `VTK_DIR`(源码编的 VTK仓库相对、无需环境变量**——这是「方案②-修订」的核心,别按旧文档去掉它。`QT_ROOT` 未 setx 时配置期会 FATAL§4
- vcpkg 只经 `CMAKE_TOOLCHAIN_FILE` 管非 Qt 依赖triplet `x64-windows`
> 全 vcpkg 方案下**不设 `CMAKE_PREFIX_PATH` 指向官方 Qt**——vcpkg 工具链接管 Qt 查找(避免双 Qt
### 6.2 `CMakeLists.txt` 要点
### 6.2 顶层 `CMakeLists.txt`(骨架)
- `find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network Sql Concurrent)`
- `find_package(VTK REQUIRED COMPONENTS GUISupportQt RenderingOpenGL2 RenderingFreeType InteractionStyle FiltersSources)`**必须列组件**)。
- MSVC flags`/utf-8 /MP /W4 /permissive-`;非 Debug 配置产 PDB`/Zi` + `/DEBUG` + `/OPT:REF,ICF`)。
- ADS/QtKeychain 经 FetchContentQwt 经 `cmake/qwt.cmake`(存在才编)。
- 视图层用 **`QVTKOpenGLStereoWidget`**QOpenGLWidget 系ADS reparent 友好)。
```cmake
cmake_minimum_required(VERSION 3.21)
project(geopro_desktop LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
---
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network Sql Concurrent)
find_package(VTK REQUIRED) # 含 GUISupportQtvtk[qt])→ QVTKOpenGLStereoWidget
find_package(GDAL CONFIG REQUIRED)
find_package(PROJ CONFIG REQUIRED)
find_package(Eigen3 CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(OpenSSL REQUIRED)
# find_package(Qt6Keychain CONFIG REQUIRED) # qtkeychain
# ADSvcpkg 端口验证通过后 find_package否则用下方 FetchContent
## 7. 首次配置、编译、运行、部署
**日常直接用 `build.bat`(推荐)**
add_subdirectory(src)
enable_testing()
add_subdirectory(tests)
```
build.bat app # 配置(首次)+ 增量编 geopro_desktopRelease
build.bat test # 编 + ctest 跑单测
build.bat run # 编 + 启动
build.bat rebuild # 强制 --clean-first 全量重编(增量疑似漏编时用)
build.bat configure # CMakeLists 改动后强制重跑 configure
> VTK 链接用 `vtk_module_autoinit`;视图层用 **`QVTKOpenGLStereoWidget`**QOpenGLWidget 系ADS reparent 友好,见设计 §K-9不用 native 版。
> **ADS 备选引入**(若 vcpkg 端口不可用spike 阶段确定):
> ```cmake
> include(FetchContent)
> FetchContent_Declare(ads GIT_REPOSITORY https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System.git GIT_TAG <tag>)
> FetchContent_MakeAvailable(ads)
> ```
---
## 7. 首次配置、编译、运行
```powershell
# 在 x64 Native Tools 命令行、项目根目录
git init # 首次:初始化仓库
vcpkg x-update-baseline --add-initial-baseline
cmake --preset msvc-debug # 首次会编译 VTK 等,耗时较长
cmake --build build/debug
# 运行VTK/Qt 的 dll 需在 PATH 或同目录)
.\build\debug\src\app\geopro_desktop.exe
```
- `build.bat` 固定 `--preset msvc-release`、`build/release`,自动激活 MSVC 环境。exe`build/release/src/app/geopro_desktop.exe`。
- 首次会拉 vcpkg 依赖 + FetchContent(ADS/QtKeychain)较久VTK/Qt/Qwt 须已按 §4/§5 就位。
**运行期 DLL 部署(官方 Qt非 vcpkg**
- Qt6*.dll + plugins用**官方 Qt 的 `windeployqt`**`D:\Qt\6.11.1\msvc2022_64\bin\windeployqt.exe`)对齐**同一份官方 Qt** 到 exe 目录。
- VTK*.dll`external/vtk-install/bin` 拷到 exe 目录(或加进 PATH
- **只保证 exe 目录一份 Qt6*.dll无双 Qt**;勿混入别处/vcpkg 的 Qt。
**运行期 DLL 部署(单一 vcpkg 链路)**
- 全部 dll含 Qt来自 vcpkg`build/vcpkg_installed/x64-windows/(debug/)bin`。
- 用 CMake `TARGET_RUNTIME_DLLS` + `add_custom_command(POST_BUILD)` 自动拷贝到 exe 目录(实现阶段加)。
- **不混用官方 Qt 的 `windeployqt`**——本方案 Qt 来自 vcpkg混用会拷错版本造成双 Qt 冲突。
- Qt pluginsplatforms/imageformats/sqldrivers 等)由 vcpkg 部署脚本处理;如缺再用 vcpkg 安装树里的 `windeployqt` 对齐同一份 Qt。
---
## 8. AI/IDE 上下文clangd
## 8. AI 编码上下文基础设施(规约 §10.1,强烈建议先建
- `.clangd``CompileFlags.CompilationDatabase: build/release`(对齐 `build.bat` 的主构建目录;若你主要用 Debug 则指 `build/debug`)。`CMAKE_EXPORT_COMPILE_COMMANDS=ON` 会在该目录产 `compile_commands.json`
- VS Code 用 **clangd** 扩展(禁用微软 C++ IntelliSense 避免冲突)。
项目根放:
- `.clang-format`:统一风格(基于 LLVM/Google + 团队微调)。
- `.clangd`
```yaml
CompileFlags:
CompilationDatabase: build/debug
```
使 clangd 读取 `compile_commands.json`,给 AI/IDE 精确类型上下文。
- VS Code 装 **clangd** 扩展(禁用微软 C++ IntelliSense 避免冲突),或 CLion 直接用 CMake。
---
## 9. 构建环境兼容性检查清单(交付/接手前逐项核对)
## 9. 验证清单
> 目的:把「配置错→运行时随机崩溃」变成「一眼可查」。**任何一项不满足都可能导致 §10 的 `std::map`/`std::string` ABI 崩溃。**
### 9.1 前置就位
- [ ] `cl` / `cmake`(≥3.21) / `ninja` / `git` 可用;`VCPKG_ROOT` 已设。
- [ ] `QT_ROOT` 已 setx 指向 **6.11.1 `msvc2022_64`** kitMSVC非 MinGW`$QT_ROOT/lib/cmake/Qt6` 在(预设 `CMAKE_PREFIX_PATH=$env{QT_ROOT}` 读它;未设则配置期 FATAL
- [ ] `external/vtk-install/lib/cmake/vtk-9.6` 存在VTK 已源码编 install
- [ ] `external/qwt-src/src` 存在(如需详情页图表)。
- [ ] `vcpkg.json``builtin-baseline` 未被改动。
### 9.2 ABI 一致性(最关键,防崩溃)
- [ ] **单一配置**Qt(预编译) / VTK(你编) / app / ADS / QtKeychain / Qwt **全 Release**(或全 Debug**绝不混**。日常一律 `build.bat`msvc-release
- [ ] **同一工具集**VTK/app 用的 MSVC 工具集与预编译 Qt 的 **v143(msvc2022)** ABI 兼容;若用 VS2026-preview 工具集,**用它重编 VTK**,别让 Qt(v143) 与 VTK/app(更新集) 混。
- [ ] **同一运行时**:全部 `/MD`Release 动态 CRT`_ITERATOR_DEBUG_LEVEL=0`。**绝不把 Release 的 Qt/VTK 链进 Debug 的 app**Debug 是 `/MDd` + IDL=2STL 布局不同)。
- [ ] **VTK 来源正确**:是**你按 §5.1 源码 Release 编、对齐本 Qt** 的那份;**不是** vcpkg 的 VTK、不是别处/别配置的 VTK。
- [ ] **单一 Qt**exe 目录/PATH 只有一份 Qt6*.dll无双 Qt。
### 9.3 干净构建验证
- [ ] 删 `build/``build.bat app` 从零配置**无错**(尤其无「找不到 Qt6/VTK」「add_subdirectory 目录不存在」)。
- [ ] `build.bat test` → ctest 全绿。
- [ ] 启动 app、点对象树、渲染视图**不崩**§10 的崩溃即出现在这里)。
- [ ] `cl` / `cmake` / `ninja` / `git` 命令可用
- [ ] `VCPKG_ROOT` 已设
- [ ] `vcpkg.json` 含 qtbase + vtk[qt](共用一份 Qtpreset **未**指向官方 Qt
- [ ] `cmake --preset msvc-debug` 成功Qt+VTK 已拉取编译,首次较久)
- [ ] `cmake --build` 出 exe
- [ ] exe 能起一个空 Qt 窗 + 一个 `QVTKOpenGLStereoWidget` 渲染窗(冒烟测试)
- [ ] 部署后 exe 目录只有一份 Qt6*.dll无双 Qt
- [ ] `compile_commands.json` 生成clangd 正常索引
---
## 10. 已知崩溃签名 → 根因速查
## 10. 与设计文档对应的 spike 门槛
| 现象 | 根因 | 处理 |
|---|---|---|
| **点对象树/渲染时,崩在 `std::_Tree::_Find_lower_bound``std::map`/`set` 查找),`_Myhead` 是 `0xFFFF...` 之类垃圾值,读取访问冲突** | **STL ABI 不匹配**Debug/Release 混链、`/MD` vs `/MDd`、`_ITERATOR_DEBUG_LEVEL` 不一致、或工具集不匹配(最常见:**别处/别配置的 VTK/Qt**,或 **Debug app 链 Release 依赖** | 按 §9.2 逐项核对;**删 `build/` 全 Release 干净重建**;确认 VTK 是 §5.1 那份 |
| 配置期 `FATAL_ERROR: QT_ROOT 未设置或无效` / `build.bat``[build] QT_ROOT is not set` | 未 setx `QT_ROOT` 或指向的 kit 不含 `lib/cmake/Qt6` | `setx QT_ROOT "<你的Qt>\6.11.1\msvc2022_64"` 后**重开终端**§4 |
| 配置期 `FATAL_ERROR: Qt 版本不匹配` / `VTK 版本不匹配` / `VTK 未就位` | 装了非 6.11.x Qt、非 9.6.x VTK`external/vtk-install` 缺失 | 按 §4/§5 装对版本/就位 VTK |
| 链接期报「找不到 Qt6::/VTK::」 | 预设 `CMAKE_PREFIX_PATH`(`$env{QT_ROOT}`)/`VTK_DIR` 指向不存在 | 按 §4/§5 就位或修正 `QT_ROOT` |
| 配置报「add_subdirectory ... 不是已存在目录」 | 引用了未随仓库的目录(如误提交本地临时工具目录) | 从 `CMakeLists.txt` 去掉该引用,或补齐该目录 |
| 详情页图表缺失但不报错 | `external/qwt-src` 未就位§5.3 | 克隆 Qwt 后重配置 |
| 双 Qt / 起不来找不到 platform 插件 | 混入了 vcpkg/别处的 Qt dll | 只用官方 `windeployqt` 对齐单一 Qt |
本指引服务于设计 §15 的第一周 spike① 全 vcpkg 构建/部署打通(本文);② ADS + `QVTKOpenGLStereoWidget` 浮动/重停靠不黑屏;③ 真实样本跑通 banded contour。三者通过再进入完整实现计划。
---
*遇到具体报错按规约由工程师复核AI 协助按 build-error-resolver 流程逐条解决。*
*遇到具体报错VTK/Qt/GDAL 链接、ADS 端口、双 Qt按规约 §10.4 由工程师复核AI 协助按 build-error-resolver 流程逐条解决。*

View File

@ -55,22 +55,3 @@
落在光标下那张切片 → 选对(`onPick` 仅命中时触发,未命中不误选)。重叠切片仍按最前优先(合理)。
逻辑闭合但**未 live 点击验证**(工具无法交互点击 3D 切片);若仍有偏差需 live 复核(重叠循环切换等)。
- **更新**2026-06-25 issue2+③+反向²(`69e8790`)+④(`63cda56`) 全部实现。
---
## OPT-003 · 二维分析 C 期dd_raster 栅格地理配准渲染(阻塞·待后端端点)
- **状态**:🔴 Open**阻塞**:后端无栅格数据端点)
- **记录日期**2026-06-26
- **背景/现状**:二维分析改造分期 A→B→Cspec `docs/superpowers/specs/2026-06-26-2d-analysis-topdown-elevation.md`)。
A一场景两相机、B足迹高程 Z 拖动已实现commit `6a10975`、`bdebe54`)。**C 期=dd_raster 栅格**
DD0623 新增 ddCode展示模式 2D形态=栅格/遥感影像)尚未做。
- **期望**`dd_raster` 纳入 2D 维度过滤(`dimOf`/`dimensionOf` 加 `dd_raster`→Dim2Dcol2D 勾选渲染按
ddCode 分派(轨迹走 `loadMapLine`,栅格走**栅格加载**,不可串);栅格取**像素 + 四至/仿射 + 投影 CRS**
作地理配准纹理平面贴到地形上(带高程,可被 B 期 Z 拖动),类似底图瓦片按经纬定位。
- **阻塞点(为何不做)**:实测后端 API 文档 `docs/apis/business_OpenAPI.json` **无任何 dd_raster / 栅格影像
端点**(仅有 grid 行/反演 grid、GPR 通道图,均非带四至+投影的栅格)。无端点 → 无像素/地理范围可加载 →
C 期无数据可渲染。**须后端提供返回「像素 + 四至/仿射 + 投影」的端点后方可落地。**
- **难点**:栅格加载路径(新)、按 ddCode 的渲染分派、地理配准纹理平面(参考 `TileBasemap``buildFlat`/
`buildWarped` 按经纬贴地形 + DEM 位移)。
- **关联**spec §6/§9/§10/§11handoff `docs/superpowers/HANDOFF-2026-06-26-vtk-anomaly-2d-analysis.md` §0。
- **更新**:—

View File

@ -1,100 +0,0 @@
# HANDOFF — VTK 3D 视图:创建异常打磨 + 切片/异常交互 + 二维分析改造2026-06-26
> 分支 `feat/vtk-3d-view`。桌面端 Qt6 + VTK 9.6。本会话围绕「创建异常」全链路打磨、切片/异常交互修复、全局中文化,最后转入「二维分析改造」的需求厘清 + 写 spec。**下一步=按 spec 实现二维分析 A 期。**
---
## 0. 立刻要做的事(下个会话从这里开始)
**二维分析改造 A 期已实现**(未提交,下个会话需用户实跑反馈手感/角度。spec`docs/superpowers/specs/2026-06-26-2d-analysis-topdown-elevation.md`commit `227ee8f`)。分期 A→B→C
- **A已实现 ✅build+439测试全绿未提交**一场景两相机。切「二维分析」tab → 近俯视(下压12°≈78°俯角)+禁旋转(左键改平移、仅平移/缩放);按维度翻 actor `SetVisibility`(轨迹↔体/帘面/异常,**不清空**);切片 `SetEnabled` 显隐(不销毁);地形+底图常驻;切回三维还原相机快照。**待用户实跑**:①近俯视角度是否合适②切换是否瞬时③左键平移手感④切回三维视角还原是否自然。
- 改动文件:`CameraPreset.{hpp,cpp}`(applyNearTop2D)、`PickInteractorStyle.{hpp,cpp}`(setLock2D)、`SliceTool.{hpp,cpp}`(setVisible)、`InteractionManager.{hpp,cpp}`(setMode2D)、`VtkSceneView.{hpp,cpp}`(setAnalysisMode2D+mapLineDs_+相机快照)、`ColumnDrawer.{hpp,cpp}`(analysisModeChanged 信号)、`main.cpp`(接信号)。
- 已知小风险2D 取景 `computeDataBounds` 含隐藏的 3D 体包围盒(地形主导,影响小);切片 `SetEnabled` 显隐属 GUI 不可自测项。
- **B已实现 ✅build+441测试全绿未提交待实跑**:二维里选中足迹(单/Ctrl 多选)→ 竖向拖动只改**高程 Z**、锁 XY、顶部实时高程读数浮层Z 偏移按 dsId 持久(切走再回/全量重建保留)。手势:单击足迹=选中、Ctrl+单击=多选切换、点空白=取消+平移、(多)选后竖向拖动=整体改 Z。
- 实现:`VtkSceneView` 加 `pickMapLineAt/nudgeSelectedMapLinesZ/selectedMapLineZ/clearMapLineSelection`vtkCellPicker+PickFromList 只拾可见足迹、选中高亮黄加粗、`mapLineZOffset_` 持久);`PickInteractorStyle` lock2D 下命中足迹→Z 拖动(`onPick2D/onDrag2D/onDrag2DEnd`+`worldPerPixelZ` 像素→世界Z)、否则平移;`InteractionManager::pickStyle()` 暴露样式;`main.cpp` 接回调 + 高程读数浮层(复用提示样式)。
- **待用户实跑**:①拾取灵敏度(tol 0.012)②拖动 Z 灵敏度/方向(上移=抬高)③多选拖动④读数是否合理(现为 actor 包围盒中心世界 Z含 placement+偏移,未除 VE)。
- **C下一步**dd_raster 纳入 2D 过滤 + 按 ddCode 分派渲染 + 栅格地理配准贴地形。**阻塞dd_raster 数据端点未确认**(需后端给「像素 + 四至/投影」端点)。
---
## 1. 环境 / 命令 / 铁律(必读)
- **构建**:用 PowerShell 工具跑 `cmd /c "D:\Git\lanbingtech\geopro\build.bat app"`Claude 的 Bash 跑 build 会被环境劫持,用 PowerShell。测试 `build.bat test`ctest**439 用例**)。`vswhere.exe not recognized` 噪声无害。LNK1104=exe 被运行中的 app 锁,需用户关 app 才能链接。
- **回复用中文**(用户要求)。
- **CLAUDE.md 两条绑定规则**:①发现技术债当场修,不以"非本轮引入"搪塞;②**能自己做的绝不让用户做**——日志(`%LOCALAPPDATA%/Geomative/Geopro3/logs/geopro_*.log`)/数据/构建/诊断都自己来,只在 LNK1104 关 app、或真正产品决策才找用户。
- **git**:精确 `git add <files>`(仓库有并行 GPR 会话的脏文件 `.superpowers/sdd/*`、`docs/.../poc-lod-shots/*.png`**勿误提交**)。提交无 Co-Authored-By全局禁
- **后端 token可访问真实接口用户给的**`geomativeauthorization: Geomative e6c1259748644c8da0954d864bb82604`base URL `http://tenant.geomative.cn/pop-api`;样例 projectId `1439735554211840`。可用 curl 查接口。
---
## 2. 本会话已完成(提交清单,新→旧)
**创建异常全链路(核心工作量)**
- `227ee8f` 二维分析 specdoc
- `c1a824e` 二维维度分类对齐数据字典 DD0623去 3 个已删除轨迹类型dimensionOf/dimOf/测试)。
- `e8bb2f8`/`1648ccb`/`f230ca8` 异常绘制提示vtkTextActor 渲染不出中文 → 改 **app 层 QLabel 浮层**(右上角、深底方角、不挡鼠标);列表切到别对象清切片选中(`deselectSlice`)。
- `9782a2b` 删除切片/异常加确认框 + **弹框按钮全局中文化**Qt zh_CN 翻译器 + `formkit::addDialogButtons` 默认「确定/取消」+ 打包补 qtbase_zh_CN.qm
- `306d7bc` 提示移右上角 + **线双击结束含双击位置**(去回滚)。
- `d7ab770` **切片保存后定稿锁定**`SetInteraction(0)` 不可移动/旋转)+ VTK/列表菜单去「保存·另存」。
- `91a7106` **结束手势**:点=单击即完成、线=双击、面=**点回起点闭合**(近起点 12px 吸附+橡皮筋指向起点)。
- `1a70ca0` 异常对话框加**样式预览**(选中类型 legend 可视化:点球/线/面)。
- `4ae8286` **异常截图配色与切面一致**:取 widget 自身 `GetColorMap()` 输出(非另建 LUT→ 逐像素一致 + RGBA 外区透明消蓝边。
- `d470dc8` 双击/单击隔离(后被 `306d7bc` 改回"含双击位置"+ 异常类型下拉误显「暂无数据」(`EmptyAwareComboBox::realItemCount` 用错 flags 角色→改 `model()->flags()`)。
- `04af569` 返工(点交互/点渲染小球/截图/类型空态)。
- `75c1327`→`3ed1ea7`→`58544ff`→`c6756aa` 截图相机方案A(已废)、**异常类型接平台真实类型**(`listExceptionTypes` 按形态)、**点/线/面三态**子菜单、**样式接平台 legend**(`getExceptionTypeDetail`)。
- 截图最终方案:**只从切片 2D 剖面图、按异常几何 buffer 裁剪**`captureAnomalyShotFromSlice`GIS buffer+掩膜,点圆/线胶囊/面外扩多边形)。
**其它**
- `56e4b3a` 登录验证码容器白底。
- `8563693` 分段折叠向上收起stretch 动态:展开=1/折叠=0+尾弹簧)。
- `d6e52cb` 三维分析分段面板视觉打磨chevron 段头/描边新增按钮/顶部留白)。
- `fb911a9` 坐标轴面板硬编码色 token 化。
- `cdd7613` vtk-3d-openapi 文档对齐实测DsPage 行在 `data.list`、DsRow 全字段、装置不在 data/page
- `2f6ec7d`→`1742b75`→`31ad7a4` **装置筛选**data/page 行不带 properties→改按行自带 `typeName/dsTypeCode` 筛选parseDsRows 兼容对象形态。
- 期间生成了一次安装包:`installer/dist/Geopro_Setup_3.0.0-20260625.exe`(脚本 `installer/build_installer.ps1`,含 windeployqt+样本+PROJ+vc_redist
---
## 3. 创建异常功能现状已落地端到端可用mock 保存)
入口VTK 切片右键 →「创建异常 → 点/线/面」。流程:圈定(AnomalyDrawTool)→草稿渲染→**从切片2D图buffer裁剪截图**→对话框(名称/异常类型[接平台]/样式预览/备注/截图)→保存(mock)→刷新树+渲染异常 actor。
- **关键文件**`src/render/interact/AnomalyDrawTool.{hpp,cpp}`(三态绘制:点单击完成/线双击/面点起点闭合Esc取消/Backspace撤点`src/app/AnomalySaveDialog.{hpp,cpp}`(接 `cmdRepo.listExceptionTypes`/`getExceptionTypeDetail`,样式预览);`src/app/SliceExport.{hpp,cpp}``captureAnomalyShotFromSlice``src/render/actors/AnomalyActor.cpp`(点=球/线=折线/面=闭合多边形);`src/app/main.cpp`onSliceContextMenuRequested ~509 起:菜单+绘制+对话框+保存接线QLabel 提示浮层)。
- **样式来源**:选中平台异常类型 → `getExceptionTypeDetail` 取 legendpolylineColor/Width/Shape, pointColor…→ 套到异常 lineColor/lineWidth/dashed。
- **截图**:只裁切片那张 2D 剖面图(`InteractionManager::selectedSliceColorImage` 现取 `SliceTool::coloredResliceImage()`=widget ColorMap 输出,与屏幕同源)。
### 仍卡在后端P3非客户端问题
- **异常真保存 `newException`** 卡:异常 `remarkSourceId` 须指向真实 dsObjectId三维体/切片),但真后端**无任何登记三维体/切片为 dsObject 的端点**(实测 `voxel/generate`、`slice/generate`、通用 `dsObject create` 全无)。当前 `saveAnomaly` 是内存 mock。后端补登记端点后整链可接真。
- 平台「点」类异常类型该项目为空(线/面有)→ 画点时类型下拉空属正常。
---
## 4. 二维分析改造——已确认的设计决策(与用户逐条敲定)
1. **不分两个场景**:一个 3D 地形场景(带高程)+ 底图贴地形,两栏只是相机不同。"二维分析只是 3D 的固定视角"。
2. **二维相机**:锁定**近俯视(7580°非绝对正俯视)**,禁旋转,仅平移+缩放。(绝对正俯视看不出高程→留倾斜)
3. **切 tab 显隐**:翻另一方数据集 `SetVisibility`**不显示,非清空**)→ 性能零代价VTK 跳过不可见 actor、切换瞬时、只占内存。地形+底图常驻。**绝不清空**(重体素重建会卡)。
4. **高程拖动(C1)**:二维里选中 2D 内容(单/多选)→ 竖向拖动只改**高程 Z**、锁 XY、实时读数。用途=分离叠在一起的 2D 层。
5. **维度过滤**2D = `dd_trajectory_data`+ C 期 `dd_raster``dd_radar_2d/3d` 是 3D不进 2D。已对齐 DD0623`c1a824e`)。
6. **雷达客户反馈**:只影响数据模型 + **3D 视图**渲染(二维雷达=线、三维雷达=切面、带打标)+ 详情页校准——**均属另立任务,不在本次 2D 改造**。2D 只显示轨迹线、打标暂不做(与本设计一致)。
---
## 5. 关键代码锚点(二维分析改造用)
- 切 2D 视图模式钩子:`Column2DDataset::view2DModeChanged` → `main.cpp``sceneCtrl`(搜 `view2DModeChanged`)。
- 维度分类:`src/app/DatasetDimension.cpp::dimOf`col2D 用);`src/data/api/Api3dRepository.cpp` + `src/data/repo/LocalSample3dRepository.cpp::dimensionOf`
- 2D 内容注入:`main.cpp` ~502 `col2D->setDatasets(splitByDimension(...).dim2D)`;勾选渲染 `Column2DDataset::checkedDatasetsChanged``loadMapLine`/`MapLineActor`。
- 场景/相机/可见:`VtkSceneView`actor 管理)、`VtkSceneController`、`InteractionManager`(拾取/选中,新增 `deselectSlice`)。
- 地形+底图:`buildTerrain`带高程VE 垂直夸张相关)、`TileBasemap`(天地图按经纬贴)。
- 数据字典:`D:\Projects\GEOPRO\DD0623.xlsx`46 个 ddCode序号/ddCode/领域/含义/状态/**展示模式(2D/3D)**/**展示形态**/备注…。解析openpyxl 可用,中文 GBK 终端会乱码→导 UTF-8 文件再 Read。
---
## 6. 待确认/风险
- **dd_raster 数据端点**(像素+四至/投影)未确认 → C 期阻塞。
- 近俯视角度需实机调;高程是否落库暂定会话内(不落库)。
- §3 翻可见标志需可靠区分每个 actor 的维度归属。
- 切相机/可见与现有 view2DModeChanged、底图、地形 VE 逻辑勿打架。
- **GUI 无法登录自测**:相机/截图/交互类改动我只能保证编译+逻辑,视觉/手感需用户实跑反馈。
---
## 7. 相关记忆/文档
- 记忆库 `MEMORY.md`(如 login-chain-truth、vtk-3d-persistence-structure、build-vs2026-vcpkg-gotchas、deploy-hardcoded-dev-paths 等)。
- vtk-3d API 设计草案:`docs/api/vtk-3d-openapi.json`已对齐实测0.6.1)。
- 真后端 API`docs/apis/business_OpenAPI.json`。
- 二维分析 spec`docs/superpowers/specs/2026-06-26-2d-analysis-topdown-elevation.md`。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,101 +0,0 @@
# 二维分析:锁定俯视相机 + 内容显隐 + 高程拖动 — Spec2026-06-26
## 0. 一句话目标
把「二维分析」从"另一套平面地图"改为**同一个 3D 地形场景的一个锁定近俯视视角**:切 tab 只切相机+翻另一方数据集的可见标志(不清空),二维内容(轨迹/栅格)落在带高程的地形上,且可选中后沿高程上下拖动分离。
---
## 1. 背景与现状
- 三维分析栏(`CategoryAnalysisTab`)与二维分析栏(`Column2DDataset` / `col2D`**共用同一个 `VtkSceneView` / `InteractionManager` / `renderWindow`**。现状:两栏勾选的 actor 叠在同一场景,切 tab 不切相机、不区分内容。
- 二维内容现状:`col2D` 勾选 → `loadMapLine``dd/ert/trajectory/line`)→ `MapLineActor`lat/lon 折线);有 `view2DModeChanged` 信号已接到 `sceneCtrl`2D 视图模式钩子,本 spec 在其上扩展)。
- 地形 + 底图:场景已有地形(`buildTerrain`,带高程)+ 天地图底图(`TileBasemap`,按经纬贴)。
- 维度分类:`DatasetDimension::dimOf` + `Api3dRepository/LocalSample3dRepository::dimensionOf`,已对齐数据字典 DD06232D = `dd_trajectory_data`commit c1a824e
**用户确认的认知**:二维分析"只是 3D 的固定视角",底图不是平面图、是**带高程的地形图**;内容沿用不清空。
---
## 2. 核心设计:一个场景 + 两种相机
- **不分两个场景**,只有一个 3D 地形场景(地形 + 底图 + 全部已勾选数据),两栏区别仅在**相机**与**哪类数据集可见**。
| | 三维分析 | 二维分析 |
|---|---|---|
| 相机 | 自由透视(可旋转/倾斜/平移/缩放) | **锁定近俯视**(不可旋转,仅平移+缩放)|
| 可见数据集 | 3D 数据集(体/切片/剖面…可见2D 数据集隐藏 | 2D 数据集(轨迹/栅格可见3D 数据集隐藏 |
| 地形 + 底图 | 常驻可见 | 常驻可见(同一地形,俯视看即"地形图"|
---
## 3. 二维分析的相机:锁定近俯视
- 切到二维分析 → 相机切到**近俯视固定角(约 7580°非绝对正俯视**:理由——绝对正俯视在正交/小透视下高程变化不可见留一点倾斜以便看出高低§5 拖动反馈需要)。
- 锁定:**禁用旋转**interactor style 不响应旋转/倾斜),仅保留**平移 + 缩放**。
- 切回三维分析 → 恢复自由透视相机(恢复切走前的视角或合理默认)。
- 实现锚点:扩展 `view2DModeChanged` 钩子 → `VtkSceneController` 切相机模式 + 切 interactor style或在 style 内按模式禁旋转)。
---
## 4. 内容显隐:切 tab 翻可见标志(**不清空**
- 切 tab 时,对"另一方"的数据集 actor 用 `SetVisibility(false)`,切回 `SetVisibility(true)`。**不移除 actor、不重建**。
- 性能VTK 渲染跳过不可见 actor → 隐藏内容**不参与绘制、不耗 FPS**;切换瞬时(无重插值/重传 GPU唯一代价是内存/显存驻留(数据本已加载,无新增加载)。
- **禁止用清空**重体素GPR/ERT每次切回要重插值+重传 GPU必卡。
- 地形 + 底图两边都不隐藏。
- 实现锚点:`VtkSceneView` 按数据集维度(`dimensionOf`/记录每 actor 的维度)批量翻可见标志;切 tab 时调用。
---
## 5. 高程拖动C1选中 2D 内容沿 Z 上下移
- 二维分析里,**拾取选中已渲染的 2D 内容**(轨迹/栅格),支持**单选 / 多选**。
- 选中后**竖向拖动 → 仅改其高程 Z离地高度****锁死 X/Y**(不动地理位置)。用于把叠在一起的 2D 层分离、看清。
- 拖动时**实时显示当前高程数值**(屏幕浮层读数)。
- 近俯视固定角§3使高低可见。
- 实现锚点:新增/复用一个 2D 拾取-拖动交互(类似切片 widget 但只允许 Z 平移 + 多选actor 的 Z 偏移持久(切走再回保留)。
- 待定:高程是否需要随数据保存(暂定仅会话内 actor 变换,不落库;接真实端点再议)。
---
## 6. dd_raster二维栅格过滤 + 渲染(本期新增)
- 数据字典 DD0623`dd_raster`(栅格/遥感影像,**本次新增**,展示模式 2D形态=栅格)。
- 纳入 2D 过滤:`dimOf`/`dimensionOf` 增 `dd_raster``Dim2D`;但 col2D 勾选渲染须**按 ddCode 分派**——轨迹走 `loadMapLine`,栅格走**栅格加载**(不可让栅格走轨迹端点)。
- 渲染:取栅格的**地理范围(四至/仿射)+ 像素**,作为**地理配准的纹理平面贴到地形上**(带高程,可被 §5 高程拖动),类似底图瓦片按经纬度定位。
- 依赖dd_raster 的**数据端点**(返回像素 + 四至/投影)——**待确认**,未明确前 §6 不落地(先做 §3§5
---
## 7. 维度过滤口径(对齐数据字典 DD0623已部分落地
- 2D足迹/栅格):`dd_trajectory_data`(统一通用轨迹,"保留",已并入 dd_radar_rtk_trajectory+ `dd_raster`(本期新增,随 §6
- 已删除、不再单列:`dd_radar_rtk_trajectory` / `dd_transient_electromagnetic_trajectory_data` / `dd_radar_channel_trajectory`(字典均"删除"。已清理commit c1a824e。
- `dd_radar_2d` / `dd_radar_3d`:字典为 `展示模式=3D`(通道剖面 / 三维插值模型)→ **属三维分析,不进 2D 过滤**
---
## 8. 与雷达客户反馈的边界(本 spec **不含**
- 雷达 TM 模型(单/双/多频,每频一个 `dd_radar_2d`/`dd_radar_3d`,共用一个 `dd_trajectory_data` 轨迹)→ 数据模型,与本 spec 无冲突。
- 雷达**数据在 3D 视图的渲染**(二维雷达=线/curtain、三维雷达=切面,按 trace 坐标,带打标 hover tip**三维分析的另立任务**
- 详情页用 trace 坐标校准异常 + 剖面打标 → 详情页另立任务。
- **2D 视图只显示轨迹线、打标暂不在 2D 展示**(客户 #6 修正)→ 与本 spec 的"2D 显示轨迹足迹"一致,无新增 2D 工作。
- 雷达轨迹就是 `dd_trajectory_data`,本 spec 的 2D 分析按统一轨迹处理,无需特判。
---
## 9. 实现分期
- **A. 一场景两相机**:切 tab → 锁定近俯视/恢复自由相机 + 翻另一方数据集可见标志§3、§4。基础先做。
- **B. 高程拖动**2D 拾取单/多选 + 仅 Z 拖动 + 锁 XY + 实时读数§5
- **C. dd_raster**:过滤纳入 + 按 ddCode 分派渲染 + 栅格地理配准贴地形§6。依赖栅格数据端点确认。
---
## 10. 风险 / 待定
- 近俯视角度7580°需实机调用户若坚持绝对正俯视则 §5 高程反馈改为纯数值(不直观)。
- §4 翻可见标志需可靠区分每个 actor 的维度归属(按数据集 ddCode 记录维度 → actor
- §5 高程是否持久化/落库待定(暂会话内)。
- §6 dd_raster 数据端点(像素 + 四至/投影)未确认 → C 期阻塞点。
- 切相机/可见标志切换需与现有 `view2DModeChanged`、底图、地形 VE垂直夸张逻辑兼容勿互相打架。
---
## 11. 验收
- 切到二维分析:相机变近俯视、**不能旋转**,只能平移+缩放3D 数据集(体/切片/剖面)不可见,轨迹+地形+底图可见。
- 切回三维分析恢复自由相机3D 数据集重新可见2D 轨迹隐藏。切换**瞬时无卡顿**(无重建)。
- 二维分析里选中一条/多条轨迹,竖向拖动→只改高程、地理位置不动、实时显示高程;叠在一起的层能被拉开。
- C 期)勾选 dd_raster → 栅格按地理范围贴在地形上、可被高程拖动;轨迹与栅格各走各的加载路径、互不串。
- 维度过滤与数据字典 DD0623 一致2D=trajectory_data+rasterradar_2d/3d 归 3D

View File

@ -1,210 +0,0 @@
# GPR 多通道三维体渲染性能问题 — 分析文档(供外部专家评审)
> 自包含技术文档。读者无需了解本代码库内部,只需具备 GPU 体绘制 / VTK 基础。
> 目的:把"探地雷达(GPR)多通道阵列数据渲成可交互三维体"遇到的性能问题、已试方案、实测数据、
> 待定关键点完整呈现,供外部专家判断方向。
---
## 1. 背景与系统
- 桌面端 C++ 应用Qt6 + **VTK 9.6.2**),渲染探地雷达(GPR)采集的地下三维数据,要求**可交互**(旋转/缩放)。
- 渲染用 VTK 的 `vtkGPUVolumeRayCastMapper`OpenGL GPU 光线投射体绘制)。
- 当前测试机 GPU32 个着色器纹理单元(典型独显/中端)。
## 2. 数据特征(关键,决定一切)
多通道阵列 GPR一次采集一条"测线(line)"
- **道(trace)**:一个位置一根天线的垂直回波,深度方向 **821 采样**
- **通道(channel)**:阵列横向并排的多对天线,本数据 **14 通道****相邻通道横向间距 ≈ 10.5cm**(来自 `.ord` 文件真实偏移 -0.686…+0.686m,跨度 1.37m)。
- **沿测线道间距 ≈ 4.9cm**(比横向通道间距细 ~2 倍)。
- 一条测线:沿路 **~45305 道**,覆盖 **~2.2km**(一条南北向道路)。
- **共 20 条测线 = 同一条路来回扫 20 趟**(车载,每趟阵列覆盖约 1.4m 宽,多趟铺横向)。
**单条线 = 一个三维体**X=沿测线(~45305)、Y=通道(14)、Z=深度(821)。
**关键业务约束(来自现场专家)**
- 通道太稀10.5cm)→ 需**线内通道间插值**加密(相邻真实天线之间插,物理成立);
- **绝不做"测线之间"的插值**(车与车之间是真实物理空隙,插出来"信号全是假的",工程上不可接受);
- 多条测线"分开各自插值,渲染可以合到一起"。
GPR 数据的统计特征(实测,见 §6**~91% 体素近零(反射层之间是空的),但反射层横贯整个深度分布**(不是集中一坨)。
## 3. 已建成并验证可用的功能(不是问题所在)
1. **线内通道插值**:读 `.ord` 真实横向偏移,规则化到 2.5cm 网格、相邻通道线性插值(不跨线)。
实测 Y 由 14 加密到 **56**。有单元测试。
2. **多体单遍合成**20 条独立体(各自插值)作为一个 `vtkGPUVolumeRayCastMapper` 的多个端口注册进
`vtkMultiVolume`**单遍 ray-cast 合成**(而非每条体一遍)。已验证。
3. **纹理单元上限自动退避**:单个 multi-volume 同时挂的体数受 GPU 每着色器纹理单元上限制约
(每体约吃 4 个单元 → 32 单元机上**一个包最多 7 体**,第 8 体报错并丢体)。已实现"渲一帧→报错则
每包减 1 重建重渲"的自动退避(强制 K=12 → 自动退避到 7无丢体
4. **运行时换贴图边界**(确定性测试结论):给某端口**就地换贴图**——若**保持包围盒不变**(同范围、
只改 Y 密度)则 multi-volume 算得对;若**改包围盒**任意子区域、origin/范围/spacing 变)则破坏
其缓存 `TexToBBox` → 体断开/消失。
5. 通道维 LOD、统一传函、色标图例等。
## 4. 核心问题:性能
- **20 条密体Y=56总览交互极卡**:静止 ~1.7 fps旋转/缩放掉到 < 1 fpsGPU 100%)。
- 渲染**视觉正确**(雷达剖面纹理清晰、横向连续、合成无误),纯属性能。
- 现有提速手段都是**"交互时降质"**方向(降屏幕分辨率、加粗采样步长)——**损可见质量**,治标。
用户明确要求:"有没有不损可见质量的根本性提速(业界最佳实践)?"
## 5. 已分析/已试的所有方案(含理由与状态)
| # | 方案 | 理由 | 状态 / 结论 |
|---|---|---|---|
| A | multi-volume 单遍合成 | 把"N 体=N 遍 ray-cast"降到分包遍数 | ✅ 已实现。但单遍内每步仍要在重叠体里逐个采样 |
| B | 纹理单元自动退避分包 | 绕开 GPU 纹理单元上限、不丢体 | ✅ 已实现K=7/包20 条=3 包)。代价:**跨包重叠合成不正确(接缝)** |
| C | 交互降屏幕采样(ImageSampleDistance) | VTK AutoAdjust 标准手段 | ✅ 已做。**损质量**;且 AutoAdjust 只降屏幕、不降沿光线步长 |
| D | 交互手动加粗沿光线步长(SampleDistance) | 通道插值后 Y 密→自动步长极细→巨卡;这才是大头 | ✅ 已实现(`--sampleDist`/`--dragSampleMul`)。**损质量;且用户尚未实测**(见 §7 待定) |
| E | 通道维 LOD远疏近密换 Y 平面子集)| 保包围盒换贴图(#4 验证安全) | ✅ 已实现。但**只减纹理内存、不减每步重叠体采样次数 → 对此瓶颈几乎无效** |
| F | **装箱单体(binning)**:各线先逐线插值,再把**真实道**摆进一个总览网格体(空隙透明、不跨线插值)| 一个体一遍、每步采 1 次 → ~20×真实数据无假信号 | ⚠️ 技术可行、合规,但**用户否决**:装箱合并后**总览里分不出各线**,而用户要"一起渲染时仍能逐线区分/查看"→ 合并即失去意义 |
| G | **空体素跳过 ESS换 OSPRay/ANARI 后端)**:跳过透明背景块 | 业界对稀疏体的头号提速、不损质量 | ❌ **实测对本数据收益有限**(见 §6保质量阈值下仅 ~2×且**ESS 跳的是空区、不解决"重叠"**。VTK 库存 mapper 无自动 ESSOSPRay 本环境未编、vcpkg 无包 |
| H | 减少同屏体数(只渲选中 ≤7 条)| 真实工作流本就是选几条1 包 1 遍 | ✅ 免费、永远有效(使用方式,非技术) |
## 6. 关键实测数据
### 6.1 ESS空体素跳过潜力——零依赖实测决定要不要上 OSPRay
对一条真实测线密体5702×56×789按块算 min/max统计"整块落在近零透明带"的占比(=ESS 可跳块):
| 透明带半宽(相对半值域) | 8³ 块可跳占比 | 理论提速上限 1/(1占比) | 说明 |
|---|---|---|---|
| 5% | 8% | 1.1× | 极保守 |
| **10%(保质量,不丢弱反射)** | **52%** | **~2.1×** | — |
| 20% | 80% | ~4.9× | 开始把弱反射当背景丢 |
| 30%(激进)| 90% | ~10× | 明显损质量 |
- 体素层面 **91% 近零**但块层面ESS 实际粒度)保质量阈值下**只能跳 ~52% → 理论 ~2×**。
- 原因:**反射层横贯整个深度分布**,多数块里总混着信号、跳不掉。要 10× 须用激进阈值(损质量)。
- **更关键**ESS 跳"空区"**不减少"重叠"**——在有信号的块里仍要逐个采样所有重叠体。
### 6.2 其它实测
- multi-volume 纹理单元上限:本机 7 体/包32 单元),第 8 体报 "Hardware does not support the number of textures"。
- 体维度示例coarse 4 → ~11000×56×793/线coarse 32 → ~1400×56×780/线。
- 全 20 条 densecoarse 32底图 level 0、Y=56、分 3 包,**渲染正确**;交互 ~1.7fps**未加 D 方案的旧构建**)。
## 7. 关键点——已实测,结论修正(原假设两条都被推翻)
### 7.1 "重叠几层"——实测:**平均 ~8.7 层、最大 15 层(不是 ~23也不是 20**
纯几何测(各线世界 AABB 投到 X-Y 俯视 footprint、细网格统计每格覆盖层数`--overlapStat`
- footprint横向 X≈37m、沿路 Y≈2.2km
- **有体覆盖处平均重叠 8.74 层,最大 15 层**;穿 1214 个体的格子占 ~42%。
- **结论修正**:原"~23 层"假设**错**(开发团队和外部专家都猜偏了);**重叠是真实的大瓶颈**。
- **且这 9 层是冗余**20 趟是同一条路反复扫,同一地下点被测了 ~9 次 → 这 9 个重叠体在该处**都非空**
(同一地下结构)→ **ESS 在重叠区一个都跳不掉**(再次印证 ESS 不解决重叠)。
### 7.2 "采样 vs 重叠谁是大头"——实测:**采样瓶颈fps 线性正比于步长**
20 条密体、静止近景、离屏(步长越大越快越糙):
| sampleDist | fps | 相对 |
|---|---|---|
| 0.2(≈自动细)| 1.3 | 1× |
| 0.5 | 3.2 | 2.5× |
| 1.0 | 5.9 | 4.5× |
| 2.0 | 11.3 | 8.7× |
| 4.0 | 20.9 | 16× |
- **步长翻倍→fps 翻倍 → GPU 是采样瓶颈**。总开销 ≈ 光线数 × (光线长/步长) × ~9 个重叠体。
- D 方案(手动步长)**确实直接、强力提速**;但**保质量的步长(≈ Nyquist0.5×体素)下仍只 ~2 fps**
——因为 **9× 冗余重叠**把它乘了回去。要到交互级(10+fps) 得把步长粗到 ~2.0(欠采样、损 Z 薄层)。
### 7.3 合并诊断(两测合起来)
**慢 = 采样密度 × ~9 倍冗余重叠,两者都真实。**
- D 方案(粗化采样):提速强,但保质量步长下被 9× 重叠压回 ~2fps要交互须损质量。
- **唯一"保质量又快"的,是去掉那 9× 冗余重叠**(同路重扫的同一地下点):合并/装箱(取真实道、不跨线
插值)→ 一个体一遍 → ~9× 提速、且 Nyquist 步长下也能交互、**零质量损失**(冗余测量本就该合并降噪)。
- **但这与用户"保持 20 条可区分"直接冲突**——而这 9 层在物理上是**冗余测量**(同一地下结构扫了 9 遍),
保持它们"可区分"的工程价值存疑。
### 7.4 CPU OSPRay vs GPU仍未测
ESS 对本数据 ~2× 且不解决 9× 重叠OSPRay 主要 CPU、对手是 GPU 数千核。**很可能换 OSPRay 比现状还慢**
且为 ~2× 重编整个 VTK 投入产出极差。不建议在去掉冗余重叠之前考虑。
### 7.5 "多线为何卡"的根因确诊passcost决定架构——结论**不是固定开销,是没用 LOD**
> 背景:最初 P11/P12 是"各线独立 mapper + 视野 LOD",实测仍 0.5fps。需确诊卡在三个嫌疑哪个:
> ①LOD 选区没削小 ②N 遍固定开销 ③重叠没摊掉。`passcost` 命令N 个独立 GPU mapper 各渲一个 64³ 小体
> (模拟 LOD 削过的小区),分"铺开/不重叠"与"叠在一起/重叠",测离屏稳态 fps vs N。
| N | 铺开(不重叠) fps | 叠加(重叠) fps |
|---|---|---|
| 1 | 177 | 204 |
| 5 | 162 | 43 |
| 10 | 144 | 22 |
| **20** | **78** | **11** |
**判读(决定性):**
- **嫌疑 ②N 遍固定开销)排除**20 个独立 mapper 铺开仍 **78fps**177→78远非线性
**各线独立 mapper 架构上完全可行,固定开销温和。"multi-volume 单遍 ⊥ 视野 LOD"这个不可兼得不致命**——
放弃单遍、回独立 mapper 并不慢。
- **嫌疑 ③(重叠)真实但小体下可控**:叠加随 N ~1/N每条光线乘 N**20 层 64³ 叠加仍 11fps**(可用),
再叠屏幕降采样更快。
- **嫌疑 ①(选区没削小)= 真凶**passcost 小体 20 层叠加=11fps而真实 view-all 只 1.7fps——差距全在
**贴图大小**:当前渲的是**整卷底图**~11000×56×200 ≈ 上亿体素/条),**根本没用视野 LOD 把它削成小区**。
这是**最好结局:可修,不动地基,只需真正用上 LOD**。
**对架构的直接含义**:本会话引入的 **multi-volume 单遍是错误取舍**——为"单遍"关掉了 LOD、改固定整卷贴图
导致大贴图 × 9 层重叠 = 1.7fps。而 passcost 证明独立 mapper 够快,**根本不必为单遍牺牲 LOD**。
## 8. 部署约束(硬件不确定,跨厂商)
客户机配置未知(可能无独显,或 N卡/A卡/Intel。**没有任何单一渲染器能在 N 卡和 A 卡上都做"GPU+ESS"**——
GPU 体光追渲染器全厂商锁定N 卡→NVIDIA VisRTX/OptiX/IndeXIntel→OSPRay-GPUA 卡→基本无成熟方案)。
跨厂商唯一通用的是 **OSPRay-CPU免显卡、任意 x86****OpenGL任意 GPU、但无 ESS=现状)**
若上多后端,需"OSPRay-CPU 保底 + 探测到 N卡/Intel独显时升对应 GPU 后端 + OpenGL 终极兜底"。
## 9. 关键问题——大多已被实测回答
1. ~~有没有不损质量的根本性提速法~~ **有且是通用解LOD视野自适应多分辨率**——让 GPU 单帧实际
ray-cast 的体素量**与数据总量解耦、只与"屏幕能看清的量"挂钩**Task 12c 单体已验证 752/380fps
§7.5 passcost 证明它**对多线也成立**(独立 mapper 开销温和20 条铺开 78fps
2. ~~"卡"的主因~~ **已确诊§7.5):不是 N 遍固定开销(排除),是【当前根本没用 LOD、渲整卷大贴图】嫌疑①。**
本会话引入的 multi-volume 单遍为"单遍"关掉了 LOD → 大贴图 × 9 层重叠 → 1.7fps。
3. **9 层重叠的正确定位**(外部专家纠偏 + passcost 印证):它只是**这批数据(同路重扫 20 趟)的特例倍数**
**不是渲染本质问题**。本质是"单帧采样量 > GPU 吞吐",通用解是 LOD扛任意大数据无重叠但更大也能扛
9 层重叠在 LOD 之上降级为"一个被摊薄的常数因子"passcost20 层小体叠加仍 11fps
**不要让渲染架构围绕这个特例设计。**
4. ESS/OSPRay/多后端:**继续埋掉**——ESS 对本数据 ~2× 且不解决重叠、CPU 对手是 GPU且**它解决的是 LOD
已经解决的通用问题**,投入产出差。
## 10. 最终结论passcost 确诊后,架构清晰)
- **渲染架构 = LOD 中心(视野自适应、单帧量与总量解耦)。** 这是扛"任意大数据"的通用根本解,
Task 12c 单体已验证、§7.5 passcost 多线也成立。
- **本会话的 multi-volume 单遍是错误取舍**:为"单遍合成"牺牲了 LOD、改固定整卷大贴图正是当前 1.7fps 的
直接原因。passcost 证明独立 mapper 开销温和20 条 78fps**根本不必为单遍弃 LOD**
- **正解 = 各线独立 mapper + 视野 LOD逐线用 Task 12c 引擎)+ 停手才重建**(不每帧重建,避免 P11/P12
那种"20 条每帧重建上传"的 thrash——那才是 0.5fps 的另一半原因,与稳态 ray-cast 无关,已被 P13 思路解决)。
让每条线只渲视野内小区 → 即使 9 层叠加也可用。
- **9 层重叠 = LOD 之上的可选应用层优化**(对同路重扫冗余可"合并/降噪",顺带省 9×**不进渲染地基**。
用户要逐条区分就不合并(靠 LOD 摊薄),要纯总览就合并。
- **采样步长D 方案)= LOD 框架内的质量旋钮**,非独立根本解。
- **ESS/OSPRay/多后端:不做**(不解决 LOD 已解决的通用问题,对本数据收益差)。
**下一步(确诊已完成,可开工)**:把多线总览从"multi-volume 单遍固定整卷"改回"各线独立 mapper +
视野 LOD + 停手重建",让单帧渲染量随视野走、与 20 条总量解耦;实测多线总览是否达交互级。
这是顺着通用 LOD 框架、被 passcost 数据支撑的明确方向——不再围着 9 层重叠这个特例转。
---
### 附:相关已落地代码 / 诊断工具(如专家要复现)
- 通道插值:`src/io/gpr/GprGeometry.cpp::planChannelInterpolation` + `Gpr3dvVolumeBridge.cpp`
- 多体合成/退避/质量控制/通道 LOD`tools/gpr_poc/main.cpp::cmdViewAll`
- **诊断命令**`tools/gpr_poc/main.cpp`,可直接跑复现 §6/§7 的数):
- `gpr_poc ess-stat <dir> <line>`ESS 空块潜力§6.1
- `gpr_poc view-all <dir> <gps> --overlapStat`实测重叠层数§7.1
- `gpr_poc view-all <dir> <gps> --sampleDist D`步长↔fps§7.2
- `gpr_poc passcost --size 64 --overlap 0|1`N 遍开销 vs 重叠 隔离测§7.5
- 数据/插值口径 spec`docs/superpowers/specs/2026-06-25-gpr-line-channel-interpolation-and-multivolume.md`
- 多后端 ESS 架构 spec**结论:不做,见本文 §10**`docs/superpowers/specs/2026-06-26-gpr-multibackend-ess-rendering.md`
---
## 摘要(一页结论,供决策)
1. **现象**20 条通道插值密体总览,~1.7fps、交互更卡。视觉正确,纯性能。
2. **确诊**passcost 隔离测):**不是 N 遍固定开销**20 独立 mapper 铺开 78fps排除
是**当前根本没用视野 LOD、在渲整卷大贴图**× 9 层重叠)。本会话的 multi-volume 单遍为"单遍"
牺牲了 LOD是直接原因。
3. **通用根本解 = LOD**单帧渲染量与数据总量解耦扛任意大数据Task 12c 单体 752fps、passcost 多线
也成立。9 层重叠只是**本批数据的特例倍数**,是 LOD 之上一个可摊薄/可选合并的因子,**不是架构核心**。
4. **正解**:各线独立 mapper + 视野 LOD + 停手才重建(弃 multi-volume 单遍)。
5. **明确否定**ESS/OSPRay/多后端(对本数据 ~2×、不解决重叠、解决的是 LOD 已解决的通用问题)。

View File

@ -1,132 +0,0 @@
# ⚠️ 本 spec 已被实测推翻,勿照此实现
> **结论(见 `2026-06-26-gpr-3d-render-perf-ANALYSIS-for-review.md` §10ESS/OSPRay/多后端【不做】。**
> 实测ESS 对本数据 ~2× 且不解决重叠passcost 确诊"多线卡"的真因是【没用视野 LOD、渲整卷大贴图】
> 不是固定开销。**正解 = LOD 中心(各线独立 mapper + 视野 LOD + 停手重建),见下方实现计划。**
> 本文余下"多后端/ESS"内容仅作历史记录。
---
## 实现计划LOD 中心多线总览 — 已确诊、可执行)
**目标**:把 `cmdViewAll` 从"multi-volume 单遍 + 固定整卷大贴图"改为"各线独立 mapper + 视野 LOD +
停手才重建",使单帧渲染量随视野走、与 20 条总量解耦。
**改动步骤(`tools/gpr_poc/main.cpp::cmdViewAll`**
1. `PlacedSource` 加回**每线自己的 `vtkSmartVolumeMapper`**GPU 模式);删 multi-volume 用法
multiMapper/multiVol/port 不再需要,但可暂留不碍)。
2. **装配**:删 `buildBundles` + 退避(无 multi-volume 即无纹理单元上限);改为逐线
`mapper->SetInputData(baseImage)``volume->SetMapper(mapper)``ren->AddVolume(volume)`
各线 mapper 收进 `mappers` 向量(供质量控制)。
3. **开 LOD**`gViewAllBaseOnly = false`(启用引擎选区换图);引擎换的是"改包围盒的子区域"
各线独立 mapper 下**安全**(无 multi-volume 可破坏)。
4. **关 channel LOD**`gChanLod = false`——引擎金字塔已逐级降 Y无需单独抽 Y 平面。
5. `viewAllPickOneLine``ps.mapper->SetInputData(ps.currentImg); ps.mapper->Update();`(非 multiMapper 端口)。
6. **停手才重建(已有,确认接线)**:拖动中(`viewAllOnInteracting`)只降质+重置裁剪、**不提交引擎目标**
松手(`viewAllOnInteract`)/定时器 idle 才 `viewAllSubmitTargets`(提交 LOD 目标)+ `viewAllPickOneLine`
(拉就绪区域换上)。避免 P11/P12"每帧 20 条重建上传"thrash。
7. **质量旋钮保留**`--sampleDist`/`--maxImgSample`/`--dragSampleMul` 作 LOD 框架内的交互降质兜底。
8. **总览级别**:引擎 `selectLod` 按屏幕像素选层——拉远时全路映射到少量像素 → 自动选粗层(小贴图)→
20 条小贴图即可用;拉近 → 小区域细层。**确认 selectLod 多线下确实选到粗层**(若没有,调
selectLod 的屏幕像素阈值——这是 §7.5 嫌疑①的修复点)。
**验收**:离屏看各线底图 level 随相机距离变(远→粗/小、近→细/小区);真窗口测 20 条总览交互级 fps、
拖动跟手、松手清晰、过档位无明显卡顿(外部专家提示重点盯"停手重建过渡手感")。
---
# GPR 三维体渲染:多后端 + 空体素跳过(ESS) 加速架构 — Spec2026-06-26已废
> 解决"20 个重叠密体在 VTK 库存 OpenGL mapper 上又卡又只能降质"的根本性方案。
> 关联:`docs/superpowers/specs/2026-06-25-gpr-line-channel-interpolation-and-multivolume.md`(数据/插值口径)。
---
## 0. 一句话目标
用**空体素跳过(ESS)** 这一不损可见质量的业界技术,把"逐线分开的多个密体合并渲染"做到**不卡**
并以**多渲染后端 + 自动适配**覆盖客户侧未知/多厂商硬件(含无显卡)。
---
## 1. 问题与根因
- 现状渲染 = VTK `vtkGPUVolumeRayCastMapper`OpenGL。**任意 GPU 可跑(最通用),但无 ESS**。
- 20 个重叠半透明密体:每条光线每步在 20 体各采一次、光线又长 → 几十亿次纹理查找/帧 → 1.7fps、交互更卡。
- 通道插值后 Y 加密到 2.5cm,使自动沿光线步长更细 → 更卡。
- **已尝试的"交互降质"(屏幕降采样 + 沿光线步长加粗)是治标**,损可见质量。
## 2. 根本方案ESS不损质量
GPR 体 ~90% 是近零背景反射层之间空。ESS 用 min/max 加速结构**跳过"在传函里全透明"的块**
对稀疏数据常 550× 提速、**零质量损失**。但 **VTK 库存 GPU mapper 不做自动 ESS**(仅有受限的
`UseDepthPass` 等高线跳过)。→ 真 ESS 必须**换专业体渲染后端**(其底层自带 ESS + 正确合成多重叠体)。
## 3. 关键事实:跨厂商 GPU 加速不存在单一方案
GPU 体光追渲染器全是**厂商锁定**
| 后端 | 硬件 | ESS | 跨厂商 | 角色 |
|---|---|---|---|---|
| OpenGLvtkGPUVolumeRayCastMapper现状| 任意 GPU(N/A/Intel) | ❌ | ✅ | **终极兜底** |
| OSPRayCPUEmbree/ISPC| 任意 x86 CPU免显卡| ✅ | ✅ | **通用基线** |
| OSPRay-GPUSYCL/oneAPI| 仅 Intel Arc/数据中心卡 | ✅ | ❌ | Intel 独显 |
| ANARI + VisRTXOptiX| 仅 NVIDIA | ✅ | ❌ | N 卡 |
| AMD GPU 体渲染+ESS | — | — | ❌ 无成熟方案 | — |
**结论**:没有"同时 N/A 卡的 GPU-ESS"。**面向未知客户机OSPRay-CPU 是最稳通用选择**免显卡、ESS、质量不降
## 4. 渲染后端架构(多后端 + 自动适配 + 手动覆盖 + 兼容灰掉)
### 4.1 用户可见选项(按硬件/结果命名,不暴露库名)
| 用户看到 | 背后实现 | 适配硬件 |
|---|---|---|
| **自动(推荐)** | 探测后选下面之一 | — |
| GPU 加速N卡| ANARI + VisRTX | NVIDIA 独显 |
| GPU 加速Intel| OSPRay-GPU | Intel Arc 独显 |
| CPU通用免显卡| OSPRay-CPU | 任意 CPU |
| 通用 GPU兼容| OpenGL现状 mapper| 任意 GPU兜底|
### 4.2 自动探测逻辑
```
if NVIDIA 独显: → VisRTX(GPU)
elif Intel Arc 独显: → OSPRay-GPU
elif AMD(独显/核显) 或 Intel 核显 或 无显卡 或 探测失败:
→ OSPRay-CPU默认 // 核显一律走 CPU弱+共享内存+多不被GPU后端支持
// 强力 A 卡可手动选"通用 OpenGL"用其 GPU(无 ESS),但不作默认
```
- **手动覆盖**:用户可自选,但**只列出与当前硬件兼容的项**(不兼容灰掉,如 A 卡上禁 VisRTX
- **集显建议**:一律 OSPRay-CPUCPU+ESS 比让弱核显硬渲更稳更快)。
### 4.3 部署策略(一句话)
**OSPRay-CPU 保底通用;探测到 N卡/Intel Arc 时升对应 GPU 后端A 卡/核显吃 CPU 基线OpenGL 终极兜底。**
## 5. 上 ESS 后端后,废弃哪些
- ❌ **装箱单体(binning)**:当初为绕 OpenGL 无 ESS 才提(代价=丢"逐线分开"。ESS 后端让**逐线分开
的多体也快** → 不需要装箱。**逐线分开 + 不造假 + 不卡,三者兼得。**
- ❌ **交互降质权宜**(屏幕/沿光线步长加粗ESS 后端有自带的自适应/渐进式细化,基本不用;保留作任何后端的兜底。
- ❌ 自写 ESS shader / 预积分 / UseDepthPass后端自带无需自行实现。
- ✅ **保留**"选几条 ds(≤7)" 是使用方式(非技术),永远有效。
## 6. 实现要点(工程)
- VTK 需**重编**带:`RenderingRayTracing`(OSPRay)、`RenderingAnari`(ANARI/VisRTX) 模块 + 依赖
OSPRay/Embree/ISPC/OpenVKLANARI-SDK/VisRTX。用户已确认工程量无所谓。
- 渲染层抽象一个 `IVolumeRenderBackend`,运行时按 §4 选具体 mapper
`vtkGPUVolumeRayCastMapper`(OpenGL) / `vtkOSPRayPass`+volume / `vtkAnariPass`+volume。
- **数据不变**逐线密体含通道插值spec 前一份)原样喂各后端;多体合成由后端负责(无 K=7 分包)。
- 硬件探测GL_VENDOR / 平台 APIDXGI 枚举适配器)判 N/A/Intel + 独显/核显。
## 7. POC 计划(先验"CPU+ESS 够不够快"——最通用、风险最低)
1. **POC-1先做**:最小程序,用 **OSPRay-CPU** 渲一个 GPR 密体tmp/lines_all_dense 里一条/几条),
**实测普通 CPU 上对多体/密体的 fps + ESS 提速比**,对照现状 OpenGL。先确认 OSPRay 在本环境
可编可跑、CPU+ESS 实际够快——这是整套方案值不值得上的关键闸门。
2. **POC-2**:若有 N 卡ANARI+VisRTX 渲同一体,对照 GPU 提速。
3. POC 通过 → 才动手重编 VTK + 接后端抽象层。
## 8. 风险 / 待定
- OSPRay-GPUIntel较新、不如 CPU 路成熟ANARI/VisRTX 需 NVIDIA 驱动 + VisRTX 库。
- 各后端传函/外观与 OpenGL 有差异,需重新调一致。
- 本环境能否编出带光追/ANARI 的 VTKvcpkg/手动依赖)待 POC-1 验证。
- CPU+ESS 在低核机上的实际帧率待实测。
## 9. 验收
1. 客户机无论有无显卡/何种显卡自动选到可跑的后端并出图OSPRay-CPU 永远兜底)。
2. 20 条密体总览:逐线分开、不造假、**ESS 后端下不卡**(目标交互 ≥ 可用帧率,质量不降)。
3. 手动选项只列兼容项;集显默认 CPU。
4. 数据层(通道插值密体)零改动复用。

View File

@ -1,144 +0,0 @@
# 反演剖面三维体客户目标方法Surfer对照与客户端差距 — 2026-06-27
> 来源:客户提供的演示视频 `ScreenShot/9f67e80cb823170bb9374d779ec4c0cb.mp4`Golden Software Surfer13min
> 与参考成果图 `ScreenShot/Weixin Image_20260627080012_429_2117.png`
> 结论一句话:**客户用的就是"合并点云 + 3D 反距离加权(IDW) + 边界裁剪(Blanking) + 体绘制/等值面"。**
> 客户端插值内核已与之一致,差距只在「搜索半径 / 边界裁剪 / 等值面」三点。
---
## 0. 背景与一次认知纠偏
用户反馈:"选多个 dd_inversion_data江西理工四条井字相交测线做三维体得到的是四个剖面各自左右
拉伸的薄板,不连成完整体。"
排查中曾推荐"逐深度层各向异性插值"——看了客户演示视频后**确认是过度设计**。客户没有逐层做,
就是**朴素的三维 IDW**(对合并后的整团散点云一次性 3D 网格化)。本文以视频实证为准。
---
## 1. 客户目标方法Surfer 实测,逐帧为证)
| 步骤 | 操作 | 视频帧(时间戳)证据 |
|---|---|---|
| 1. 合并点云 | 所有测线/剖面反演单元合并成**一个 XYZC 文件** `combine-e_m_m.xyz`,列= **X, Y, Elevation, Resistivity, Conductivity, Sensitivity** | t48.7Grid Data 导入对话框Data Type=**XYZC** |
| 2. 三维网格化 | Grid Data → 方法 **Inverse Distance to a PowerIDW幂次** → XYZC 直接生成 **3D 网格体**;注脚明示"does not extrapolate beyond the range of data" | t48.7Gridding Method 选中 IDWt97.5Gridding 进度t195`.grd has been created` |
| 3. 边界裁剪 | 数字化**测区边界多边形**Base vector / Polyline**Blanking** 把体裁到测区真实足迹 | t292.5Polygon 工具数字化边界 |
| 4. 三维渲染 | 3D View → 3D Grid Volume`out-BAIHUA.vtk`**Volume render**(Sliced / Slice count 500 / Tri-linear / Alpha blending / Opacity 80%) + **Isosurface**(阈值 isovalue) + **Image slice**(YZ/Z) | t585Volume render 属性t682Isosurface(isovalue=1794.39)t633Image slice(YZ) |
要点:
- **真三维 IDW**,对合并点云一次成体(非逐层 2D
- IDW **不外推到数据范围外**;测区足迹靠 **Blanking 多边形**裁出(参考图那个不规则边界即来源于此)。
- 红色异常体 = **等值面抽取**Isosurface 按阈值)。
- 坐标为真实投影坐标 + 高程,可叠地形/影像底图。
---
## 2. 客户端现状(已实现部分)
生产路径:`Api3dRepository::createVolume``src/data/api/Api3dRepository.cpp`
→ 把所有选中 ds 的反演单元按测线真实几何配准合并成 `PointSet`
`buildVolume(pts, cellXY, cellZ, power, maxDist)``src/core/algo/VolumeBuilder.cpp`
**三维 IDW**`src/core/algo/IdwInterpolator.cpp``maxDist` 外置 NaN 留空。
即:**「合并点云 + 3D IDW」内核与 Surfer 一致**。参数见 `src/data/repo/VolumeBuildParams.hpp`
`cellXY=1.0, cellZ=0.5, power=2.0, maxDist=4.0`
渲染:`VoxelActor``src/render/actors/VoxelActor.hpp`)仅 GPU 体绘制NaN→透明**无等值面**
`GridContourActor`/`ContourBands` 是 2D 网格等值线,非 3D 等值面)。地形/影像/坐标轴/电极点已有
`TerrainActor`/`TileBasemap`/`AxesActor`/`ElectrodeActor`)。
---
## 3. 差距与修复(共 3 点)
> 不需改插值算法(内核已对);改的是搜索域、裁剪、与等值面。
### G1. 搜索半径 maxDist 太小 → "四块板"
`maxDist=4m` 远小于井字测线间距 → IDW 只填测线 ±4m 管套,线间留空 → 四块薄板。
**修复**:把搜索半径放大到覆盖测区(或提供"覆盖全域"选项),对齐 Surfer 默认搜索域行为。
### G2. 缺边界裁剪(Blanking) → 单纯放大半径只会"变粗"
**这是用户观察到"调大 maxDist 只是让体看起来更粗"的真正原因**:没有足迹裁剪,放大半径会把体
鼓满整个外接盒 → 粗大臃肿。Surfer 不粗,是因为 Blanking 把体裁到了测区真实多边形足迹。
**修复**:加**足迹掩膜**——
- 自动:散点平面**凸包 / alpha-shape**(或沿测线 buffer 并集);
- 或手动:支持用户数字化/导入边界多边形(对齐 Surfer Blanking
掩膜外体素整列置空NaN/透明)。
### G3. 缺 3D 等值面 → 出不来红色异常体
`VoxelActor` 只有体绘制。
**修复**:在体上加 **`vtkFlyingEdges3D` / `vtkContourFilter`** 抽等值面,阈值可调(对齐 Surfer Isosurface
---
## 4. 修复落地顺序
1. **G1+G2 一起做**(插值搜索域放大 + 足迹掩膜)→ 出满铺、裁到测区足迹的体。这是核心,先做。
2. **G3 等值面**(阈值可调)→ 出红色异常体。参考图第二主角,紧接着做。
3. 影像底图/坐标轴/电极点复用现有。
---
## 5. 必须先和客户对齐的预期(避免"又看起来不对"
参考图是**密集测网**(顶面可见很多条测线点阵);江西项目只有**四条井字线**。
- **形态可复刻**(满铺体 + 等值面 + 影像底图);
- 但**框内细节出不来**——参考图的细碎红异常源于密集采样,四条线之间只能给出平滑趋势,
等值面会是几个光滑大团。要那种精度需加密测线。
---
## 6. 非目标 / 说明
- 不改 IDW 内核算法本身(已与 Surfer 一致),不引入逐层各向异性(客户未用)。
- 各向异性搜索椭球可作为后续可选增强Surfer 亦为可选项,非默认),本期不做。
- Kriging 仍为占位(`VolumeBuildParams::Model::Kriging`core 未实现),本期不依赖。
---
## 7. 续:白化方式之争 + 体绘制边界「梯田」2026-06-28branch `fix/3d-volume-blanking-mask`
> 本节记录 §16 之后这一轮的来龙去脉、技术取舍与权威佐证,供后续决策不再反复。
### 7.1 前因后果(时间线)
客户原话:"**我们这个白化确实有问题,填色了,填了蓝色**"——无数据区被填成蓝色,而非 Surfer 那种透明白化。排查分三层、逐个修:
1. **切片填蓝**`vtkImagePlaneWidget` 会按【输入标量范围】(含哨兵)自动 window/level把哨兵顶到 LUT 最低色格(蓝)且不透明。
修复:`SliceTool` 钉死 `SetWindowLevel([vmin,vmax])` + `ColorLutBuilder` 预留 0 号"白化槽"(全透明),哨兵(<vmin)钳到该槽即透明实测证据`tests/spike/slice_alpha_probe.cpp` widget 离屏渲染 + 回读像素背板透出=透明成功)。**这条同时纠正了切片颜色映射**(之前 colorbar 被错误拉伸到切片局部范围)。
2. **早期"满屏蓝"是误判**:实为 `maxDist=0`(自动覆盖测区,`kDefMaxDist=0.0`)把整个凸包**填实**=真实低值数据(蓝)**不是空值**(见 §3 G1。日志实证请求体 `"maxDist":0`
3. **三维体渗蓝**:无数据格设哨兵 `vmin-1`、不透明度 0三线性插值在"哨兵↔真值"交界处插出低值(蓝)且不透明度非零 → 渗一圈蓝。为消除,给体绘制加**二值 mask**`VoxelActor::makeMaskLike` + `assembleVolume``vtkGPUVolumeRayCastMapper`+`SetMaskInput`+`SetMaskTypeToBinary`mask=0 体素被光线投射**硬跳过**)。
**mask 的副作用 = 用户 2026-06-28 截图的「竖条/梳齿/底边锯齿」**:硬跳过=边界不再三线性插值/羽化。`maxDist=0` 下体填满**凸包足迹**(带斜边多边形),斜边在 1m 规则网格上离散成**体素阶梯(staircase)**。以前(SmartVolumeMapper+哨兵→不透明度0 软消隐)斜边被羽化抹圆、看不出;加 mask 后变成逐体素硬台阶。**即使色阶不透明度=100% 也可见**——因 mask 在不透明度传函【之前】就把体素从光线上删了(两个并行子代理:渲染侧 + 数据/建体侧共同确诊)。
### 7.2 取舍的本质
| 方案 | 边界观感 | 数据诚实度 | 误判风险 |
|---|---|---|---|
| **二值 mask当前** | 体素梯田(难看) | ✅ 只画真数据、零假值 | 低(梯田明显是网格对齐的足迹边界,不像地质体) |
| 软消隐哨兵→不透明度0无 mask | 平滑 | ❌ 边界是插值出的假值 | **高** |
### 7.3 「细蓝边」是什么 + 为何【不能】回退软消隐(权威佐证)
软消隐回退后的"细蓝边" = 真数据格与哨兵格(`vmin-1`)三线性插值出的、**数据里根本不存在的假低阻值**。低阻在物探通常指含水/导电体,这圈蓝出现在测区最外缘会被**误读成"边界存在真实低阻(导电)异常带"**——真有数据分析歧义。
多个权威来源一致认定"插值进 NoData 产生边缘伪影"是错误做法、应 mask 排除:
- **ESRI 官方**:双线性/三次插值 "**interpolate incorrectly into the NoData** and background areas... producing **artifacts or black ridges**" —— 我们的"蓝脊"与之同源(哨兵钳到最低色=蓝)。<https://support.esri.com/en-us/knowledge-base/faq-why-do-bilinear-interpolation-and-cubic-convolution-000003271>
- **GDAL 官方**:正确做法是把 masked NoData 的 "**weights of contributing source pixels are set to zero to ignore them**" / "will not be used in interpolation"。<https://gdal.org/en/stable/programs/gdalwarp.html>
- **rasterio**bilinear 重采样在 NoData 边界产生 invalid 值(已知 issue #1721)。<https://github.com/rasterio/rasterio/issues/1721>
- **Golden Software Surfer**客户参照工具NoData "**removed from the neighborhood**",不跨它插值。<https://surferhelp.goldensoftware.com/gridmisc/Blanked_Nodes_Grid_Filter.htm>(定义 <https://surferhelp.goldensoftware.com/glossary/def_blanking.htm>
- **凸包外 = 外推**"**Extrapolated data is usually meaningless and misleading.**" <https://github.com/fatiando/fatiando/pull/44><https://www.spatialanalysisonline.com/HTML/gridding_and_interpolation_met.htm>
**结论:二值 mask = 业界标准的"把 NoData 排除出插值"做法,是对的**;梯田只是 mask 在斜足迹边界上的网格离散观感(诚实、不误导)。**不应回退软消隐**=让哨兵参与插值=以上权威明确反对的造假值做法)。
### 7.4 决策与待办(截至 2026-06-28本分支未提交
- ✅ **保留二值 mask**(数据诚实/合规,符合 ESRI/GDAL/Surfer 标准)。
- 梯田若要压平,走**不造假值**的路(二选一,**待用户/客户拍板**
- (a) **细化 XY 网格**`cellXY` 1m→0.5m阶梯缩到亚像素代价体素×4、耗时 ~3.5s→~14s、内存×4。
- (b) **接受梯田**:它诚实、且明显是足迹边界,不会被当成地质体。
- 渲染侧本轮其它已落地修复(排查"分层/稠密"时做、确认非主因但保留GPU 探测+CPU 回退(`setVolumeGpuSupported`)、细采样距离+`UseJittering`、`ScalarOpacityUnitDistance=对角/10`、去 `kMaxOpacity`(改由色阶「不透明度」单一控制、100%=实心)、移除工具条「透」滑块。
- **附带缺陷(待修)**`VoxelGenerateRequest::maxDist` 结构体默认 `4.0``src/data/dto/Vtk3dRequests.hpp:18`)与对话框 `kDefMaxDist=0.0``VolumeParamsDialog.cpp:34`)不一致——绕过对话框直建会拿到 4m → 退回"四块板/线间空隙"老问题,应统一为 0。
- 抽稀空间哈希 `(ix*p1)^(iy*p2)^(iz*p3)``VolumeBuilder.cpp` ~146-151为 XOR 非单射、有碰撞风险(与本症无关,但宜换 `(iz*ny+iy)*nx+ix` 线性键)。

View File

@ -1,231 +0,0 @@
# 数据集详情视图:架构详解 + 「新增一种 ds 类型详情页」扩展指南 — 2026-06-28
> 读者:将为**一种新数据集类型**开发详情页的同事。读完本文你应能**独立按现有架构扩展,无需口头交接**。
> 所有引用均为 `文件:行号`(可点击)。代码以 2026-06-28 `fix/3d-volume-blanking-mask` 分支为准。
---
## 0. 一句话架构
**详情页 = 数据驱动的"策略 + 页签引擎"**:每种 ds 类型(由 **`ddCode`** 标识)有一个**策略**声明它有哪些页签(每页签 = 一种视图 + 一个加载键);控制器按 `ddCode` 找策略 → 建页签 → 按加载键**异步**拉数据 → 解析成**类型擦除的 payload(`QVariant`)** → **工厂**按视图种类造视图 → 视图 `setPayload` 自解包渲染。**新增类型 = 加一个策略 + 一条加载键分发 + (必要时)一个视图****不动**列表/控制器/壳层。
---
## 1. 端到端数据流(双击一个数据集会发生什么)
```
[列表 QTreeWidget] 双击 item
main.cpp:1501 itemDoubleClicked → 读 item 的 data role
kDsIdRole / kDsDdCodeRole / kDsNameRole / kDsTmObjectIdRole
main.cpp:1509 detailCtrl.openDataset(dsId, ddCode, dsName, tmObjectId)
[编排层 DatasetDetailController]
DatasetDetailController.cpp:21 openDataset()
registry_.find(ddCode) → 找策略(找不到 → emit loadFailed "暂不支持该数据类型的预览",优雅降级)
strategy->tabs() → std::vector<TabSpec>
emit datasetOpened(dsId, ddCode, dsName, tmObjectId, tabs) ──┐ 建页
对每个【非 lazy】页签 loadTab(dsId, ddCode, i) │
│ │
▼ DatasetDetailController.cpp:46 loadTabImpl() │
repo_.loadAsync(spec.loaderKey, dsId, pageNo, pageSize) │ 按【loaderKey】异步加载
→ DetailLoad*(在飞句柄,存 inflight_[tabIndex]abort-and-replace
emit tabLoadStarted(dsId, i) → 页上盖「加载中」遮罩 │
│ DetailLoad::done(QVariant payload) │
▼ │
emit tabReady(dsId, i, payload) │
│ │
▼ ▼
[壳层 DatasetDetailPanel(QTabWidget每个 ds 一页) / DatasetDetailPage(单 ds多页签)]
Panel.onDatasetOpened (DatasetDetailPanel.cpp:38) ← datasetOpened
new DatasetDetailPage → 注入(repo/viewState/tmObjectId) → page.build(tabs)
Page.build (DatasetDetailPage.cpp:33)
对每个 TabSpecmakeDetailView(spec.kind, ...) 造视图 → 组装页签
Panel.onTabReady (DatasetDetailPanel.cpp:61) ← tabReady
page.setTabPayload(i, payload) (DatasetDetailPage.cpp:92)
→ views_[i]->setPayload(payload) ← 视图自解包 payload 渲染
```
懒加载 / 分页是**反向**信号(页 → 面板 → 控制器):
- **lazy 页签**首次点开 → `Page::tabNeeded``Panel::tabNeeded``detailCtrl.loadTab`main.cpp:1522
- **paginated 页签**翻页 → `DataTableView::pageRequested``Page::tabPageNeeded``Panel::tabPageNeeded``detailCtrl.loadTabPaged`main.cpp:1525
---
## 2. 五个核心抽象(必须先理解)
| 抽象 | 定义处 | 作用 |
|---|---|---|
| **`ViewKind`**(枚举) | `src/controller/DatasetDetailTab.hpp:10` | 视图渲染种类全集:`Scatter / FilledContour / Bar / LineProfile / PolylineMap / Table / WebMap` |
| **`TabSpec`** | `src/controller/DatasetDetailTab.hpp:13` | 页签描述符 `{QString title; ViewKind kind; QString loaderKey; bool lazy; bool paginated;}` |
| **`IDatasetChartStrategy`** + **`ChartStrategyRegistry`** | `src/controller/IDatasetChartStrategy.hpp` | 策略:`ddCode()` + `tabs()`;注册表按 `ddCode` 索引策略 |
| **`IDetailView`** | `src/app/panels/chart/IDetailView.hpp` | 视图统一接口:`QWidget* widget()` + `void setPayload(const QVariant&)` |
| **Payload 结构体**6 种)+ **`DetailLoad`/`ApiDetailLoad`** | `src/core/model/detail/DetailPayloads.hpp`、`src/data/api/DatasetLoadHandles.hpp` | 纯数据载荷(`QVariant` 类型擦除)+ 异步加载句柄 |
**关键契约**
- **`ddCode`** = 分发主键(后端给每个 ds 带的类型码,如 `dd_inversion_data`)。列表把它存进 item 的 `kDsDdCodeRole`,双击时透传给 `openDataset`
- **`loaderKey`** = 控制器与仓储之间的字符串契约(如 `"inversion.scatter"`)。控制器只认 `TabSpec.loaderKey`,仓储 `loadAsync` 按它分发到具体加载函数。
- **payload** 必须 `Q_DECLARE_METATYPE` + 用 `QVariant::fromValue(...)` 装、`qvariant_cast<T>(...)`(或 `.value<T>()`)解。
---
## 3. 现有 5 种 ds 类型对照表(照抄即模板)
| ddCode | 策略(`src/app/panels/chart/*Strategy.hpp` | 页签title / ViewKind / loaderKey / lazy / paginated | payload | 视图 |
|---|---|---|---|---|
| `dd_inversion_data` | `ErtInversionStrategy.hpp` | 原数据 / Scatter / `inversion.scatter` / 否 / 否;<br>网格数据 / FilledContour / `inversion.grid` / **lazy** / 否 | `ScatterPayload` / `ContourPayload` | RawDataChartView / GridDataChartView |
| `dd_ert_measurement_data` | `MeasurementStrategy.hpp` | 散点 / Scatter / `ert_measurement.scatter`;列表 / Table / `ert_measurement.rows` | `ScatterPayload` / `TablePayload` | RawDataChartView / DataTableView |
| `dd_ert_measurement_gr_data` | `GrMeasurementStrategy.hpp` | 柱状 / Bar / `gr.bar`;列表 / Table / `gr.rows` | `BarPayload` / `TablePayload` | BarChartView / DataTableView |
| `dd_trajectory_data` | `TrajectoryStrategy.hpp` | 列表 / Table / `traj.rows`;高程 / LineProfile / `traj.elev`;地图 / WebMap / `traj.map` | `TablePayload` / `LinePayload` / `MapPayload` | DataTableView / LineChartView / TrajectoryMapView |
| `dd_grid` | `GridStrategy.hpp` | 列表 / Table / `grid.rows` / 否 / **paginated** | `TablePayload`(分页) | DataTableView |
注册处(**唯一**集中点):`src/app/main.cpp:2378-2383`
```cpp
geopro::controller::ChartStrategyRegistry chartRegistry;
chartRegistry.add(std::make_unique<geopro::app::ErtInversionStrategy>());
chartRegistry.add(std::make_unique<geopro::app::MeasurementStrategy>());
// ... 共 5 个
geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry); // main.cpp:2384
```
---
## 4. 分层职责与关键文件(按数据流顺序)
1. **列表层** —— `QTreeWidget``DatasetListPanel::populateDatasetList``src/app/panels/DatasetListPanel.cpp:33`)。**通用、与类型无关**:从后端 ds 列表(`std::vector<data::DsRow>`)填树,每行把 `ddCode` 等写进 data role。role 常量定义在 `src/app/panels/DatasetListPanel.hpp:16-26`
`kDsIdRole=0x0100` / `kDsDdCodeRole=0x0104` / `kDsNameRole=0x0105` / `kDsTmObjectIdRole=0x0108`
**两个详情入口**(都按 `kDsDdCodeRole` 路由):
- 左下数据列表双击 → `main.cpp:1501-1510` 直接 `detailCtrl.openDataset(dsId, ddCode, dsName, tmObjectId)`
- 数据列分类区双击 → `CategorySection::detailRequested(dsId, ddCode, name)``src/app/panels/columns/CategorySection.cpp:124`)→ `CategoryAnalysisTab``columns/CategoryAnalysisTab.cpp:50`)→ `buildWorkbench``openDataset``main.cpp:1967`)。⚠️ 此入口只带 dsId/ddCode/name、**不带 tmObjectId**(白化模板列表可能为空——新类型若依赖 tmObjectId 需留意)。
**新增类型通常【不用动列表】**:只要后端把新 ds 带它的 `ddCode` 返回,它就会出现在树里、双击即按 `ddCode` 路由到你的策略。
2. **编排层** —— `DatasetDetailController``src/controller/DatasetDetailController.cpp``openDataset:21` / `loadTab:37` / `loadTabPaged:41` / `loadTabImpl:46`。只依赖 `IAsyncDatasetRepository` + `ChartStrategyRegistry`**不认识任何具体类型**。
3. **策略层** —— `src/app/panels/chart/*Strategy.hpp`(每个 ~15 行)。**新增类型主要在这里加一个文件**。
4. **数据层** —— `src/data/api/ApiDatasetRepository.cpp`
- 分发 `loadAsync:160``if (loaderKey == "...") return makeXxx(dsId);`,未知 key 抛 `runtime_error`→ 控制器兜成 `loadFailed`)。
- 加载函数 `makeXxx:175-247``new ApiDetailLoad(xxxBatch(api_, dsId), [](r){ return QVariant::fromValue(payload); })`。
- 批次 `xxxBatch:52-154`(匿名命名空间):`new net::ApiBatch({api.getAsync(端点), api.postJsonAsync(端点, body)...}, &isFailure)`——**唯一端点定义处**。
- 解析器 `parseXxxParts`(本文件匿名 ns`dto::parseXxx``src/data/dto/*Dto.cpp``QList<ApiResponse>` → payload。
5. **视图层** —— 工厂 `src/app/panels/chart/DetailViewFactory.cpp:15``ViewKind` → `IDetailView`,并注入 cmdRepo/colorTplRepo/viewState/getter各视图 `RawDataChartView/GridDataChartView/DataTableView/BarChartView/LineChartView/TrajectoryMapView`
6. **壳层** —— `DatasetDetailPage`(单 ds 多页签:`build:33`、`setTabPayload:92`、lazy 遮罩、分页冒泡);`DatasetDetailPanel`QTabWidget每 ds 一页,路由控制器信号:`onDatasetOpened:38` / `onTabReady:61`)。
7. **接线** —— `main.cpp`:面板创建+注入 `1390-1395`;控制器↔面板信号 `1513-1531`;策略注册+控制器 `2378-2384`
---
## 5. 扩展配方:新增一种 ds 类型的详情页
### 前置:先判断走 5A 还是 5B
- 你的页签能否**复用现有 `ViewKind`**(散点 / 等值面 / 表格 / 柱状 / 折线 / 地图)?
- **能** → 走 **5A**(最常见,多数"又一种表格/散点"属于此)。
- **不能**(要全新的图) → 走 **5B**5A + 新视图)。
### 5A. 复用现有视图4 步,全在已有文件里加)
**① 写策略** —— 新建 `src/app/panels/chart/FooStrategy.hpp`(照抄 `GridStrategy.hpp`
```cpp
#pragma once
#include <vector>
#include "IDatasetChartStrategy.hpp"
namespace geopro::app {
struct FooStrategy : controller::IDatasetChartStrategy {
std::string ddCode() const override { return "dd_foo"; } // ← 后端给的类型码
std::vector<controller::TabSpec> tabs() const override {
return {
{QStringLiteral("列表"), controller::ViewKind::Table,
QStringLiteral("foo.rows"), /*lazy*/ false, /*paginated*/ false},
// 慢的页签设 lazy=true服务端分页设 paginated=true视图须是 DataTableView
};
}
};
} // namespace geopro::app
```
**② 注册策略** —— `src/app/main.cpp:2383` 后加一行:
```cpp
chartRegistry.add(std::make_unique<geopro::app::FooStrategy>());
```
并在 main.cpp 顶部 include `panels/chart/FooStrategy.hpp`
**③ 加 loaderKey 分发 + 加载函数** —— `src/data/api/ApiDatasetRepository.cpp`
- 分发表 `loadAsync`:171 后):`if (loaderKey == "foo.rows") return makeFooRows(dsId);`
- 批次(匿名 ns:154 附近):
```cpp
net::ApiBatch* fooRowsBatch(net::ApiClient& api, const std::string& dsId) {
QList<net::IApiCall*> calls{
api.getAsync(QStringLiteral("/business/dd/foo/rows?dsObjectId=%1").arg(enc(dsId))),
};
return new net::ApiBatch(calls, &isFailure);
}
```
- 加载函数(:247 附近):
```cpp
DetailLoad* ApiDatasetRepository::makeFooRows(const std::string& dsId) {
return new ApiDetailLoad(fooRowsBatch(api_, dsId), [](const QList<net::ApiResponse>& r) {
return QVariant::fromValue(dto::parseFooTable(r[0].data)); // → 复用的 payload如 TablePayload
});
}
```
- 在 `src/data/api/ApiDatasetRepository.hpp` 声明 `makeFooRows`
**④ 写解析器** —— `src/data/dto/FooDto.{hpp,cpp}`(照抄 `MeasurementDto`/`GridDto``parseFooTable(const QJsonObject&) → core::TablePayload`。新增 .cpp 记得加进 `src/data/CMakeLists.txt`(或对应子目录的 CMakeLists
> 复用视图时**视图层、工厂、壳层、控制器、列表全不动**。
### 5B. 需要全新视图(在 5A 之上多 5 步)
**① 加 ViewKind** —— `src/controller/DatasetDetailTab.hpp:10` 枚举追加 `Foo`
**② 加 payload** —— `src/core/model/detail/DetailPayloads.hpp`:定义 `struct FooPayload {...};`,文件末尾加 `Q_DECLARE_METATYPE(geopro::core::FooPayload)`
**③ 写视图** —— `src/app/panels/chart/FooChartView.{hpp,cpp}`,实现 `IDetailView`
```cpp
class FooChartView : public QWidget, public IDetailView {
public:
QWidget* widget() override { return this; }
void setPayload(const QVariant& v) override {
const auto p = v.value<geopro::core::FooPayload>(); // 解包
/* 用 p 渲染QwtPlot / ECharts-via-QWebEngine / 自绘) */
}
};
```
**④ 加工厂分支** —— `src/app/panels/chart/DetailViewFactory.cpp:22``switch` 里加 `case controller::ViewKind::Foo: return std::unique_ptr<IDetailView>(new FooChartView(parent));`(需注入仓储就照 Scatter/Table 分支注入)。
**⑤ 注册 .cpp** —— `src/app/CMakeLists.txt``FooChartView.cpp`
然后照 **5A 的 ①②③④** 接策略/loaderKey/加载/解析。
---
## 6. 关键约定与坑(务必知道)
- **优雅降级**`ddCode` 未注册策略 → 控制器 `emit loadFailed "暂不支持该数据类型的预览"``DatasetDetailController.cpp:26`)。所以"详情打不开"先查**策略是否注册**。
- **未知 loaderKey** → 仓储 `loadAsync``std::runtime_error`,控制器 `loadTabImpl` 就地兜成 `loadFailed``:59-63`**不会**让遮罩永久悬挂。策略 loaderKey 必须与仓储分发表**字符串完全一致**。
- **payload 类型擦除**:忘了 `Q_DECLARE_METATYPE``QVariant::fromValue`/`.value<T>()` 不匹配 → 视图拿到空 payload、静默不渲染。
- **lazy**:数据慢的页签设 `lazy=true`——开页**不**加载,首次点开才发 `tabNeeded``DatasetDetailPage.cpp:80`),其间盖 `LoadingOverlay`
- **paginated**:服务端分页**只支持 `DataTableView`**`Page::build:61` 用 `qobject_cast<DataTableView*>``pageRequested``makeXxxRows` 要透传 `pageNo/pageSize`(见 `makeGridRows:238`)。
- **注入链**:视图所需仓储/getter`cmdRepo`/`colorTplRepo`/`projectIdGetter`/`tmObjectId`/`viewState`)由工厂在造视图时注入(`DetailViewFactory.cpp:15-65`),而面板/页**必须在 `build()` 之前**完成注入(`DatasetDetailPanel.cpp:45-48`)。新视图若要新仓储,沿 `makeDetailView` 形参 → `Page::build``Panel::onDatasetOpened``main.cpp` 注入这条链透传。
- **跨视图色阶联动(可选)**:若新视图涉及色阶且要和 2D/3D 联动,注入 `DatasetViewState* viewState`(单一真源,按 dsId不涉及就忽略传 nullptr 无副作用。详见 `src/controller/DatasetViewState.hpp`
- **并发正确性**:同一页签重复加载用 **abort-and-replace** + **句柄身份比对丢弃迟到信号**`DatasetDetailController.cpp:66,72`);退出时 abort 全部在飞句柄(`~DatasetDetailController:17`)。你写 `makeXxx` 不用操心,沿用 `ApiDetailLoad` 即可。
---
## 7. 你需要触碰的文件清单(速查)
| 场景 | 必改文件 |
|---|---|
| **5A 复用视图** | ① `FooStrategy.hpp`(新) ② `main.cpp`(注册+include) ③ `ApiDatasetRepository.{cpp,hpp}`(分发+批次+make) ④ `FooDto.{hpp,cpp}`(新解析) + 对应 `CMakeLists.txt` |
| **5B 全新视图** | 5A 全部 + `DatasetDetailTab.hpp`(ViewKind) + `DetailPayloads.hpp`(payload) + `FooChartView.{hpp,cpp}`(新) + `DetailViewFactory.cpp`(分支) + `src/app/CMakeLists.txt` |
| **永远不用动** | `DatasetDetailController`、`DatasetDetailPanel/Page`、列表 `QTreeWidget`、`IDetailView`、`DetailLoad` |
---
## 8. 自测建议
- **策略层单测**`FooStrategy{}.ddCode()` / `.tabs()` 返回符合预期(纯函数,易测)。
- **分发单测**`ApiDatasetRepository::loadAsync("foo.rows", ...)` 不抛、返回非空 `DetailLoad`
- **解析器单测**:喂一份真实后端 JSON 给 `dto::parseFooTable`,断言 payload 字段(参考现有 `tests/` 下 DTO 测试)。
- **联调**:构建 `build.bat app`,登录后在数据集树双击该类型 ds看页签/数据/遮罩是否正常;打不开先看桌面日志 `%LOCALAPPDATA%/Geomative/Geopro3/logs/geopro_*.log``[detail] openDataset ...``[detail] 未注册策略/loadAsync 失败`
---
_本文覆盖详情视图全链路列表视图为通用容器按后端 ds 列表 + ddCode 驱动),新增类型一般无需改它。如新类型在树中的归类/图标有特殊需求,再看列表填充处(`kDsDdCodeRole` 等 role 的写入点。_

View File

@ -1,198 +0,0 @@
# 三维雷达 DS 接入设计:规范化格式 → 体渲染 + 切片 + 异常 — 2026-06-29
> 负责人范围:**仅三维雷达的体渲染 + 切片 + 异常标注**(不含 2D 波列图详情页、不含二维雷达)。
> 本 spec 基于精读现有架构HEAD `main`+ Explore 代理交叉验证 + 用真实 Mala 数据实测确诊。
> 所有"数据事实"均已用 `D:/Downloads/MALA南同大道_rSlicer` 6 条测线核对,**无未验证假设**。
---
## 1. 背景与锁定决策
客户新增雷达数据类型,模型层级沿用 `项目 → GS → TM(TmObject) → DS列表`。三维雷达 TM 携带
`头信息(.head)` + DS 列表 `= 雷达RTK轨迹(.cor,备份+快照) + 每频率 雷达3D-频率N(打标.index + 通道1..N data.data)`
数据体 = **K 切片(沿运动) × M 通道(sweep) × N 采样(深度)**(与 IDS/OpenGPR 手册一致)。
四项锁定决策(用户 2026-06-29 确认):
| 维度 | 决策 |
|---|---|
| 职责范围 | 仅三维体 + 切片 + 异常;不做 2D 波列图详情、不做二维雷达 |
| 数据来源 | 后端未就绪 → 先读**本地已转换好的目录**(后端就绪后切按 DS 下载) |
| 输入格式 | 客户端**规范化 `.head/.data/.cor/.index`**(新写 reader |
| 渲染规模 | 先**单线 / 下采样稠密体**打通 |
---
## 2. 已验证的数据事实★C++ reader 必须遵守,零假设)
实测工具:`tools/radar_convert/malamira.py probe`B-scan/C-scan 主序核对)。
1. **数据体主序 = position-major**:磁盘扫描顺序 = `(道0:通道0..M-1)(道1:通道0..M-1)…`,每 sweep 内 N 采样连续。
`flat.reshape(K, M, N)[道][通道][采样]`。probe 确诊position-major 的 B-scan 出现连续直达波 +
地层 + 双曲线绕射channel-major 为竖条乱码(已排除)。
2. **直接对应 geopro 体轴** `X=道(nx=K)` / `Y=通道(ny=M)` / `Z=采样(nz=N)`**无需轴置换**——与现有
`Gpr3dvVolumeBridge` 轴映射完全一致。⚠️ 但**磁盘主序(采样最快→通道→道)与体内存布局(道最快,`((k·ny+j)·nx+i)`
`ScalarVolumeI16.hpp:58`)相反**,新 reader 必须**逐体素散射重排填值**(照 bridge 的 `vol.at(to,j,s)=`
**严禁 memcpy/整块拷贝**。"无需转置"指轴语义,不是字节布局。
3. **维度推导**`M=NUMBER_OF_CH`、`N=SAMPLES`、`K=LAST_TRACE/NUMBER_OF_CH``.head` 的 `LAST_TRACE`
是**总扫描数**=K×M不是道数
4. **数据类型**int16 小端(`.rd3`/`bits=16`) / int32 小端(`.rd7`/`bits=32`)。`-32768` 是直达波饱和真实值,**非空值哨兵**。
5. **`BITS` 公式 bug**:客户文档 §3.3 `BITS = 文件大小/LAST_TRACE/NUMBER_OF_CH×8` **漏了 SAMPLES 维、量纲错**
正确:`bytes = 文件大小/(LAST_TRACE×SAMPLES)`,并与扩展名交叉校验。**须同步后端**(见 `tools/radar_convert/README.md`)。
6. **ddCode数据字典 DD0623 权威)**:三维雷达体 DS = **`dd_radar_3d`**(本次新增,形态=三维插值模型);
轨迹 DS = **`dd_trajectory_data`**(保留,复用现成 `TrajectoryStrategy`/`TrajectoryMapView`,零改动);
`dd_voxel` **仅物探反演体(电阻率/速度)、不含雷达**`dd_slice` 切片共用;`dd_radar_rtk_trajectory` 已删除。
**雷达体不复用 `dd_voxel`。**
---
## 3. 架构论点DS 优先 + 只换最内层 reader
### 3a. 数据层:只换最内层 reader下游建体链复用
现有真实雷达体链路是分层的,**只有最内层 reader 绑定厂商格式**
```
data::createRadarVolumeGrid [data/GprVolumeRepository 新]
├ io::gpr::buildLineVolumeFromNormalized ← 新写:仅此层认规范化 .head/.data
└ data::builtI16ToVolumeGrid [data/GprVolumeRepository.cpp:11] int16→float 稠密体
```
(范本:现有 `createGprVolume`[`Api3dRepository.cpp:128`]→`createGprVolumeGrid`→`buildLineVolumeFromGpr3dv`(Impulse .iprb)。
我们换最内层 reader复用 `builtI16ToVolumeGrid` 及其下游。)**产 `core::BuiltI16`(轴 X=道/Y=通道/Z=采样)即可**
这正是 `GprVolumeRepository.cpp:14` 注释写明的"数据层方案 A"。
### 3b. DS 优先:运行期路由按 `volumes_` 成员,不看 ddCode关键发现已验真代码
桌面端体渲染的运行期分流**只认 `Api3dRepository::volumes_` map 是否含此 dsId**,与 ddCode/"vol-" 前缀无关:
- `isVolumeDataset(dsId)` = `volumes_.count(dsId)``Api3dRepository.cpp:105`
- `VtkSceneController::addDatasetAsync`(:182) 按 `isVolumeDataset` 分流体素/帘面;
- `loadVolume(dsId)`(:341) 唯一查找键 = `volumes_` 的 dsId命中 `cachedGrid` 直渲(:347)。
**所以 DS 优先落地 = 把一条"真实 radar DS id"为 key 的 `StoredVolume` 写进 `volumes_`**(携 `ddCode=dd_radar_3d` + `lineDir/prefix`
`isVolumeDataset/addDatasetAsync/loadVolume` **零改动**自动按体渲染。三维分析栏 voxel 段内容来自 `volumeRows()`
`section("voxel")->setDatasets``main.cpp:602`**绕过 `splitByCategory`**),故 `dimensionOf`/`CategoryConfig` **也无需改**——
唯一要改 `volumeRows()`(:170) 把硬编码 `dd_voxel` 改成输出该 DS 的真实 ddCode。
### 完全复用、不碰的部分
`render::buildVoxel`GPU 体绘制)、勾选→`addDatasetAsync`→`loadVolume`→`addVolume` 路由、`isVolumeDataset`、
切片(`SliceTool` updown 深度 C-scan / leftright·frontback B-scan + `InteractionManager`)、
异常(`AnomalyDrawTool` 点/线/面 → `dd_anomaly` 挂体)、跨视图色阶联动、`builtI16ToVolumeGrid` 适配器、
轨迹 `dd_trajectory_data` 详情视图。
---
## 4. 插件化转换层(已落地 Mala
转换 = "按雷达型号的插件",本地 Python 工具 = 未来服务端下发插件的原型,契约不变:
```
plugin_id : RADAR_TYPE_MALAMIRA # 对齐文档 §2.1 型号列表
supports(fileset) -> bool
convert(lineDir, prefix, outDir) -> {head, data, cor} # 厂商原始 → 规范化
```
- **`RADAR_TYPE_MALAMIRA`(已完成)**`tools/radar_convert/malamira.py`。`.rad→.head`(§3.3)、
`.rd3→.data`(§3.5 原样拷贝)、`.pos→.cor`(§2.2.2)。`info/convert/probe` 三命令。6 条线全部转换 + 校验通过。
与文档偏差 5 处见 `tools/radar_convert/README.md`(含 BITS 公式修正)。
- **`RADAR_TYPE_IMPLUSE`(规划,暂不做)**:见 §7。
客户端职责(未来):设备对接/原始导入 → 按型号拉插件 → `convert` → 喂规范化 reader。
**契约两点待补(登记)**(a) `convert` 返回 `{head,data,cor}` **未含 `.index`(打标)**,有打标的型号需扩约定;
(b) 规范化 `.data` 无扩展名C++ reader 解析字节宽(2/4) **单点依赖 `.head` 的 `BITS` 字段** → 保证转换器 BITS 永远正确是隐性前提(`malamira.py` 的 `filesize==LAST_TRACE×SAMPLES×bytes` 断言已守住,见 §2.5)。
---
## 5. 新增 / 改动清单C++ 侧,待实现)
| # | 文件 | 动作 |
|---|---|---|
| ① | `src/io/gpr/NormalizedRadarReader.{hpp,cpp}` **新** | 纯函数:`parseHead(.head)→RadarHeader``readDataCube(.data,header)→cube`(按 `BITS`+`ENDIAN_TYPE` 解二进制,`reshape(K,M,N)` 主序已定);`parseCor(.cor)→vector<TracePos>`(先解析,世界配准后置) |
| ② | `src/io/gpr/NormalizedRadarVolumeBridge.{hpp,cpp}` **新** | `buildLineVolumeFromNormalized(lineDir,prefix,metrics,coarse,targetDy)→core::BuiltI16`。⚠️ **DRY**`Gpr3dvVolumeBridge.cpp:133-197` 的值域扫描/`Quant`构造/逐体素填值/spacing 当前内联且耦合 Impulse 模型——动工时**先把这段抽成共享 helper**(签名接受抽象立方体访问器 `(c,t,s)→short` + 通道偏移/几何),两个 bridge 同调;`planChannelInterpolation`/`depthOfSample` 已是共享纯函数。**不要复制填体循环**(否则两份独立漂移) |
| ③ | `src/data/GprVolumeRepository.{hpp,cpp}` 改 | 加 `createRadarVolumeGrid(lineDir,prefix,coarse,targetDy)`(调②,复用 `builtI16ToVolumeGrid`)。**不动**现有 `createGprVolumeGrid` |
| ④ | `src/data/api/Api3dRepository.{cpp,hpp}` 改 | **DS 优先 + 懒加载后台建体**3 处):(a) `StoredVolume`(:139) 加 `lineDir/linePrefix/coarse/ddCode/structParentId` 字段(`ddCode` 默认 `"dd_voxel"`,存量行为不变);(b) 新 `registerRadarDataset(lineDir,prefix,name,structParentId,coarse)`——**只存元数据、不建体**,以 `ddCode="dd_radar_3d"` 写进 `volumes_`id=`"radar-"+(++counter)`,瞬时返回;(c) `loadVolume`(:347 后) 加 radar 分支:未命中 cachedGrid 且有 `linePrefix`**后台线程** `createRadarVolumeGrid`(仿 `finalizeVolume:268-335``std::thread`+`QMetaObject::invokeMethod(qApp,...)` 回主线程;无 `QCoreApplication` 时退化同步,可单测)→ 建体 + 灰度色阶 + 缓存 + async `onOk`(d) `volumeRows()`(:170) 输出 `sv.ddCode` + `sv.structParentId`。运行期 `isVolumeDataset/addDatasetAsync``volumes_` 成员 → **零改动**。**懒加载+后台建体同时消除 UI 冻结(评审 HIGH)与 spinner 永久转圈(评审 MEDIUM)**。⚠️ `dimensionOf`/`CategoryConfig`/`splitByCategory` **不需改**voxel 段走 `volumeRows` 注入、绕过分类,见 §3b |
| ⑤ | `src/app/main.cpp` + `src/app/panels/columns/CategorySection.cpp` 改 | (1) 导入入口「三维雷达→导入测线目录」:选目录+前缀 → `registerRadarDataset``refreshAnalysis``setItemChecked(true)`+`setItemBusy(true)`+`scrollItemToTop`(同 `generateVolume:978-980`渲染异步、spinner 由 `datasetRendered` 撤)。(2) **放开两处 ddCode gate**给 `dd_radar_3d``main.cpp:1011` `detailRequested`→体属性;**`CategorySection.cpp:397`** 列表右键 `if (ddCode=="dd_voxel")` 块(「生成切片」+「色阶」**同在此一个块内****只改 :397 这一处**即同时放开二者,无独立 :403 gate——**不改则 radar 体无切片入口、P0 验收②不可达 — 评审 CRITICAL**|
| ⑥ | 测试 | `tests/io/gpr/test_normalized_radar_reader.cpp`(喂 `tests/data/radar/` 截断样例,断言维度/字节序/通道插值/主序) |
### 几何映射(关键算法,纯函数易测)
- **X(沿线 spacing)** = `DISTANCE_INTERVAL`(距离模式,本期唯一支持)。
- **Y(通道横向)** = 复用 `GprGeometry::planChannelInterpolation(chXOffsets, targetDy)``GprGeometry.hpp:27`)——
通道偏移取自 `.head``CH_X_OFFSETS`(不读 `.ord`),线内插值、**绝不跨线**(默认 2.5cm)。
- **Z(深度 spacing)** = `TIMEWINDOW/(SAMPLES-1) × 波速/2`;波速由 `DIELECTRIC` 求,`.rad` 无该字段时用默认波速。
- **量化**`core::BuiltI16`(复用 `core/algo/GprVolumeBuilder.hpp`)。
- **`.cor`/`.index`** 本期只解析不深用:单线放原点沿 +X切片/异常即可工作。逐道 GPS 世界配准 + 打标 → 多线阶段。
---
## 6. 分期
- **P0本期**:①–⑥,单线规范化体打通。**验收**:导入 `samples/radar/malamira_南同大道`(如 000 线)→
三维体段出现一条 **`dd_radar_3d`** DS → 勾选 → `loadVolume` 懒建体渲染 → 切 updown/leftright/frontback → 画点/线/面异常并挂体下。
- **P1**`.cor` 逐道 GPS 世界配准 + 高程、`.index` 打标、多线合一场景、轨迹 DS(`dd_trajectory_data`) 一并登记、DS 进左侧数据集树(挂 TM)。
- **P2**:大体量 → 把 `gpr_poc``ChunkedVolumeStore`+`*VolumeSource` 核外/LOD 接进 app碰 controller/view单独立项
---
## 7. 双数据集测试策略 + Impulse 转换器决策
已有完整 POC 基于**另一套雷达数据 明星路Impulse**,交接见 `.superpowers/sdd/HANDOFF-2026-06-27-gpr-lod-slice.md`
明星路 数据在本地(`D:/Downloads/明星路`14G20 测线)。
**关键差异**:明星路 是 `RADAR_TYPE_IMPLUSE`——每线 **14 通道为 14 个独立 `.iprb` 二进制**`_A01.._A14`,各 ~74MB+
逐通道 `.iprh` + `.ord` + `.gps/.time/.cor/.mrk`。Mala 则把 16 通道打进**一个** position-major `.rd3`
现有 C++ P1 链(`buildLineVolumeFromGpr3dv`**能直读 `.iprb`**。
> ⚠️ **校正(评审 HIGH-2**:明星路真正"经验证"的渲染是 **`gpr_poc` CLI 的核外 LOD 多线**路径,**不是** app 内
> `createGprVolume→builtI16ToVolumeGrid→loadVolume→addVolume` 的**稠密单体**路径——后者的 **app 导入 UI 触发从未接**
> HANDOFF-2026-06-27 §6 第 3 条:"数据层 `createGprVolume` 已就绪,但 UI 触发未接")。故 app 内稠密渲染对**两套数据都是首次**。
**决策:本期不建 Impulse→规范化 转换器。** 理由:
1. Impulse `.iprb` 原始二进制远比 Mala "改名+字段映射"复杂14 个独立通道文件、需逆向二进制布局);且 P1 链含**处理**
gain/filter而规范化 `.data` 是**原始**——raw vs processed 语义需先厘清。高成本、低边际价值。
2. 明星路 的 `.iprb` 已有现成 in-app 数据层入口(`createGprVolume`,只差接 UI 触发),复用成本低。
**双数据集测试(校正后表述)**
- **Mala16ch规范化 reader 新路径)** + **明星路14ch现有 `.iprb` P1 路径)** 在 app 内**首次**稠密渲染,
二者喂**同一条下游 render/slice/anomaly 链** → 互证下游对两种通道数/几何无关。**前置任务**:明星路 in-app 导入入口
同样需接(复用现成 `createGprVolume`,与清单⑤同形)。
- 以 **`gpr_poc` CLI 的明星路成像作为离线对照基线**(那才是已验证的),不声称 in-app 已验证。
- ⚠️ 注意:明星路是**处理后**数据、Mala 是**原始**数据 → 双测只验"管路通 + 几何/通道数无关"**不验"成像一致"**(二者有无增益滤波,视觉不可比)。
- 把 **`RADAR_TYPE_IMPLUSE` 转换器列为规划的第二插件**,待规范化管线需吞并 Impulse多半与服务端插件同步时再做
届时一并解决 raw/processed 取舍。当前"插件产规范化→reader 吃规范化"契约**仅对 Mala 一家成立,属过渡态**。
---
## 8. 风险 / 待确认
1. **`.cor` 坐标语义**`.pos` 是本地投影坐标按文档直映入经纬度列、N/E/M 占位、解状态=4。
真实 CRS 未知;单线渲染不依赖 `.cor` 配准,多线阶段需后端给 `CRS_CODE`
2. **波速默认值**Mala `.rad``DIELECTRIC` → Z 深度 spacing 用默认波速建议介电常数≈9 / v≈0.1 m·ns⁻¹
仅影响深度标尺,不影响体结构。需确认默认值。
3. **采集模式**:本期只支持**距离模式**X 用 `DISTANCE_INTERVAL`);时间模式(`SCAN_SECOND`)推迟。
4. **大体量 / 内存实算**app 侧 `ScalarVolume``std::vector<double>`8 字节/体素,**比 int16 体放大 4×**,是真正天花板)。
单线最大线K=3778, ny≈49@2.5cm, N=516coarse=4 → nx=945 → 体素 23.9M → **≈182 MiB**+中间 BuiltI16 48MB +VTK 副本,单线峰值 <0.5GB**P0 安全**
coarse=1 全分辨率 → **≈764 MB** 仅体(单线勉强,非 P0 默认)。**多线/全分辨率必走 P2 核外**`ChunkedVolumeStore`,已有 gpr_poc 实现)。
---
## 9. 测试计划
- **转换器(已验证)**`info` 维度校验 6/6 通过;`convert` 产物 `.data` 与源 `.rd3` 字节一致;`probe` 主序确诊。
- **C++ reader 单测**`tests/data/radar/` 放截断样例position-major 可按字节前截 K' 道得合法小体);
断言 K/M/N、字节序、`planChannelInterpolation` 行数、主序填值正确。
- **bridge/repo 单测**`createRadarVolumeGrid` 产 `VolumeGrid` 维度/spacing/vmin-vmax 合理NaN 空值语义。
- **联调(人工)**`build.bat app` → 导入样例 → 渲染 + 三向切片 + 三类异常;对照 明星路 现有路径同场景。
- 失败排查看桌面日志 `%LOCALAPPDATA%/Geomative/Geopro3/logs/geopro_*.log`
---
## 10. 文件地图(现有锚点)
- 数据体模型 `src/data/repo/I3dSceneRepository.hpp:19``VolumeGrid` 稠密 float
- 渲染入口 `src/controller/I3dSceneView.hpp:45``addVolume`/ `src/app/VtkSceneView.cpp:173`
- 体素工厂 `src/render/actors/VoxelActor.hpp:29/47``buildVoxel`/`buildVoxelI16`
- GPR IO `src/io/gpr/Gpr3dvVolumeBridge.hpp:47`(轴映射范本)/ `GprGeometry.hpp:27,32`(通道插值/深度)
- 体进 app `src/data/api/Api3dRepository.cpp:128``createGprVolume`/ `src/data/GprVolumeRepository.cpp:11,38`
- 切片/异常 `src/render/interact/SliceTool.hpp` / `InteractionManager.hpp:52,57` / `AnomalyDrawTool.hpp:33`
- 三级树 `src/app/panels/columns/CategoryAnalysisTab.hpp:28` / `CategoryConfig.hpp:23`
- 转换器 `tools/radar_convert/malamira.py` + `README.md`;样例 `samples/radar/malamira_南同大道/`
- 详情视图扩展(如未来做 2D 波列图,非本期)`docs/superpowers/specs/2026-06-28-dataset-detail-view-architecture-and-extension-guide.md`

View File

@ -1,115 +0,0 @@
# 三维雷达 导入→处理→渲染 全链路方案(结合 POC 评估)
> 2026-06-30。本文把用户给出的雷达产品目标落成方案并结合 POC明星路 13G已验证的资产
> 评估复用/缺口、定关键架构缝与决策、排风险与分期。范围限定**三维雷达**(渲染/切片/异常 +
> 其上游的导入/处理管线);不含 2D 雷达图、不含后端反演链。
## 1. 用户目标(七步)
1. 设备经 USB 接到用户电脑。
2. 客户端「设备连接」功能:自动识别设备、打开 USB 存储,用户选文件导入。
3. 另一导入分支:文件已在用户电脑,经**文件夹选择**导入。
4. 导入过程按文件类型(不同型号雷达)**自动加载插件**,对数据做 **ds 标准化转换**
5. 导入完成自动形成 **项目 / GS / TM / ds** 结构(建 GS/TM 的「方案」待细化)。
6. 数据集详情页:用**数据处理插件**处理原始数据,插件支持多种方法(用户勾选);
另**固定加入两个客户端内置处理方法****插值**、**预渲染**(即为 LOD 做准备)。
处理后**存为新 ds**。
7. VTK 视图:选三维雷达 ds 渲染、切片等;**所选 ds 可能是未处理原数据,也可能是处理后数据**(不同 ds
## 2. 关键决策(用户已拍板)
- **D1 — 预渲染LOD 烘焙)是可选的。** 默认勾选,但用户可取消。
**渲染路径必须同时支持「未预渲染」与「已预渲染」两类 ds**(不能假设所有大体都已烘焙 LOD
→ 采用**混合渲染源**(见 §4原/插值 ds 走整卷源;预渲染 ds 走 LOD 源。
## 3. POC ↔ 目标 映射(复用 vs 缺口)
**结论算法基本齐POC/app 已有标准化、插值、增益、LOD 引擎、渲染源抽象);缺的是三层"框架/管线"
+ 设备接入。**
| 步骤 | 已有POC/现状) | 缺口 |
|---|---|---|
| 12 设备 USB/存储 | 无 | **全新**Windows 设备识别 + USB 盘浏览(与 POC 无关,纯平台 plumbing |
| 3 文件夹导入 | 已有导入入口、`tools/radar_convert` malamira 转换器 | 文件夹选择 + 批量 |
| 4 按型号插件标准化 | 转换算法有malamira→规范化 `.head/.data`、`RadarVolumeAssembler`、int16 量化) | **导入插件框架**(按文件类型注册 reader现写死一种 |
| 5 项目/GS/TM/ds 结构 | ds 树(`sourceShowParentId` 派生嵌套)已在 | 自动建 GS/TM 的「方案」 |
| 6 处理插件 + 两内置 | 两内置**算法都有**:插值=`createRadarVolumeGrid` 通道插值(targetDy);预渲染=`ChunkedVolumeStore::write`+`buildPyramidStreaming`。增益(dewow/AGC/tpow)亦有 | **处理插件框架** + 「处理→存为新 ds」管线 + 多方法勾选 UI |
| 7 选 ds 渲染/切片 | **渲染源抽象 `IVolumeRenderSource`(整卷/LOD 多态,含 `sliceSource()`** + 整卷渲染 + 切片 + 异常 | 把 app 雷达路径迁到 `IVolumeRenderSource`LOD 源接进 appTrack D |
## 4. 架构缝:`IVolumeRenderSource`(已设计好,最低风险)
POC 已建好渲染源抽象(`src/render/source/IVolumeRenderSource.hpp`):上层(控制器/`SliceTool`)只认此接口,
运行时在两种实现间切换:
- **`WholeVolumeSource`(整卷)** —— 给**未预渲染**的原/插值 ds小体单纹理够用
- **`ViewAdaptiveVolumeSource`(核外金字塔 LOD** —— 给**已预渲染**的 ds大体按相机选层/选块重组单纹理)。
接口自带 `update(vtkCamera)`、`currentImages()`、**`sliceSource()`**(切片/异常的 reslice 基底也走它),
故"切片在两种源上都能切"是接口内建能力,不需两套切片代码。
> **D1 落到这里**:选 ds 渲染时按"该 ds 是否带 LOD store"路由到对应源。未预渲染 → 整卷源(现有内存
> 体路径迁入即可);已预渲染 → LOD 源Track D 接入)。
## 5. 处理与数据血缘模型
- 处理一律**产出新 ds**,挂在源 ds 下(复用现有派生树 `sourceShowParentId`
```
原始 ds ─[插值]→ 插值 ds ─[预渲染]→ 预渲染 ds(LOD store)
└─[增益/migration/…(可多选)]→ 处理 ds
```
- **两个内置处理方法**client 自带、固定加入):
- **插值**:线内通道插值(读真实道偏移、目标横向间距如 2.5cm**绝不跨线**)。算法=`createRadarVolumeGrid` 的 targetDy 路径。
- **预渲染LOD 烘焙)**:把体烘成 **`ChunkedVolumeStore` 分块金字塔**int16 量化、64³ brick、qCompress、
逐级 2× 降采样、每块 min/max流式 `buildPyramidStreaming` 不持整卷)。产出 = 一个 **store 目录**
不是普通稠密体 → 该 ds 须带「类型=LOD store + 路径」标记,供 §4 渲染路由。
- 顺序:通常先插值再预渲染(烘焙插值后的体);模型支持任选基底(也可直接烘原始)。
## 6. 预渲染专用落盘格式LOD 前置,已实现于 POC
`ChunkedVolumeStore`(一个目录,非单文件):
- `meta.json`:几何 + 量化(scale/offset) + 逐块索引(offset/压缩长/**每块 min/max**)
- `data.bin`:逐块 int16 → qCompress块内 i 最快、k 最慢;偏移全 64 位(卷 >2GB
- `data_L1.bin…`:金字塔各级(逐级 2× 降采样)。
构建:整卷 `write` 或流式 `StreamingVolumeWriter`(逐块写不持整卷)+ `buildPyramid(Streaming)`
渲染:`ViewAdaptiveVolumeSource` 打开 store`update(相机)`→选层+选块→`readBrick`→重组单 `vtkImageData`
**内存恒定、绕 16384 纹理墙**。
## 7. 需要新建的三块骨架
1. **插件框架(两类,别混)**
- **导入插件**步4按文件类型/型号 → 标准化成 ds 的 reader 注册表。
- **处理插件**步6吃一个 ds → 产出新 ds 的 transform可多选串联两内置插值、预渲染即自带处理插件。
- 待定:插件接口(输入 ds/参数 → 输出 ds、发现/注册、进程内 DLLABI/崩溃隔离风险vs 子进程。
2. **「处理 → 新 ds」管线**:血缘落树、预渲染 ds 的 store 路径/缓存/失效/磁盘占用、重处理**异步+进度+可取消**。
3. **设备/USB 接入**步12Windows 设备识别 + USB 盘浏览。最独立、与 POC 无关,可最后做;先跑通文件夹导入。
## 8. 风险排序
1. **中**插件框架架构骨架影响步4/6定义不好后面返工
2. **中**:预渲染 ds 的渲染/切片路由Track D 核心;但**引擎+缝已验证**,是"接线"风险非"能不能做"风险)。
3. **低–中**:处理管线异步/进度/缓存(工程量明确)。
4. **低**:设备 USBplumbing独立
## 9. 分期建议
- **P0 验证最高技术风险**:把 app 雷达渲染迁到 `IVolumeRenderSource`,使
- 未预渲染 ds → `WholeVolumeSource`(迁现有内存体路径);
- 预渲染 ds → `ViewAdaptiveVolumeSource`
南同大道先烘一个小 store 验"选 ds→按是否预渲染路由→渲染+切片"全链路。**验通则整个方案立住。**
- **P1 插件框架**:先定**处理插件**接口(含两内置),跑通"原 ds→插值→预渲染→渲染";导入插件框架并行。
- **P2 处理管线 UI/异步**:详情页多方法勾选、进度、新 ds 落树。
- **P3 设备 USB**:最后接。
## 10. 现状基线(本轮已落地的交互/渲染精修,作为接入前的稳定底座)
- 切片拾取**精确化**:光标射线 vs 切片真实矩形求交 + 可见数据(alpha)双判定,去除外扩(雷达+反演通用)。
- 取消选中:点击体/空白/帘面即取消(精确"命中切片"判据)+ Esc 兜底。
- 滚轮步长:按**沿法向体素间距 × N**Shift 粗调),不随体长跳变。
- 双击正视:缩放到切片(按面内尺寸+视角框住),不再"又小又远"。
- 不透明度:各向异性体用特征尺度(门控;近立方反演维持原对角线)。
- **B 方案视角导航**#1 绕拾取点旋转(无选中时绕光标射线穿体中段点,不甩飞);
#2 沿线位置滑块(雷达专属,沿最长轴 dolly 到窗口;仅细长体显示)。
- 雷达显示**增益模式**右键切换AGC/保幅 tpow/关),纯显示重建、不动原始数据。
> 这些是**单内存体 + 渲染期采样距自适应**底座;多分辨率/视锥 LOD 仍属 §4/§9 的 Track D 接入范畴(未做)。

View File

@ -1,256 +0,0 @@
# VTK 视图重构:合并数据集单栏 + 动态段 + 图标工具条 + 2D 平面底图 — Spec2026-06-30
> 分支起点:`main`PR #9 已合并。职责范围VTK 视图左侧数据集栏(面板结构 + 段交互 + 2D/3D 共存渲染 + 底图)。
> 本 spec 替代 2026-06-26「二维分析锁定俯视」模型的相机/显隐部分(见 §10 迁移说明)。
## 0. 一句话目标
把「三维分析 + 二维分析」两个 tab 合并成**一个无标题的单列数据集栏**2D 与 3D 数据集用**一致的分段组织**同列呈现;段按数据有无**动态显隐**;段操作改**响应式图标工具条**2D 数据以**按类型一块平面**的方式与 3D 体/帘面在**同一个自由透视场景**里共存。
---
## 1. 背景与现状(接手必读)
- **面板**`ColumnDrawer` = 左侧抽屉,`QTabWidget` 两 tab
- 三维分析 `CategoryAnalysisTab``QScrollArea` 竖堆 `CategorySection`×4电阻率/视电阻率/瞬变/三维体)。
- 二维分析 `Column2DDataset`:平铺树 + 底图下拉 + 2D视图模式下拉 + 自定义Z 滑块。
- 切 tab 发 `analysisModeChanged(bool is2D)`
- **段** `CategorySection`段头chevron+标题 新增三维体[反演类] 导入雷达[voxel]+ 段体(**固定显示**的筛选行[日期范围+装置类型] + 可勾选数据树)。
- **分类**3D 用 `splitByCategory()``categoryConfigs()` 4 段2D 用 `splitByDimension().dim2D`(当前仅 `dd_trajectory_data`)。
- **渲染**
- 2D 轨迹 → `MapLineActor`(橙色 `vtkPolyLine`),经 `VtkSceneController::set2DPlacement(mode,z)` 摆到**单一全局 Z**5 模式0关/1 Z=0/2顶+50/3底-50/4自定义。已有逐 ds 的 Z 拖动偏移 `mapLineZOffset_`
- `VtkSceneView::setAnalysisMode2D(is2D)`:切 tab 时**按维度翻 actor 可见标志** + 相机锁定近俯视 + `VtkViewToolbar` 禁 6 向视图。
- 底图 `TileBasemap`(单例):天地图 WMTS卫星 `img_w`→`buildWarped` 带高程地形 / 矢量 `vec_w`→`buildFlat` 纯平面),透明度**固定 0.55**,瓦片范围 = `dataHorizontalRadius()×10``[2000,30000]m`,置 Z=0。`buildFlat` 已能纯平贴矢量瓦片。
- **顶部工具条** `TopBar`:视图/项目管理/业务工具/**设备** 四菜单。
- **渲染区竖排工具栏** `VtkViewToolbar`段1 `Gear`(坐标轴设置) → 分隔线 → 6向视图 → 缩放。
---
## 2. 已确认的关键决策(与用户逐条确认)
1. **单一自由场景共存**:合并删 tab 后,**取消**「锁定近俯视相机 + 按维度自动显隐」。场景恒为自由透视,勾选的 2D 平面与 3D 体/帘面**同时可见**。
2. **底图 = 1 个 3D 底图 + N 个 2D 底图**:所有三维数据**共用一个** 3D 底图(现状 `TileBasemap`);每个 **2D 类型(段)** 一块**独立平面底图**N = 2D 段数,非每条 ds 一张)。
3. **默认勾选**:沿用现有「直接挂项目下的 ds 默认进 VTK」逻辑本次只保证动态显隐段时不破坏它。
4. **2D z 值按类型一块平面**:每个 2D 类型一块平面 + 一个 z 滑块;平面初始 z = 该类型**第一个被勾选 ds** 的 z同类型其余 ds 投影到此平面。
5. **3D 底图控件移到 `VtkViewToolbar`**Gear 之后),不在 3D 段上。
6. **「导入雷达」移到 `TopBar` 设备菜单**(临时测试功能,后续整体移除)。
7. **`view2DMode` 5 模式下拉废弃**2D 高度完全由「z值」滑块替代。
---
## 3. 架构方案
**采用方案 A统一单列 `DatasetColumn` + 类型抽象§5。**
- 用**一份类目描述符目录 `categoryCatalog()`**§5每类型一份 `CategoryDescriptor`:分类/筛选/操作/渲染策略)同时驱动 3D/2D 段;`CategorySection` 按描述符建筛选器与图标条;`VtkSceneController` 按描述符的 `renderStrategyId` 查可插拔渲染策略§5.4)渲染。**消费方不再 `if dimension/ddCode` 散判**。
- 删除 `Column2DDataset``QTabWidget``ColumnDrawer` 承载单个 `DatasetColumn`,保留折叠开关。
- 取舍:复用已验证的勾选保留/折叠/spinner/结构树逻辑改动集中在描述符目录、段头工具条OpKind 映射)、渲染策略注册表。
(备选 B「两 widget 去 tab 竖堆」因 2D/3D 段不一致、重复逻辑被否C「全重写」工作量过大被否。
---
## 4. 面板结构§1 重构)
- `ColumnDrawer`:去 `QTabWidget` + `Column2DDataset`;改持 `DatasetColumn`**移除** `analysisModeChanged` 信号链。折叠开关保留。
- `DatasetColumn`(原 `CategoryAnalysisTab` 改名/改造):`QScrollArea` 竖堆 N 个 `CategorySection`**无栏目标题**。
- **动态显隐**:段 bucket 为空 → 段 `hide()`;非空 → `show()`。三维体段同理(默认空→默认不显示)。`relayoutSections()`/stretch 逻辑只对可见段生效。
- **空面板占位**:所有段均空时,滚动区中央显示提示语占位(文案:「请在左侧对象树勾选测线 / 数据集」);任一段非空则隐藏占位。
---
## 5. 类型抽象(扩展契约 —— 本 spec 的架构基石)
> 目标:**接入一个新 ds 类型 = 实现一份描述符(必要时再补一个渲染策略 / 一个操作 / 一个筛选器)**无论它「按什么规则接入、有什么操作、怎么渲染」UI 层与渲染层都只消费抽象、不再 `if dimension/ddCode` 散判。这是「数据集栏目」的统一规范,所有现存 5 类与未来新类都走它。
### 5.1 类目描述符 `CategoryDescriptor`data 层,纯 C++,无 Qt/VTK
```cpp
namespace geopro::data {
enum class SceneKind { Volume3D, Curtain3D, Plane2D }; // 渲染语义 / 共存规则
enum class FilterKind { DateRange, ArrayType }; // 筛选器契约(可扩展)
enum class OpKind { GenerateVolume, Filter, PlaneZ, Basemap }; // 段操作契约(可扩展)
struct CategoryDescriptor {
std::string id; // "resistivity"/"apparent"/"transient"/"voxel"/"trajectory" ...
std::string title; // 段标题
SceneKind sceneKind; // 渲染语义
std::function<bool(const DsRow&)> classify; // 轴1 数据来源/分类("无论按什么规则接入"
std::vector<FilterKind> filters; // 轴2 本段筛选器(顺序=显示顺序)
std::vector<OpKind> operations; // 轴3 段头图标操作(顺序=显示顺序)
std::string renderStrategyId; // 轴4 渲染策略键(解析到注册表,见 §5.4
};
// classify 便捷构造器(覆盖现有按 ddCode / dsTypeCode 接入的常见情形;任意复杂规则可直接写 lambda
std::function<bool(const DsRow&)> byDdCode(std::initializer_list<std::string> codes);
std::function<bool(const DsRow&)> byDsTypeCode(std::initializer_list<std::string> codes);
const std::vector<CategoryDescriptor>& categoryCatalog(); // 有序目录,取代 categoryConfigs()
} // namespace geopro::data
```
### 5.2 目录定义catalog取代 `categoryConfigs()`
| id | title | sceneKind | classify | filters | operations | renderStrategyId |
|---|---|---|---|---|---|---|
| resistivity | 电阻率数据 | Curtain3D | `byDsTypeCode({"ERT platform inversion data"})` | DateRange,ArrayType | GenerateVolume,Filter | `"curtain"` |
| apparent | 视电阻率数据 | Curtain3D | `byDsTypeCode({"visual resistivity data"})` | DateRange,ArrayType | GenerateVolume,Filter | `"curtain"` |
| transient | 瞬变电磁数据 | Curtain3D | `byDsTypeCode({"DD TRANSIENT ELECTROMAGNETIC INVERSION"})` | DateRange | GenerateVolume,Filter | `"curtain"` |
| voxel | 三维体 | Volume3D | `byDdCode({"dd_voxel"})`mock 注入,见 §10 | DateRange | Filter | `"volume"` |
| trajectory | 轨迹数据 | Plane2D | `byDdCode({"dd_trajectory_data"})` | DateRange | PlaneZ,Filter,Basemap | `"plane2d"` |
段顺序即表序(电阻率→…→轨迹)。分流:`splitByCategory(rows)` 改为遍历 catalog对每行命中**首个** `classify(row)==true` 的描述符即归入该段(保留原顺序)。`Column2DDataset` / `splitByDimension` 退役;旧 `categoryConfigs()`/`CategorySpec` 由 `categoryCatalog()`/`CategoryDescriptor` 取代。
### 5.3 消费方只认抽象
- `CategorySection(descriptor)`:按 `descriptor.filters` 建筛选器(`FilterKind→UI` 映射一处)、按 `descriptor.operations` 建图标条(`OpKind→按钮+信号` 映射一处,见 §6/§7
- `DatasetColumn`:遍历 `categoryCatalog()` 建段、用 `descriptor.classify` 路由数据(即 `splitByCategory`)。
- `VtkSceneController`:勾选某 ds → 查其所属描述符 → `renderStrategyId` → 渲染策略 `add/remove` + 首勾/全消 `onTypeActivated/Deactivated`(见 §5.4、§8
### 5.4 可插拔渲染策略 `IDatasetRenderStrategy`controller/render 层)
```cpp
namespace geopro::controller {
class IDatasetRenderStrategy {
public:
virtual ~IDatasetRenderStrategy() = default;
virtual void add(const std::string& typeId, const std::string& dsId) = 0; // 异步加载+入场
virtual void remove(const std::string& dsId) = 0;
// 每类型场景资源生命周期(可选):本类型首个 ds 入场 / 全部离场
virtual void onTypeActivated(const std::string& typeId) {}
virtual void onTypeDeactivated(const std::string& typeId) {}
};
// 注册表renderStrategyId(字符串键) → 策略实例。当前 3 实现:
// "volume" VolumeRenderStrategy —— 体素/雷达体(包现 isVolumeDataset 分支)
// "curtain" CurtainRenderStrategy —— 反演帘面(包现 dd_section 等分支)
// "plane2d" Plane2DRenderStrategy —— 2D 折线落类型平面 + 平面底图(封装 §8.2 平面 z + §9.2 底图)
} // namespace geopro::controller
```
**关键**2D 的全部特殊性(按类型平面 z 生命周期 + N 个平面底图)**封死在 `Plane2DRenderStrategy` 一个类内**(内含 `PlaneZRegistry` + 平面底图管理3D 两个策略只是包住现有渲染分支。控制器主流程不再含维度/ddCode 分支,只做「查描述符 → 取策略 → 调用」。
### 5.5 「接入一个新 ds 类型」标准动作(即本规范)
| 场景 | 要做的 |
|---|---|
| 新类型、**沿用**已有筛选/操作/渲染 | **只加一条 `CategoryDescriptor`**(纯数据),完。 |
| 新类型、要**新渲染方式** | 加描述符 + 实现一个 `IDatasetRenderStrategy` 并注册(新 `renderStrategyId`)。 |
| 新类型、要**新操作** | `OpKind` 加一项 + `CategorySection` 加该 kind→UI 的**一处**映射;描述符 `operations` 列上。 |
| 新类型、要**新筛选器** | `FilterKind` 加一项 + `CategorySection` 加**一处**映射;描述符 `filters` 列上。 |
数据驱动优先;只有真出现「新渲染/新操作/新筛选器」才动对应那**一个**扩展点,且改动收敛在单一位置,不扩散。
---
## 6. 响应式图标工具条(新建 `SectionIconBar`
- 段头右侧承载图标工具条:**默认最多显示 3 个图标**;图标总数超 3**或段宽被挤压放不下时****右侧图标依次收进末尾「…」下拉菜单**。`resizeEvent` 动态重算可见数 —— 这是**必须实现并可验证**的行为:即使图标 ≤3当栏位宽度收窄到放不下时右侧图标也要实时折进「…」栏位变宽再弹回。
- 每个图标 = `QToolButton`autoRaise + glyph + tooltip点击触发对应操作部分弹 popup
- **图标集由描述符 `operations``OpKind` 列表)驱动**,不是按维度硬编码。`CategorySection` 内有一处 `OpKind→(图标 glyph + tooltip + 点击/popup)` 映射表新增操作只加一项映射§5.5)。当前 catalog 各段对应:
| 段 | `operations`(左→右,右侧优先收进…) |
|---|---|
| 3D 反演(电阻率/视电阻率/瞬变) | `GenerateVolume`、`Filter` |
| 3D 三维体 | `Filter` |
| 2D 轨迹 | `PlaneZ`、`Filter`、`Basemap` |
- 注:当前各段图标 ≤3「数量超 3」分支暂不触发但**「宽度挤压」分支必须工作**(窄栏即折叠);两个分支都要实现(后续图标会增加,数量分支随之生效)。
---
## 7. 段内操作行为(每个 = 一个 `OpKind`
> 下列每个操作对应 §5 的一个 `OpKind``CategorySection` 的 `OpKind→UI` 映射表据描述符 `operations` 装配。`PlaneZ`/`Basemap` 的渲染落点封装在 `Plane2DRenderStrategy`§5.4/§8.2/§9.2)。
### 7.1 筛选 `OpKind::Filter`通用D2+D3
- 现「固定显示的筛选行(日期范围 + 装置类型[仅 ERT])」改为**默认折叠**(段体内不占位)。
- 点「筛选」图标 → 展开筛选行;再点 → 收起toggle。筛选逻辑`passesFilters`/`rebuildList`)不变。
### 7.2 新增三维体 `OpKind::GenerateVolume`(仅 3D 反演段)
- 保持原功能(发 `generateVolumeRequested`),入口从文字按钮改图标。
### 7.3 z值 `OpKind::PlaneZ`(仅 2D 段)
- 点图标弹 popup一个滑块整体上下移动**该 2D 类型那块平面**的 z。
- 初值 = 该类型第一个被勾选 ds 的 z见 §8.2);范围按场景高程量级合理取(实现期定)。
### 7.4 底图 `OpKind::Basemap`(仅 2D 段)
- 点图标弹 popup【底图类型矢量平面(默认) / 无】+【透明度滑块(默认 50%)】。
- 作用于**该 2D 类型自己那块平面底图**§9.2,由 `Plane2DRenderStrategy` 持有)。
### 7.5 3D 底图(移到 `VtkViewToolbar`,非段操作)
- 在 `Gear`(坐标轴设置) 正下方新增「地图」图标按钮,点击弹 popup【底图类型天地图(默认) / 无】+【透明度滑块(默认 50%)】。
- 控制**全局唯一**的 3D 底图(`TileBasemap`):「无」= 隐藏;透明度去掉固定 `0.55`、改默认 `0.5` 可调。
### 7.6 导入雷达(移到 `TopBar` 设备菜单,临时测试)
- `TopBar` 设备菜单加「导入雷达测线」→ 子项「规范化(.head/.data)…」「Impulse(.iprb)…」,发等价于现 `radarImportRequested(impulse)` 的信号到既有导入流程。
- 从 `CategorySection`(voxel 段头) 移除该入口及 `radarImportRequested` 转发。
---
## 8. 渲染模型(经渲染策略 §5.4
> 控制器主流程:勾选 diff → 对每个新增/移除 ds 查其描述符 → 取 `renderStrategyId` 对应策略 → `add/remove`;某类型「首勾」「全消」时调 `onTypeActivated/Deactivated`。无维度/ddCode 分支。
### 8.1 删除维度耦合
- 移除 `VtkSceneView::setAnalysisMode2D` 的「相机锁定俯视 + 按维度翻可见标志」与 `VtkViewToolbar::setAnalysisMode2D` 的 6 向禁用。场景恒自由透视2D/3D actor 同时可见(各自由勾选控制显隐)。
- 移除 `view2DMode` 5 模式与旧 `set2DPlacement` mode 维度(其职责并入 `Plane2DRenderStrategy`)。
### 8.2 2D 按类型平面(需求 5—— 封装在 `Plane2DRenderStrategy`
- 同一 2D 类型(段)勾选的全部 ds 投影到**一块平面**
- 平面 z = 该类型**第一个被勾选 ds** 的 z首个勾选时确定后**固定不变**,仅由 z 滑块整体升降)。
- 平面**纯平、不渲染高程**。
- 同类型其余 ds 的折线全部落在此平面 z。
- **平面生命周期**:该类型**首个 ds 被勾选** → 创建平面(定 z+ 创建其平面底图§9.2);期间 z 固定;该类型**全部 ds 取消勾选** → 平面**与其底图一并销毁**。
- 逐 ds 独立拖动 Z`nudgeSelectedMapLinesZ` / `mapLineZOffset_`**废弃**,统一到类型平面 z。
- 渲染复用 `MapLineActor`(折线几何不变),仅 Z 落点改为「所属类型平面 z」。
---
## 9. 底图体系§6
### 9.1 3D 共享底图1 个)
- 沿用 `TileBasemap` 单例带高程地形Z=0。透明度参数化默认 0.5,由 §7.5 popup 调);支持隐藏。瓦片范围规则不变。
### 9.2 2D 平面底图N 个,每类型一块)—— 实现选型已定
- **决策:参数化 `TileBasemap` 支持多实例**(不另抽 `PlaneBasemap`)。依据:`TileBasemap` 的「相机驱动 LOD + 四叉树细分 + 视锥剔除 + 限并发下载 + GeoLocalFrame 配准」正是平面底图所需,且其状态全为 per-instance无全局/静态状态,`tileKey` 为纯函数),多实例天然可行。新抽类要么重写数百行 LOD/网络逻辑,要么退化成单层无 LOD 大平面(缩放发虚、且违背「瓦片范围参考三维规则」)。
- **需改的 3 处硬编码**
| 改动 | 现状 | 改为 |
|---|---|---|
| 地面 Z | `kGroundZ=0` 常量 | 构造/setter 传 `groundZ`2D 平面 = 类型平面 z |
| 透明度 | `kTerrainOpacity=0.55` 固定 | 参数化,默认 0.5 可调 |
| 平面/矢量模式 | 由 `Kind` 隐含 | 复用 `Street`(vec_w)+`buildFlat`(已是纯平矢量路径,跳过 DEM/warp |
- **最终布局**1 个 `TileBasemap`3DSatellite/带高程Z=0§9.1,挂控制器)+ N 个 `TileBasemap`(每 2D 类型一个Street/纯平矢量,`groundZ`=平面 z**由 `Plane2DRenderStrategy` 持有**)。共享同一 `Scene` / `GeoLocalFrame`
- **生命周期**(在 `Plane2DRenderStrategy` 内,经 `onTypeActivated/Deactivated`):按 2D 类型持有 N 个实例 + 各自平面 z`PlaneZRegistry`);该类型首勾 → 建实例§8.2 定 z该类型全消 → 销毁实例(连同折线平面)。
- 瓦片范围**复用** `dataHorizontalRadius()×10``[2000,30000]m` 规则(各实例共享同一 `dataRadiusProvider`)。
- 坐标对齐沿用 `GeoLocalFrame`经纬→局部frame 重锚逻辑不变。
---
## 10. 信号 / 默认勾选§7
- 2D 勾选/选中信号从 `Column2DDataset` 迁到 2D `CategorySection`(复用 `checkedDatasetsChanged` / `datasetSelected`)。`basemapChanged` / `view2DModeChanged` / `customZChanged` 退役,由 §7.4/§7.5 的 popup + §8.2 平面 z 替代。
- 「直接挂项目下 ds 默认进 VTK」既有逻辑保留动态显隐段时确保默认勾选的 ds 所属段被显示且勾选状态正确。
---
## 11. 迁移说明 / 取舍 / 待定
- **替代 2026-06-26 spec**:该 spec 的「一场景两相机 + 按维度显隐 + 高程拖动分层」中,**相机锁定 + 维度显隐被本 spec 推翻**改单一自由场景共存。其「2D 沿 Z 拖动分离」语义改由「按类型平面 + z 滑块」承担——逐 ds 独立拖动 `nudgeSelectedMapLinesZ` / `mapLineZOffset_`(及拾取拖动浮层读数)**废弃移除**,统一到类型平面 z。
- **`dd_raster`** 仍未接2026-06-26 §6 遗留),本 spec 不含;后续作为新的 2D 类型段加入时复用 §8.2/§9.2 平面+底图机制。
- **3D 反演段 vs 三维体段**:均为 D3但 3D 反演段渲染为帘面、三维体段为体素/雷达体——共用 3D 底图与自由场景,无需区分。
- **责任拆分**`CategoryDescriptor`/`categoryCatalog`(描述符)、`IDatasetRenderStrategy` 注册表(渲染)、`SectionIconBar`(响应式工具条)、`TileBasemap` 多实例、`DatasetColumn`(动态显隐)相互独立,可分别实现与测试。
- **抽象的 YAGNI 边界**`FilterKind`/`OpKind`/`SceneKind` 只列当前真用到的枚举值;渲染策略只实现 `volume`/`curtain`/`plane2d` 三个。扩展点是「加新值/新策略」的预留口,不预先实现任何未用类型。
---
## 12. 验收
1. 左侧只有**一个无标题数据集栏**(无 tab其中同时出现 3D 类型段(电阻率/视电阻率/瞬变/三维体)与 2D 类型段(轨迹)。
2. 段**动态显隐**:无对应数据的段不显示;面板全空时显示居中占位提示。
3. 段头操作为**图标工具条**:默认最多 3 个,超出/挤压时右侧收进「…」下拉。3D 反演段含「新增三维体/筛选」2D 轨迹段含「z值/筛选/底图」。
4. 「筛选」图标可**展开/收起**段内筛选行(默认折叠)。
5. 勾选 2D 轨迹:同类型 ds 投影到**一块纯平平面**,平面 z = 首个勾选 ds 的 z之后固定「z值」滑块整体升降该平面「底图」popup 可换矢量平面底图/无 + 调透明度(默认 50%)。该类型**全部取消勾选 → 平面与其底图一并消失**。
6. 3D 与 2D 数据在**同一自由透视场景**同时可见,可自由旋转(无锁定俯视、无 tab 切换)。
7. 渲染区工具栏 Gear 下方新增「地图」按钮,控制全局 3D 底图(天地图/无 + 透明度默认 50%)。
8. 「导入雷达」入口出现在顶部「设备」菜单;三维体段头不再有该按钮。
9. 「直接挂项目下的 ds」加载时默认勾选进 VTK 不被破坏。
10. **可扩展性**:现存 5 类全部经 `categoryCatalog()` 描述符 + 渲染策略注册表驱动,控制器/段头无维度/ddCode 散判。新增一个「沿用已有渲染/操作/筛选」的类型,只需在 catalog 加一条描述符即可显示+渲染(以一个验证性 demo 描述符或单元测试佐证分类/路由走通)。

View File

@ -1,67 +0,0 @@
# VTK 视图导航与坐标轴改进 — Spec2026-07-01
> 分支 `feat/vtk-merged-dataset-column`(合并数据集单栏重构之后的增量)。职责:渲染区坐标轴、方向标、相机导航、列表双击联动。全部决策已与用户逐条确认。
## 0. 背景与动机
合并单栏 + 单一自由场景后2D 平面与 3D 体/帘面共存于一个世界坐标系。现状渲染坐标轴是**全场景合并包围盒**的一套 cube axes`AxesActor`/`rebuildAxes`)。用户痛点:多个渲染物相距很远/尺度悬殊时,全场景轴只贴近某一个物体,**离轴远的物体读不出真实尺寸**。业界通行解法不是「每物体一套常驻轴」,而是「全场景一套参考 + 选中物体单独出其贴合轴 + 可点击方向标 + 按需测量」。本 spec 落地导航侧改进。
## 1. 已确认决策(逐条)
1. **坐标轴(空间)全场景一套是对的**——坐标轴量的是空间位置(一个世界 CS物理量差异由逐数据集色阶承担尺度悬殊用统一 VE不拆轴。
2. **选中物体 → 隐全景轴、只显该物体贴合轴**Q1
3. **贴合轴按层级子树归一到一个包围盒**:选中三维体(或其下切片、切片下异常)→ 包围盒覆盖「该三维体 + 其切片 + 其异常」整棵子树,合成**一个**盒;选谁都归到该子树同一个盒。
4. **角落三向标gnomon常驻 + 可点击**Q2点击某方向轴 → 相机**绕支点转到该轴、保留当前缩放距离**ViewCube 手感选项1
5. **绕轴支点规则**:有选中 → 选中物体**子树包围盒中心**;无选中 → **全场景合并包围盒中心**。两者都保留当前缩放。
6. **列表双击 DS →a视图适配到该 DS 的空间范围 +b联动打开中下方数据集详情页**;三维体等**详情页未做**的类型要**容错静默**(找不到对应详情页不报错、不弹窗,仅完成适配)。
## 2. 术语:子树包围盒
- 一个 ds 的「子树」= 该 ds 自身 + 其所有后代 ds三维体 → 其切片 → 切片的异常),仅计入**当前已渲染**的成员。
- 子树包围盒 = 子树内所有已渲染 ds 的 actor 包围盒并集(`dsProps_` 按 dsId 取 actor bounds
- 后代关系来源:数据模型的父子(切片 parentId=体、异常 parentId=切片/体)——由控制器/仓储或面板树提供 dsId→后代集。渲染侧只需拿到「要并集的 dsId 列表」。
## 3. 设计
### 3.1 相机/包围盒基元(`VtkSceneView` + `CameraPreset`
- `bool datasetBounds(const std::vector<std::string>& dsIds, double outB[6]) const`:并集给定 dsIds 的已渲染 actor 包围盒;无有效则返回 false。
- `void fitToBounds(const double b[6])`:把相机适配到指定包围盒(保持朝向,`ResetCamera(b)`)。用于双击适配、贴合。
- `void orbitToAxis(ViewDir dir, const double pivot[3])`:相机**绕 pivot** 转到沿 dir 轴看向 pivot**保留当前 focal-to-camera 距离**(即当前缩放)。区别于 `applyCameraView`(正视重置)与 6 视图按钮(重置+fit。实现取当前 |camfocal| 距离 dfocal 设为 pivot按 dir 设 cam=pivot+dir_offset*d、view-up 按预设;`ResetCameraClippingRange`。
### 3.2 贴合轴 + 隐全景轴(决策 2/3
- `VtkSceneView` 维护两态:`rebuildAxes()`(全场景,现状)与新 `showFittedAxes(const double b[6])`/`showSceneAxes()`。
- 选中某 ds`datasetSelected` 到达)→ 上层算该 ds 子树的 dsIds → `datasetBounds``showFittedAxes(box)`(把 cube axes 的 bounds 设为子树盒)+ 隐全景轴。
- 取消选中(空选中)→ `showSceneAxes()`(恢复全场景轴)。
- 实现可复用 `AxesActor`:新增「按外部 bounds 显示」的模式(不再固定用全场景 bounds选中/取消在两种 bounds 间切换。
### 3.3 可点击方向标 gnomon决策 4/5
- 新增角落三向标:`vtkOrientationMarkerWidget` + `vtkAxesActor`XYZ 三向 + 轴标签),常驻右下(或右上,避开现有工具栏)。
- **可点击**`vtkOrientationMarkerWidget` 默认不透传点击到轴。需在其上做拾取——用一个 `vtkPropPicker`/自定义,把点击落到 +X/X/+Y/Y/+Z/Z 六向之一 → 调 `orbitToAxis(dir, pivot)`。pivot 按决策 5选中子树中心 / 全场景中心)。
- 六向 → `ViewDir` 映射:+Z=Top、Z=Bottom、+Y=Back(北望反)/Y=Front… 与 `CameraPreset` 现有 ViewDir 语义对齐(复用,不新造方向定义)。
### 3.4 双击 DS → 适配 + 详情(决策 6
- 列表双击现已发 `detailRequested(dsId, ddCode, name)``CategorySection::itemDoubleClicked`)。扩展双击行为:**并发**触发
1. **适配**:算该 dsId 子树 dsIds → `datasetBounds``fitToBounds`(相机适配到该 DS 空间范围,保持朝向)。
2. **详情联动**:沿用现有 `detailRequested → DatasetDetailController` 链打开中下方 `DatasetDetailPanel`
- **容错静默**:对详情页未实现的类型(三维体 `dd_voxel`/`dd_radar_3d` 等),详情链找不到对应页时**静默**——不弹错、不打断,适配照常完成。实现:详情控制器/面板在无匹配详情页时走「无操作」分支(或双击前按 ddCode 判定「有详情页才发 detailRequested」。优先在联动入口按「该 ddCode 是否有详情页」gate无则只做适配。
## 4. 影响文件(预估)
- `src/app/VtkSceneView.{hpp,cpp}``datasetBounds`/`fitToBounds`/`orbitToAxis`/`showFittedAxes`/`showSceneAxes` + gnomon widget。
- `src/render/CameraPreset.{hpp,cpp}``src/render/actors/AxesActor.*`orbit-to-axis 相机数学、按外部 bounds 的轴。
- `src/controller/VtkSceneController.*` + `I3dSceneView`:转发新相机/轴/适配接口;子树 dsIds 解析(或由 main 用面板树/仓储父子算)。
- `src/app/main.cpp`:选中→贴合轴、双击→适配+详情 gate、gnomon 点击接线。
- `src/app/panels/columns/CategorySection.*`/`CategoryAnalysisTab.*`:双击透传(已有 detailRequested可能加「fitRequested(dsId)」或复用 datasetSelected
## 5. 分期/任务
- **T1基元**`datasetBounds`/`fitToBounds`/`orbitToAxis`(相机+包围盒基元,纯渲染,可单测 orbit 数学)。
- **T2贴合轴**:选中→子树盒→贴合轴+隐全景轴;取消→恢复全景轴。含「子树 dsIds 解析」。
- **T3gnomon**:常驻可点击三向标 → orbitToAxis(pivot 规则)。
- **T4双击**:双击→适配到子树盒 + 详情联动 gate无详情页静默
- 顺序T1 先T2/T3/T4 都依赖 T1 的基元T2/T4 依赖「子树 dsIds 解析」T2 引入T4 复用)。
## 6. 验收
1. 无选中:显示全场景总览轴 + 右下角三向标。
2. 选中某体/切片/异常:全景轴隐去,出现覆盖「该体+其切片+异常」子树的一个贴合包围盒轴,能读该子树真实尺寸;取消选中恢复全景轴。
3. 点击三向标某方向轴:相机绕支点(选中子树中心/无选中则全场景中心)转到该轴、**缩放不变**、数据仍居中。
4. 双击列表某 DS视图适配到该 DS子树空间范围有详情页的类型联动打开中下方详情页三维体等无详情页类型**静默**(只适配、不报错)。
5. VE 对全部渲染物一致;坐标轴仍是空间位置度量、逐数据集色阶不变。

View File

@ -124,16 +124,6 @@ if (-not $SkipDeploy) {
} finally {
if (-not $adsPreexisted) { Remove-Item $adsTmp -Force -ErrorAction SilentlyContinue }
}
# 中文化windeployqt --no-translations 不带翻译,单独拷 Qt 自带 zh_CNQMessageBox/QFileDialog
# 等标准按钮中文化app 启动按 exe 旁 translations\ 加载)。
$qtZh = Join-Path $QtBin '..\translations\qtbase_zh_CN.qm'
if (Test-Path $qtZh) {
$stageTr = Join-Path $StageDir 'translations'
New-Item -ItemType Directory -Force $stageTr | Out-Null
Copy-Item $qtZh $stageTr -Force
} else {
Warn "未找到 qtbase_zh_CN.qm$qtZh)—部署版标准按钮可能仍为英文"
}
}
# --- 5.5 随包数据:本地样本演示数据 + PROJ 数据exe 旁布局,运行时相对定位)-------

View File

@ -1,21 +0,0 @@
# 样例MALA南同大道RADAR_TYPE_MALAMIRA → 规范化格式)
三维雷达联调/单测固定样例。由 `tools/radar_convert/malamira.py` 从原始 Mala Mira
数据转换而来16 通道、516 采样、距离模式6 条测线)。
- `*.head` / `*.cor`**纳入 git**(小文本)。
- `*.data`**.gitignore 忽略**(每条 ~4062MB共 ~277MB过大不入库。需要时按下方命令重生成。
## 重新生成 .data及全部规范化档
```bash
python tools/radar_convert/malamira.py convert \
"D:/Downloads/MALA南同大道_rSlicer" \
--out "samples/radar/malamira_南同大道"
```
原始数据来源:`D:/Downloads/MALA南同大道_rSlicer`(厂商 Mala rSlicer 导出,`.rad/.rd3/_G01.pos`)。
## 已验证数据事实(见 tools/radar_convert/README.md
- `.data` 主序 = **position-major** `(K道, M通道, N采样)`int16 小端,无需转置。
- 维度K∈[2333,3778]M=16N=516`K = LAST_TRACE / NUMBER_OF_CH`。

View File

@ -1,527 +0,0 @@
VERSION:1
1 317.179340 N 472.759046 E 49.980000 M 4
12 317.201303 N 472.700649 E 51.040000 M 4
17 317.384393 N 473.183925 E 51.140000 M 4
23 317.597198 N 473.749575 E 51.200000 M 4
29 317.792284 N 474.255457 E 51.240000 M 4
35 318.046986 N 474.867687 E 51.280000 M 4
41 318.273818 N 475.403025 E 51.310000 M 4
48 318.543100 N 475.999329 E 51.340000 M 4
55 318.830654 N 476.625089 E 51.370000 M 4
60 319.046043 N 477.118469 E 51.390000 M 4
65 319.246667 N 477.600376 E 51.410000 M 4
71 319.458364 N 478.140509 E 51.430000 M 4
77 319.651051 N 478.641082 E 51.450000 M 4
82 319.850383 N 479.176249 E 51.470000 M 4
88 320.012986 N 479.663978 E 51.490000 M 4
93 320.156763 N 480.190410 E 51.510000 M 4
101 320.339483 N 480.949576 E 51.540000 M 4
108 320.495626 N 481.651715 E 51.570000 M 4
113 320.608581 N 482.152802 E 51.590000 M 4
121 320.752912 N 482.893130 E 51.620000 M 4
128 320.857007 N 483.611708 E 51.650000 M 4
134 320.936555 N 484.149101 E 51.670000 M 4
139 321.010566 N 484.662347 E 51.690000 M 4
145 321.078671 N 485.236388 E 51.710000 M 4
150 321.144377 N 485.778405 E 51.730000 M 4
156 321.211744 N 486.372825 E 51.750000 M 4
162 321.271174 N 486.920493 E 51.770000 M 4
168 321.334665 N 487.508405 E 51.790000 M 4
173 321.408491 N 488.041688 E 51.810000 M 4
179 321.493207 N 488.605625 E 51.830000 M 4
184 321.570171 N 489.118015 E 51.850000 M 4
190 321.651565 N 489.665169 E 51.870000 M 4
195 321.736650 N 490.163345 E 51.890000 M 4
201 321.830410 N 490.703820 E 51.910000 M 4
206 321.917156 N 491.205763 E 51.930000 M 4
211 322.003164 N 491.757884 E 51.950000 M 4
217 322.081051 N 492.279179 E 51.970000 M 4
223 322.169273 N 492.859214 E 51.990000 M 4
228 322.248452 N 493.432741 E 52.010000 M 4
235 322.327447 N 494.097546 E 52.030000 M 4
242 322.416592 N 494.750021 E 52.050000 M 4
250 322.513489 N 495.501994 E 52.070000 M 4
257 322.607618 N 496.247460 E 52.090000 M 4
266 322.703962 N 497.114858 E 52.110000 M 4
275 322.803443 N 497.962561 E 52.130000 M 4
280 322.862135 N 498.476150 E 52.140000 M 4
289 322.964385 N 499.396294 E 52.160000 M 4
300 323.075125 N 500.446076 E 52.180000 M 4
305 323.114068 N 500.948705 E 52.190000 M 4
310 323.166485 N 501.465718 E 52.200000 M 4
315 323.218717 N 501.993521 E 52.210000 M 4
322 323.279255 N 502.624075 E 52.220000 M 4
327 323.330749 N 503.176538 E 52.230000 M 4
333 323.382981 N 503.739962 E 52.240000 M 4
339 323.436874 N 504.314345 E 52.250000 M 4
346 323.500181 N 504.996790 E 52.260000 M 4
352 323.545399 N 505.590525 E 52.270000 M 4
358 323.602984 N 506.183061 E 52.280000 M 4
364 323.662784 N 506.758986 E 52.290000 M 4
370 323.734949 N 507.422763 E 52.300000 M 4
376 323.800839 N 507.989612 E 52.310000 M 4
382 323.868759 N 508.555433 E 52.320000 M 4
388 323.936311 N 509.118342 E 52.330000 M 4
394 324.012537 N 509.770132 E 52.340000 M 4
400 324.070121 N 510.323965 E 52.350000 M 4
405 324.138964 N 510.879682 E 52.360000 M 4
411 324.210023 N 511.431118 E 52.370000 M 4
418 324.293262 N 512.071091 E 52.380000 M 4
423 324.365058 N 512.618416 E 52.390000 M 4
429 324.436854 N 513.161974 E 52.400000 M 4
434 324.510312 N 513.702621 E 52.410000 M 4
441 324.599088 N 514.329408 E 52.420000 M 4
446 324.668854 N 514.855498 E 52.430000 M 4
451 324.744342 N 515.387582 E 52.440000 M 4
457 324.819091 N 515.916241 E 52.450000 M 4
463 324.899931 N 516.526074 E 52.460000 M 4
468 324.966744 N 517.039491 E 52.470000 M 4
473 325.032450 N 517.540578 E 52.480000 M 4
484 325.178626 N 518.604403 E 52.500000 M 4
494 325.295457 N 519.583630 E 52.520000 M 4
505 325.410811 N 520.655333 E 52.540000 M 4
510 325.469872 N 521.155221 E 52.550000 M 4
515 325.536869 N 521.657335 E 52.560000 M 4
520 325.609219 N 522.164245 E 52.570000 M 4
526 325.687660 N 522.761234 E 52.580000 M 4
531 325.748382 N 523.274480 E 52.590000 M 4
537 325.804860 N 523.798173 E 52.600000 M 4
542 325.862075 N 524.328030 E 52.610000 M 4
548 325.934425 N 524.949337 E 52.620000 M 4
554 326.000500 N 525.487415 E 52.630000 M 4
559 326.066575 N 526.031829 E 52.640000 M 4
565 326.129696 N 526.582408 E 52.650000 M 4
572 326.199831 N 527.231972 E 52.660000 M 4
577 326.258893 N 527.792826 E 52.670000 M 4
583 326.323122 N 528.361387 E 52.680000 M 4
589 326.391042 N 528.933887 E 52.690000 M 4
596 326.472620 N 529.610166 E 52.700000 M 4
602 326.542386 N 530.196366 E 52.710000 M 4
608 326.612706 N 530.788560 E 52.720000 M 4
614 326.684502 N 531.385378 E 52.730000 M 4
621 326.772356 N 532.087687 E 52.740000 M 4
627 326.844521 N 532.692040 E 52.750000 M 4
633 326.921670 N 533.302558 E 52.760000 M 4
640 326.998265 N 533.915987 E 52.770000 M 4
647 327.089810 N 534.636450 E 52.780000 M 4
653 327.170835 N 535.256387 E 52.790000 M 4
659 327.252413 N 535.878036 E 52.800000 M 4
666 327.334729 N 536.501740 E 52.810000 M 4
673 327.432734 N 537.231622 E 52.820000 M 4
680 327.506191 N 537.853614 E 52.830000 M 4
686 327.588693 N 538.481086 E 52.840000 M 4
692 327.673039 N 539.109072 E 52.850000 M 4
700 327.773443 N 539.842207 E 52.860000 M 4
706 327.857975 N 540.469165 E 52.870000 M 4
713 327.945275 N 541.094582 E 52.880000 M 4
719 328.032390 N 541.718458 E 52.890000 M 4
726 328.133532 N 542.444571 E 52.900000 M 4
733 328.216402 N 543.059199 E 52.910000 M 4
739 328.304994 N 543.677766 E 52.920000 M 4
745 328.393586 N 544.294792 E 52.930000 M 4
752 328.496020 N 545.018166 E 52.940000 M 4
759 328.582213 N 545.638274 E 52.950000 M 4
765 328.666006 N 546.259409 E 52.960000 M 4
771 328.750353 N 546.881915 E 52.970000 M 4
779 328.850018 N 547.606488 E 52.980000 M 4
785 328.926060 N 548.225226 E 52.990000 M 4
791 329.010037 N 548.847389 E 53.000000 M 4
798 329.091985 N 549.470922 E 53.010000 M 4
805 329.185560 N 550.200803 E 53.020000 M 4
812 329.265292 N 550.827077 E 53.030000 M 4
818 329.345948 N 551.454035 E 53.040000 M 4
824 329.424758 N 552.083391 E 53.050000 M 4
832 329.513719 N 552.820294 E 53.060000 M 4
838 329.574441 N 553.450334 E 53.070000 M 4
845 329.646976 N 554.087225 E 53.080000 M 4
851 329.717295 N 554.727884 E 53.090000 M 4
859 329.798320 N 555.479514 E 53.100000 M 4
865 329.867717 N 556.126509 E 53.110000 M 4
872 329.933422 N 556.775217 E 53.120000 M 4
878 329.997098 N 557.426150 E 53.130000 M 4
886 330.069817 N 558.187885 E 53.140000 M 4
893 330.115405 N 558.837277 E 53.150000 M 4
899 330.176681 N 559.491807 E 53.160000 M 4
906 330.241463 N 560.148906 E 53.170000 M 4
914 330.317874 N 560.916977 E 53.180000 M 4
921 330.381549 N 561.575446 E 53.190000 M 4
927 330.445225 N 562.233230 E 53.200000 M 4
934 330.507239 N 562.890329 E 53.210000 M 4
942 330.579220 N 563.653091 E 53.220000 M 4
948 330.625361 N 564.298202 E 53.230000 M 4
955 330.686637 N 564.943484 E 53.240000 M 4
961 330.748652 N 565.582601 E 53.250000 M 4
969 330.824693 N 566.321045 E 53.260000 M 4
975 330.885969 N 566.945264 E 53.270000 M 4
981 330.945584 N 567.561433 E 53.280000 M 4
987 331.004460 N 568.169039 E 53.290000 M 4
994 331.072934 N 568.873233 E 53.300000 M 4
1000 331.122029 N 569.454809 E 53.310000 M 4
1006 331.184597 N 570.047688 E 53.320000 M 4
1012 331.244212 N 570.636457 E 53.330000 M 4
1019 331.310102 N 571.320785 E 53.340000 M 4
1025 331.369348 N 571.908526 E 53.350000 M 4
1031 331.434315 N 572.497123 E 53.360000 M 4
1037 331.502789 N 573.083666 E 53.370000 M 4
1044 331.582706 N 573.770049 E 53.380000 M 4
1050 331.641029 N 574.348371 E 53.390000 M 4
1056 331.711165 N 574.942106 E 53.400000 M 4
1062 331.783884 N 575.535328 E 53.410000 M 4
1069 331.874506 N 576.233014 E 53.420000 M 4
1075 331.953316 N 576.835483 E 53.430000 M 4
1081 332.031203 N 577.442918 E 53.440000 M 4
1088 332.106136 N 578.054806 E 53.450000 M 4
1095 332.193990 N 578.775782 E 53.460000 M 4
1101 332.257112 N 579.388355 E 53.470000 M 4
1108 332.328723 N 580.020108 E 53.480000 M 4
1114 332.394429 N 580.659911 E 53.490000 M 4
1122 332.466041 N 581.415994 E 53.500000 M 4
1128 332.527317 N 582.072065 E 53.510000 M 4
1135 332.590069 N 582.733788 E 53.520000 M 4
1142 332.655221 N 583.400477 E 53.530000 M 4
1150 332.728125 N 584.185159 E 53.540000 M 4
1156 332.777958 N 584.854075 E 53.550000 M 4
1163 332.839049 N 585.535320 E 53.560000 M 4
1170 332.904755 N 586.220333 E 53.570000 M 4
1178 332.991501 N 587.022141 E 53.580000 M 4
1185 333.071418 N 587.712463 E 53.590000 M 4
1192 333.152443 N 588.405354 E 53.600000 M 4
1200 333.232544 N 589.101156 E 53.610000 M 4
1208 333.325012 N 589.915808 E 53.620000 M 4
1215 333.401792 N 590.605787 E 53.630000 M 4
1222 333.488722 N 591.308953 E 53.640000 M 4
1229 333.578052 N 592.014345 E 53.650000 M 4
1238 333.682517 N 592.839957 E 53.660000 M 4
1245 333.771293 N 593.552542 E 53.670000 M 4
1252 333.858224 N 594.267353 E 53.680000 M 4
1259 333.944970 N 594.981651 E 53.690000 M 4
1268 334.049065 N 595.815825 E 53.700000 M 4
1275 334.125107 N 596.524643 E 53.710000 M 4
1282 334.213699 N 597.244934 E 53.720000 M 4
1290 334.298415 N 597.968136 E 53.730000 M 4
1298 334.398819 N 598.813100 E 53.740000 M 4
1306 334.486488 N 599.541440 E 53.750000 M 4
1313 334.577110 N 600.268582 E 53.760000 M 4
1320 334.667547 N 600.998634 E 53.770000 M 4
1329 334.768874 N 601.853531 E 53.780000 M 4
1336 334.840486 N 602.575877 E 53.790000 M 4
1344 334.928708 N 603.312095 E 53.800000 M 4
1351 335.019146 N 604.047970 E 53.810000 M 4
1360 335.124164 N 604.907833 E 53.820000 M 4
1368 335.210356 N 605.646619 E 53.830000 M 4
1375 335.294150 N 606.386091 E 53.840000 M 4
1383 335.379788 N 607.124364 E 53.850000 M 4
1391 335.483514 N 607.987994 E 53.860000 M 4
1399 335.558448 N 608.711711 E 53.870000 M 4
1406 335.642795 N 609.447586 E 53.880000 M 4
1414 335.724927 N 610.185002 E 53.890000 M 4
1422 335.820532 N 611.045208 E 53.900000 M 4
1430 335.903403 N 611.780740 E 53.910000 M 4
1437 335.987380 N 612.515759 E 53.920000 M 4
1445 336.069512 N 613.248895 E 53.930000 M 4
1453 336.164379 N 614.103106 E 53.940000 M 4
1461 336.230639 N 614.816718 E 53.950000 M 4
1468 336.310002 N 615.543860 E 53.960000 M 4
1475 336.387705 N 616.269802 E 53.970000 M 4
1484 336.478511 N 617.114766 E 53.980000 M 4
1491 336.555660 N 617.836599 E 53.990000 M 4
1499 336.632255 N 618.557917 E 54.000000 M 4
1506 336.707558 N 619.280264 E 54.010000 M 4
1514 336.792458 N 620.120604 E 54.020000 M 4
1522 336.849305 N 620.827708 E 54.030000 M 4
1529 336.920917 N 621.545431 E 54.040000 M 4
1536 336.992713 N 622.263325 E 54.050000 M 4
1544 337.076875 N 623.094930 E 54.060000 M 4
1552 337.150886 N 623.807002 E 54.070000 M 4
1559 337.228035 N 624.519586 E 54.080000 M 4
1566 337.304630 N 625.231658 E 54.090000 M 4
1575 337.391007 N 626.062578 E 54.100000 M 4
1582 337.450991 N 626.763004 E 54.110000 M 4
1589 337.530354 N 627.471650 E 54.120000 M 4
1596 337.613225 N 628.177385 E 54.130000 M 4
1604 337.712890 N 629.000257 E 54.140000 M 4
1612 337.793546 N 629.706677 E 54.150000 M 4
1619 337.871987 N 630.412583 E 54.160000 M 4
1626 337.953380 N 631.116262 E 54.170000 M 4
1634 338.051939 N 631.934511 E 54.180000 M 4
1641 338.119490 N 632.627059 E 54.190000 M 4
1648 338.206051 N 633.327142 E 54.200000 M 4
1655 338.291690 N 634.027739 E 54.210000 M 4
1664 338.390802 N 634.845474 E 54.220000 M 4
1671 338.477918 N 635.546242 E 54.230000 M 4
1678 338.566509 N 636.244442 E 54.240000 M 4
1685 338.655470 N 636.942813 E 54.250000 M 4
1693 338.761596 N 637.757464 E 54.260000 M 4
1700 338.835238 N 638.445560 E 54.270000 M 4
1707 338.922907 N 639.144787 E 54.280000 M 4
1714 339.009838 N 639.845213 E 54.290000 M 4
1723 339.113010 N 640.665345 E 54.300000 M 4
1730 339.207323 N 641.369538 E 54.310000 M 4
1737 339.300714 N 642.067053 E 54.320000 M 4
1744 339.387829 N 642.766109 E 54.330000 M 4
1752 339.482512 N 643.582644 E 54.340000 M 4
1759 339.541388 N 644.271083 E 54.350000 M 4
1767 339.621306 N 644.971680 E 54.360000 M 4
1774 339.706575 N 645.670736 E 54.370000 M 4
1782 339.805134 N 646.492066 E 54.380000 M 4
1789 339.880990 N 647.190780 E 54.390000 M 4
1796 339.950203 N 647.890521 E 54.400000 M 4
1803 340.018861 N 648.593515 E 54.410000 M 4
1812 340.103577 N 649.415360 E 54.420000 M 4
1819 340.155994 N 650.109963 E 54.430000 M 4
1826 340.226314 N 650.819123 E 54.440000 M 4
1833 340.294788 N 651.527255 E 54.450000 M 4
1841 340.375443 N 652.353723 E 54.460000 M 4
1849 340.452961 N 653.062712 E 54.470000 M 4
1856 340.525865 N 653.780434 E 54.480000 M 4
1863 340.593970 N 654.500897 E 54.490000 M 4
1872 340.684961 N 655.337127 E 54.500000 M 4
1879 340.734978 N 656.050739 E 54.510000 M 4
1886 340.823755 N 656.763666 E 54.520000 M 4
1893 340.909947 N 657.482759 E 54.530000 M 4
1902 341.004814 N 658.324640 E 54.540000 M 4
1909 341.089161 N 659.046130 E 54.550000 M 4
1917 341.180706 N 659.774299 E 54.560000 M 4
1924 341.272435 N 660.499214 E 54.570000 M 4
1933 341.376162 N 661.346404 E 54.580000 M 4
1940 341.434485 N 662.079539 E 54.590000 M 4
1948 341.514217 N 662.822265 E 54.600000 M 4
1955 341.597641 N 663.572012 E 54.610000 M 4
1964 341.702475 N 664.445403 E 54.620000 M 4
1972 341.793835 N 665.199089 E 54.630000 M 4
1979 341.885564 N 665.956200 E 54.640000 M 4
1987 341.975817 N 666.715537 E 54.650000 M 4
1996 342.083973 N 667.601772 E 54.660000 M 4
2004 342.157061 N 668.351690 E 54.670000 M 4
2011 342.253590 N 669.111027 E 54.680000 M 4
2019 342.345504 N 669.873618 E 54.690000 M 4
2028 342.450891 N 670.759683 E 54.700000 M 4
2036 342.540959 N 671.517307 E 54.710000 M 4
2044 342.630843 N 672.274418 E 54.720000 M 4
2051 342.721281 N 673.029302 E 54.730000 M 4
2060 342.823346 N 673.909544 E 54.740000 M 4
2068 342.892558 N 674.651927 E 54.750000 M 4
2075 342.982442 N 675.405613 E 54.760000 M 4
2083 343.072695 N 676.159127 E 54.770000 M 4
2092 343.181589 N 677.036286 E 54.780000 M 4
2099 343.277194 N 677.788602 E 54.790000 M 4
2107 343.372430 N 678.534581 E 54.800000 M 4
2115 343.469512 N 679.280561 E 54.810000 M 4
2123 343.583390 N 680.146246 E 54.820000 M 4
2131 343.664045 N 680.873558 E 54.830000 M 4
2138 343.760942 N 681.613544 E 54.840000 M 4
2146 343.854517 N 682.351817 E 54.850000 M 4
2155 343.961566 N 683.209967 E 54.860000 M 4
2162 344.052003 N 683.947212 E 54.870000 M 4
2170 344.144102 N 684.683944 E 54.880000 M 4
2177 344.236754 N 685.419819 E 54.890000 M 4
2186 344.343987 N 686.280538 E 54.900000 M 4
2193 344.423166 N 687.003912 E 54.910000 M 4
2201 344.515449 N 687.742185 E 54.920000 M 4
2208 344.607363 N 688.482341 E 54.930000 M 4
2217 344.717918 N 689.348712 E 54.940000 M 4
2224 344.812232 N 690.091780 E 54.950000 M 4
2232 344.905807 N 690.837245 E 54.960000 M 4
2240 344.996613 N 691.583909 E 54.970000 M 4
2249 345.099601 N 692.456102 E 54.980000 M 4
2256 345.176750 N 693.189580 E 54.990000 M 4
2264 345.261835 N 693.941039 E 55.000000 M 4
2271 345.345813 N 694.694211 E 55.010000 M 4
2280 345.439942 N 695.574796 E 55.020000 M 4
2288 345.518936 N 696.332420 E 55.030000 M 4
2295 345.596823 N 697.090045 E 55.040000 M 4
2303 345.672864 N 697.851437 E 55.050000 M 4
2312 345.761641 N 698.741097 E 55.060000 M 4
2320 345.824393 N 699.493071 E 55.070000 M 4
2328 345.895820 N 700.257031 E 55.080000 M 4
2335 345.967432 N 701.022534 E 55.090000 M 4
2344 346.050671 N 701.916133 E 55.100000 M 4
2352 346.121914 N 702.681464 E 55.110000 M 4
2360 346.194448 N 703.448850 E 55.120000 M 4
2368 346.267537 N 704.215893 E 55.130000 M 4
2377 346.351514 N 705.110349 E 55.140000 M 4
2384 346.414267 N 705.863864 E 55.150000 M 4
2392 346.491046 N 706.627996 E 55.160000 M 4
2400 346.568195 N 707.388360 E 55.170000 M 4
2409 346.660847 N 708.276308 E 55.180000 M 4
2416 346.741687 N 709.033762 E 55.190000 M 4
2424 346.827326 N 709.786420 E 55.200000 M 4
2432 346.919425 N 710.530173 E 55.210000 M 4
2440 347.029980 N 711.387295 E 55.220000 M 4
2448 347.120417 N 712.100565 E 55.230000 M 4
2455 347.227650 N 712.824453 E 55.240000 M 4
2462 347.343004 N 713.543032 E 55.250000 M 4
2471 347.488073 N 714.375836 E 55.260000 M 4
2478 347.622253 N 715.086195 E 55.270000 M 4
2485 347.762893 N 715.793642 E 55.280000 M 4
2493 347.910915 N 716.500918 E 55.290000 M 4
2501 348.094004 N 717.327900 E 55.300000 M 4
2508 348.254946 N 718.024387 E 55.310000 M 4
2516 348.430469 N 718.741596 E 55.320000 M 4
2523 348.614850 N 719.463429 E 55.330000 M 4
2532 348.841313 N 720.310961 E 55.340000 M 4
2540 349.040645 N 721.041356 E 55.350000 M 4
2548 349.250681 N 721.776204 E 55.360000 M 4
2555 349.471976 N 722.515162 E 55.370000 M 4
2565 349.741258 N 723.381532 E 55.380000 M 4
2572 349.977503 N 724.110557 E 55.390000 M 4
2580 350.226114 N 724.859105 E 55.400000 M 4
2588 350.485983 N 725.610393 E 55.410000 M 4
2598 350.805283 N 726.483956 E 55.420000 M 4
2606 351.088592 N 727.230449 E 55.430000 M 4
2614 351.381499 N 727.980025 E 55.440000 M 4
2622 351.679389 N 728.729429 E 55.450000 M 4
2631 352.030803 N 729.603335 E 55.460000 M 4
2639 352.332200 N 730.333045 E 55.470000 M 4
2647 352.627137 N 731.084847 E 55.480000 M 4
2656 352.915244 N 731.838019 E 55.490000 M 4
2665 353.239896 N 732.721172 E 55.500000 M 4
2673 353.509548 N 733.481194 E 55.510000 M 4
2681 353.768494 N 734.242415 E 55.520000 M 4
2689 354.021165 N 735.006204 E 55.530000 M 4
2699 354.310935 N 735.896550 E 55.540000 M 4
2707 354.552901 N 736.642358 E 55.550000 M 4
2715 354.793575 N 737.409573 E 55.560000 M 4
2723 355.022068 N 738.180384 E 55.570000 M 4
2732 355.272525 N 739.083916 E 55.580000 M 4
2741 355.475917 N 739.861577 E 55.590000 M 4
2749 355.669158 N 740.640608 E 55.600000 M 4
2757 355.855200 N 741.420838 E 55.610000 M 4
2766 356.061546 N 742.332248 E 55.620000 M 4
2774 356.224702 N 743.099805 E 55.630000 M 4
2782 356.375124 N 743.885515 E 55.640000 M 4
2790 356.514840 N 744.672938 E 55.650000 M 4
2800 356.662493 N 745.592397 E 55.660000 M 4
2808 356.776555 N 746.378278 E 55.670000 M 4
2816 356.880466 N 747.160734 E 55.680000 M 4
2823 356.972934 N 747.934456 E 55.690000 M 4
2833 357.069831 N 748.833707 E 55.700000 M 4
2840 357.141812 N 749.585338 E 55.710000 M 4
2848 357.212501 N 750.350669 E 55.720000 M 4
2856 357.283559 N 751.110520 E 55.730000 M 4
2865 357.367721 N 751.992303 E 55.740000 M 4
2872 357.436933 N 752.745646 E 55.750000 M 4
2880 357.504485 N 753.494365 E 55.760000 M 4
2887 357.570190 N 754.239659 E 55.770000 M 4
2896 357.645309 N 755.104660 E 55.780000 M 4
2903 357.704924 N 755.833514 E 55.790000 M 4
2911 357.764354 N 756.566135 E 55.800000 M 4
2918 357.821200 N 757.295332 E 55.810000 M 4
2927 357.886721 N 758.139953 E 55.820000 M 4
2934 357.942460 N 758.858360 E 55.830000 M 4
2941 357.997830 N 759.570603 E 55.840000 M 4
2948 358.055046 N 760.277022 E 55.850000 M 4
2957 358.125735 N 761.092702 E 55.860000 M 4
2964 358.189041 N 761.777201 E 55.870000 M 4
2970 358.259361 N 762.458275 E 55.880000 M 4
2977 358.332633 N 763.129588 E 55.890000 M 4
2985 358.415688 N 763.900057 E 55.900000 M 4
2992 358.485639 N 764.548935 E 55.910000 M 4
2998 358.558727 N 765.188566 E 55.920000 M 4
3004 358.633476 N 765.813641 E 55.930000 M 4
3012 358.719669 N 766.525883 E 55.940000 M 4
3018 358.788143 N 767.108658 E 55.950000 M 4
3023 358.858647 N 767.686809 E 55.960000 M 4
3029 358.926937 N 768.245780 E 55.970000 M 4
3036 359.006116 N 768.877019 E 55.980000 M 4
3041 359.073852 N 769.400712 E 55.990000 M 4
3046 359.139557 N 769.909848 E 56.000000 M 4
3057 359.277059 N 770.956719 E 56.020000 M 4
3066 359.397212 N 771.852202 E 56.040000 M 4
3075 359.527146 N 772.769264 E 56.060000 M 4
3083 359.634933 N 773.557200 E 56.080000 M 4
3091 359.749364 N 774.348390 E 56.100000 M 4
3098 359.835741 N 774.998125 E 56.120000 M 4
3105 359.922672 N 775.656594 E 56.140000 M 4
3110 359.992438 N 776.199124 E 56.160000 M 4
3115 360.058697 N 776.715624 E 56.180000 M 4
3121 360.129017 N 777.278191 E 56.210000 M 4
3127 360.204320 N 777.846752 E 56.250000 M 4
3186 360.009972 N 776.423466 E 61.980000 M 4
3192 360.070509 N 776.937054 E 62.050000 M 4
3197 360.126618 N 777.453897 E 62.090000 M 4
3202 360.182541 N 777.967999 E 62.120000 M 4
3208 360.246955 N 778.566701 E 62.150000 M 4
3215 360.316905 N 779.262503 E 62.180000 M 4
3220 360.367292 N 779.763932 E 62.200000 M 4
3226 360.421924 N 780.343111 E 62.220000 M 4
3232 360.474340 N 780.922803 E 62.240000 M 4
3239 360.532848 N 781.584526 E 62.260000 M 4
3245 360.584711 N 782.207374 E 62.280000 M 4
3252 360.635836 N 782.841525 E 62.300000 M 4
3257 360.677732 N 783.391248 E 62.320000 M 4
3263 360.719444 N 783.958096 E 62.340000 M 4
3268 360.753220 N 784.490180 E 62.360000 M 4
3274 360.797700 N 785.073982 E 62.380000 M 4
3280 360.834429 N 785.627302 E 62.400000 M 4
3286 360.879094 N 786.237306 E 62.420000 M 4
3292 360.932803 N 786.809121 E 62.440000 M 4
3298 360.998693 N 787.404911 E 62.460000 M 4
3303 361.069013 N 787.933912 E 62.480000 M 4
3309 361.162957 N 788.480210 E 62.500000 M 4
3314 361.278865 N 788.975988 E 62.520000 M 4
3319 361.439437 N 789.489063 E 62.540000 M 4
3324 361.630279 N 789.953844 E 62.560000 M 4
3330 361.888303 N 790.453904 E 62.580000 M 4
3336 362.182870 N 790.915260 E 62.600000 M 4
3342 362.574151 N 791.393057 E 62.620000 M 4
3348 363.013234 N 791.800469 E 62.640000 M 4
3355 363.570993 N 792.185446 E 62.660000 M 4
3361 364.150716 N 792.483255 E 62.680000 M 4
3368 364.816262 N 792.730545 E 62.700000 M 4
3375 365.450247 N 792.903682 E 62.720000 M 4
3382 366.160273 N 793.015511 E 62.740000 M 4
3389 366.836339 N 793.055926 E 62.760000 M 4
3396 367.560393 N 793.009517 E 62.780000 M 4
3403 368.217449 N 792.898373 E 62.800000 M 4
3410 368.906988 N 792.713077 E 62.820000 M 4
3417 369.525655 N 792.494729 E 62.840000 M 4
3424 370.155210 N 792.171232 E 62.860000 M 4
3431 370.704110 N 791.821190 E 62.880000 M 4
3438 371.257809 N 791.382439 E 62.900000 M 4
3444 371.737866 N 790.943003 E 62.920000 M 4
3451 372.199651 N 790.402871 E 62.940000 M 4
3458 372.569337 N 789.858285 E 62.960000 M 4
3465 372.911892 N 789.233039 E 62.980000 M 4
3472 373.179328 N 788.642216 E 63.000000 M 4
3479 373.390841 N 787.953778 E 63.020000 M 4
3486 373.524836 N 787.296507 E 63.040000 M 4
3493 373.606045 N 786.569195 E 63.060000 M 4
3500 373.618596 N 785.905931 E 63.080000 M 4
3507 373.540340 N 785.172967 E 63.100000 M 4
3514 373.444181 N 784.513642 E 63.120000 M 4
3521 373.329934 N 783.789412 E 63.140000 M 4
3528 373.238389 N 783.122894 E 63.160000 M 4
3536 373.112331 N 782.367153 E 63.180000 M 4
3543 372.982950 N 781.654911 E 63.200000 M 4
3551 372.825699 N 780.864577 E 63.220000 M 4
3559 372.685429 N 780.134524 E 63.240000 M 4
3567 372.524487 N 779.310454 E 63.260000 M 4
3575 372.392338 N 778.531251 E 63.280000 M 4
3584 372.272001 N 777.684575 E 63.300000 M 4
3592 372.188577 N 776.942363 E 63.320000 M 4
3600 372.082636 N 776.141583 E 63.340000 M 4
3607 371.998842 N 775.414099 E 63.360000 M 4
3615 371.910066 N 774.637466 E 63.380000 M 4
3622 371.839193 N 773.944575 E 63.400000 M 4
3630 371.757061 N 773.190204 E 63.420000 M 4
3637 371.684157 N 772.504849 E 63.440000 M 4
3644 371.612361 N 771.775823 E 63.460000 M 4
3650 371.558652 N 771.131911 E 63.480000 M 4
3658 371.494054 N 770.434910 E 63.500000 M 4
3664 371.438499 N 769.804356 E 63.520000 M 4
3671 371.379069 N 769.136468 E 63.540000 M 4
3677 371.334219 N 768.544959 E 63.560000 M 4
3683 371.265745 N 767.911836 E 63.580000 M 4
3689 371.192657 N 767.347899 E 63.600000 M 4
3695 371.119015 N 766.758274 E 63.620000 M 4
3700 371.058477 N 766.239719 E 63.640000 M 4
3706 370.990742 N 765.690852 E 63.660000 M 4
3713 370.906210 N 764.982720 E 63.690000 M 4
3720 370.841612 N 764.321168 E 63.720000 M 4
3725 370.775722 N 763.772644 E 63.750000 M 4
3732 370.720167 N 763.123252 E 63.790000 M 4
3737 370.662583 N 762.589284 E 63.830000 M 4
3743 370.613488 N 762.024662 E 63.900000 M 4
3777 370.988711 N 759.407569 E 66.060000 M 4
3778 370.993695 N 759.363044 E 66.110000 M 4

View File

@ -1,34 +0,0 @@
DATE:2022-03-10
START_TIME:10:46
STOP_TIME:
UNITS:m
MODE:距离模式
ANTENNAS:200 MHz shielded
FREQUENCY:5351.611816
STACKS:2
LAST_TRACE:60448
POSITIVE_DIRECTION:1
SAMPLES:516
TIME_INTERVAL:0.000000
TIMEWINDOW:96.419553
DEPTH:
ZERO_POSITION:
DIELECTRIC:
SOIL_TYPE:
BITS:16
MARK:
DISTANCE_INTERVAL:0.099194
START_POSITION:0.000000
STOP_POSITION:374.656262
WHEEL_GPS:
WHEEL_CALIBRATION:84.4270000000
SCAN_SECOND:
NUMBER_OF_CH:16
CH_X_OFFSETS:0.080 0.160 0.240 0.320 0.400 0.480 0.560 0.640 0.720 0.800 0.880 0.960 1.040 1.120 1.200 1.280
RTK_X_OFFSET:
RTK_Y_OFFSET:0.350
RTK_Z_OFFSET:
GAIN:
FILTER:
SMOOTH:
ENDIAN_TYPE:1

View File

@ -1,335 +0,0 @@
VERSION:1
1 353.073787 N 657.340790 E 70.990000 M 4
6 352.968400 N 656.889195 E 71.210000 M 4
12 352.865227 N 656.326114 E 71.260000 M 4
20 352.732339 N 655.609419 E 71.300000 M 4
26 352.607388 N 654.979550 E 71.330000 M 4
32 352.505138 N 654.409448 E 71.350000 M 4
38 352.405472 N 653.834208 E 71.370000 M 4
45 352.293625 N 653.205537 E 71.390000 M 4
52 352.181409 N 652.526518 E 71.410000 M 4
60 352.051659 N 651.717346 E 71.430000 M 4
69 351.922278 N 650.910572 E 71.450000 M 4
74 351.843837 N 650.416678 E 71.460000 M 4
82 351.710211 N 649.585243 E 71.480000 M 4
93 351.561635 N 648.594200 E 71.500000 M 4
102 351.433177 N 647.654191 E 71.520000 M 4
113 351.288847 N 646.606635 E 71.540000 M 4
123 351.151714 N 645.635800 E 71.560000 M 4
129 351.082871 N 645.102004 E 71.570000 M 4
135 351.000923 N 644.463571 E 71.580000 M 4
141 350.930050 N 643.902888 E 71.590000 M 4
147 350.861022 N 643.329532 E 71.600000 M 4
153 350.791625 N 642.744360 E 71.610000 M 4
160 350.708755 N 642.054894 E 71.620000 M 4
166 350.640650 N 641.513391 E 71.630000 M 4
172 350.569592 N 640.933870 E 71.640000 M 4
178 350.501671 N 640.358973 E 71.650000 M 4
185 350.423046 N 639.688174 E 71.660000 M 4
191 350.354018 N 639.112249 E 71.670000 M 4
197 350.284252 N 638.535126 E 71.680000 M 4
203 350.215594 N 637.959372 E 71.690000 M 4
210 350.132908 N 637.288402 E 71.700000 M 4
215 350.071817 N 636.760085 E 71.710000 M 4
221 350.003527 N 636.185531 E 71.720000 M 4
227 349.933392 N 635.611318 E 71.730000 M 4
234 349.845723 N 634.942574 E 71.740000 M 4
240 349.770789 N 634.369903 E 71.750000 M 4
246 349.701946 N 633.796547 E 71.760000 M 4
252 349.635502 N 633.223363 E 71.770000 M 4
259 349.552632 N 632.555132 E 71.780000 M 4
264 349.491540 N 632.031782 E 71.790000 M 4
270 349.415684 N 631.468359 E 71.800000 M 4
276 349.336874 N 630.907847 E 71.810000 M 4
283 349.241822 N 630.250406 E 71.820000 M 4
288 349.159506 N 629.681503 E 71.830000 M 4
294 349.075528 N 629.108489 E 71.840000 M 4
300 348.984721 N 628.531708 E 71.850000 M 4
307 348.873982 N 627.852175 E 71.860000 M 4
313 348.787605 N 627.312385 E 71.870000 M 4
319 348.691076 N 626.721218 E 71.880000 M 4
325 348.597686 N 626.122860 E 71.890000 M 4
333 348.489899 N 625.418837 E 71.900000 M 4
339 348.392448 N 624.806436 E 71.910000 M 4
346 348.295735 N 624.180334 E 71.920000 M 4
352 348.200499 N 623.545840 E 71.930000 M 4
360 348.094558 N 622.788730 E 71.940000 M 4
366 348.016487 N 622.177698 E 71.950000 M 4
373 347.929556 N 621.511865 E 71.960000 M 4
380 347.837827 N 620.836100 E 71.970000 M 4
389 347.729117 N 620.036004 E 71.980000 M 4
396 347.638126 N 619.339346 E 71.990000 M 4
403 347.548980 N 618.633098 E 72.000000 M 4
410 347.460942 N 617.916916 E 72.010000 M 4
419 347.359062 N 617.070583 E 72.020000 M 4
426 347.278037 N 616.383515 E 72.030000 M 4
434 347.187784 N 615.640104 E 72.040000 M 4
442 347.100669 N 614.888302 E 72.050000 M 4
451 347.000634 N 614.000525 E 72.060000 M 4
459 346.915180 N 613.231256 E 72.070000 M 4
467 346.830833 N 612.453423 E 72.080000 M 4
475 346.745748 N 611.669597 E 72.090000 M 4
485 346.645344 N 610.746028 E 72.100000 M 4
492 346.568749 N 609.997138 E 72.110000 M 4
501 346.482187 N 609.192932 E 72.120000 M 4
509 346.393226 N 608.382219 E 72.130000 M 4
519 346.288762 N 607.429537 E 72.140000 M 4
527 346.195740 N 606.608549 E 72.150000 M 4
536 346.101058 N 605.782081 E 72.160000 M 4
544 346.006745 N 604.949447 E 72.170000 M 4
554 345.894344 N 603.973304 E 72.180000 M 4
563 345.804829 N 603.179716 E 72.190000 M 4
571 345.706455 N 602.332184 E 72.200000 M 4
580 345.606051 N 601.480199 E 72.210000 M 4
591 345.489405 N 600.482306 E 72.220000 M 4
599 345.387525 N 599.622443 E 72.230000 M 4
608 345.285091 N 598.759327 E 72.240000 M 4
617 345.183579 N 597.891072 E 72.250000 M 4
628 345.062504 N 596.877424 E 72.260000 M 4
636 344.967083 N 596.055751 E 72.270000 M 4
645 344.860773 N 595.188011 E 72.280000 M 4
654 344.752617 N 594.319414 E 72.290000 M 4
665 344.629511 N 593.303882 E 72.300000 M 4
674 344.520986 N 592.432717 E 72.310000 M 4
683 344.412830 N 591.563264 E 72.320000 M 4
692 344.300799 N 590.698264 E 72.330000 M 4
702 344.172341 N 589.691123 E 72.340000 M 4
711 344.072121 N 588.877328 E 72.350000 M 4
719 343.966549 N 588.015752 E 72.360000 M 4
728 343.859870 N 587.155205 E 72.370000 M 4
739 343.736764 N 586.151832 E 72.380000 M 4
748 343.630454 N 585.294709 E 72.390000 M 4
757 343.525436 N 584.437244 E 72.400000 M 4
765 343.422632 N 583.581834 E 72.410000 M 4
776 343.303956 N 582.585482 E 72.420000 M 4
784 343.207613 N 581.779051 E 72.430000 M 4
793 343.106839 N 580.924497 E 72.440000 M 4
802 343.004405 N 580.072512 E 72.450000 M 4
812 342.886836 N 579.078044 E 72.460000 M 4
821 342.789201 N 578.226059 E 72.470000 M 4
830 342.692673 N 577.375102 E 72.480000 M 4
838 342.595591 N 576.519863 E 72.490000 M 4
849 342.480052 N 575.522826 E 72.500000 M 4
857 342.389246 N 574.717765 E 72.510000 M 4
866 342.292718 N 573.862526 E 72.520000 M 4
875 342.199143 N 573.007287 E 72.530000 M 4
885 342.092832 N 572.008880 E 72.540000 M 4
894 342.003133 N 571.152614 E 72.550000 M 4
903 341.914357 N 570.294464 E 72.560000 M 4
911 341.827242 N 569.435800 E 72.570000 M 4
922 341.726838 N 568.432598 E 72.580000 M 4
930 341.650796 N 567.620686 E 72.590000 M 4
939 341.568849 N 566.757056 E 72.600000 M 4
948 341.486348 N 565.893940 E 72.610000 M 4
958 341.390004 N 564.886628 E 72.620000 M 4
967 341.307688 N 564.019573 E 72.630000 M 4
976 341.227032 N 563.148578 E 72.640000 M 4
985 341.146007 N 562.274673 E 72.650000 M 4
996 341.048741 N 561.254860 E 72.660000 M 4
1004 340.971592 N 560.427364 E 72.670000 M 4
1013 340.882631 N 559.548321 E 72.680000 M 4
1023 340.792563 N 558.665510 E 72.690000 M 4
1033 340.688099 N 557.629599 E 72.700000 M 4
1042 340.595631 N 556.738397 E 72.710000 M 4
1052 340.500395 N 555.845996 E 72.720000 M 4
1061 340.402575 N 554.950856 E 72.730000 M 4
1072 340.285744 N 553.904841 E 72.740000 M 4
1080 340.192538 N 553.054739 E 72.750000 M 4
1090 340.087151 N 552.154461 E 72.760000 M 4
1099 339.980102 N 551.253497 E 72.770000 M 4
1110 339.853490 N 550.198920 E 72.780000 M 4
1119 339.742935 N 549.291449 E 72.790000 M 4
1129 339.629796 N 548.380210 E 72.800000 M 4
1138 339.513150 N 547.467944 E 72.810000 M 4
1149 339.377309 N 546.403091 E 72.820000 M 4
1158 339.271737 N 545.537748 E 72.830000 M 4
1168 339.155460 N 544.622742 E 72.840000 M 4
1177 339.037707 N 543.705167 E 72.850000 M 4
1188 338.897621 N 542.637231 E 72.860000 M 4
1198 338.777469 N 541.721540 E 72.870000 M 4
1207 338.658054 N 540.802938 E 72.880000 M 4
1217 338.541962 N 539.884335 E 72.890000 M 4
1228 338.411474 N 538.812290 E 72.900000 M 4
1237 338.308670 N 537.941295 E 72.910000 M 4
1246 338.199407 N 537.017042 E 72.920000 M 4
1256 338.093835 N 536.091931 E 72.930000 M 4
1267 337.977189 N 535.017146 E 72.940000 M 4
1276 337.881769 N 534.103681 E 72.950000 M 4
1286 337.786163 N 533.197237 E 72.960000 M 4
1295 337.689820 N 532.296103 E 72.970000 M 4
1306 337.577050 N 531.253170 E 72.980000 M 4
1315 337.487350 N 530.417968 E 72.990000 M 4
1324 337.393960 N 529.539267 E 73.000000 M 4
1333 337.296693 N 528.666218 E 73.010000 M 4
1343 337.180232 N 527.657365 E 73.020000 M 4
1352 337.077429 N 526.801784 E 73.030000 M 4
1361 336.972041 N 525.953566 E 73.040000 M 4
1369 336.865178 N 525.110144 E 73.050000 M 4
1379 336.739119 N 524.136055 E 73.060000 M 4
1388 336.633731 N 523.357024 E 73.070000 M 4
1396 336.523915 N 522.529186 E 73.080000 M 4
1405 336.412067 N 521.703060 E 73.090000 M 4
1415 336.281025 N 520.741815 E 73.100000 M 4
1423 336.168440 N 519.919800 E 73.110000 M 4
1432 336.053640 N 519.099839 E 73.120000 M 4
1440 335.938101 N 518.281077 E 73.130000 M 4
1450 335.802630 N 517.330108 E 73.140000 M 4
1458 335.689675 N 516.570771 E 73.150000 M 4
1466 335.576720 N 515.772902 E 73.160000 M 4
1474 335.464504 N 514.989418 E 73.170000 M 4
1484 335.333093 N 514.092736 E 73.180000 M 4
1491 335.217370 N 513.339564 E 73.190000 M 4
1499 335.093157 N 512.596496 E 73.200000 M 4
1507 334.961192 N 511.864560 E 73.210000 M 4
1516 334.795082 N 511.026275 E 73.220000 M 4
1522 334.650936 N 510.364723 E 73.230000 M 4
1530 334.487410 N 509.666353 E 73.240000 M 4
1537 334.316502 N 508.980826 E 73.250000 M 4
1545 334.112003 N 508.192033 E 73.260000 M 4
1553 333.931681 N 507.525173 E 73.270000 M 4
1560 333.746746 N 506.867047 E 73.280000 M 4
1567 333.554797 N 506.216284 E 73.290000 M 4
1575 333.325381 N 505.464825 E 73.300000 M 4
1581 333.133617 N 504.876398 E 73.310000 M 4
1588 332.934101 N 504.242248 E 73.320000 M 4
1595 332.737538 N 503.612721 E 73.330000 M 4
1602 332.514951 N 502.886607 E 73.340000 M 4
1609 332.332784 N 502.269410 E 73.350000 M 4
1615 332.159661 N 501.657864 E 73.360000 M 4
1622 331.997058 N 501.050772 E 73.370000 M 4
1629 331.820797 N 500.346749 E 73.380000 M 4
1635 331.679419 N 499.798910 E 73.390000 M 4
1641 331.537303 N 499.205689 E 73.400000 M 4
1648 331.400540 N 498.619489 E 73.410000 M 4
1655 331.248088 N 497.947149 E 73.420000 M 4
1661 331.123136 N 497.382355 E 73.430000 M 4
1666 331.002061 N 496.832804 E 73.440000 M 4
1672 330.888184 N 496.298493 E 73.450000 M 4
1678 330.764524 N 495.693455 E 73.460000 M 4
1688 330.576082 N 494.735465 E 73.480000 M 4
1694 330.488598 N 494.234206 E 73.490000 M 4
1700 330.391147 N 493.650233 E 73.500000 M 4
1705 330.311783 N 493.151201 E 73.510000 M 4
1710 330.237957 N 492.652340 E 73.520000 M 4
1715 330.170959 N 492.154849 E 73.530000 M 4
1721 330.098794 N 491.575157 E 73.540000 M 4
1731 329.995437 N 490.635833 E 73.560000 M 4
1742 329.888019 N 489.570295 E 73.580000 M 4
1752 329.808286 N 488.589013 E 73.600000 M 4
1763 329.717849 N 487.534265 E 73.620000 M 4
1772 329.633502 N 486.619943 E 73.640000 M 4
1783 329.526454 N 485.583176 E 73.660000 M 4
1793 329.428449 N 484.639057 E 73.680000 M 4
1803 329.315310 N 483.626265 E 73.700000 M 4
1812 329.212507 N 482.752359 E 73.720000 M 4
1822 329.091431 N 481.763714 E 73.740000 M 4
1832 328.988074 N 480.864635 E 73.760000 M 4
1842 328.873089 N 479.906473 E 73.780000 M 4
1850 328.770839 N 479.086683 E 73.800000 M 4
1860 328.648657 N 478.158662 E 73.820000 M 4
1868 328.550652 N 477.315068 E 73.840000 M 4
1878 328.435852 N 476.418043 E 73.860000 M 4
1886 328.331572 N 475.654939 E 73.880000 M 4
1895 328.220463 N 474.774354 E 73.900000 M 4
1903 328.112676 N 473.968779 E 73.920000 M 4
1912 327.989940 N 473.098299 E 73.940000 M 4
1920 327.882337 N 472.355573 E 73.960000 M 4
1929 327.746866 N 471.487833 E 73.980000 M 4
1937 327.621176 N 470.687566 E 74.000000 M 4
1946 327.482198 N 469.820682 E 74.020000 M 4
1954 327.357985 N 469.074360 E 74.040000 M 4
1963 327.215684 N 468.196687 E 74.060000 M 4
1972 327.086119 N 467.382892 E 74.080000 M 4
1981 326.957660 N 466.503849 E 74.100000 M 4
1988 326.857810 N 465.752047 E 74.120000 M 4
1998 326.746701 N 464.878313 E 74.140000 M 4
2006 326.648143 N 464.075991 E 74.160000 M 4
2015 326.548662 N 463.211333 E 74.180000 M 4
2022 326.463023 N 462.480082 E 74.200000 M 4
2031 326.370555 N 461.641968 E 74.220000 M 4
2039 326.284363 N 460.886056 E 74.240000 M 4
2047 326.188942 N 460.089558 E 74.260000 M 4
2054 326.113085 N 459.427321 E 74.280000 M 4
2062 326.027262 N 458.675690 E 74.300000 M 4
2068 325.945868 N 458.019962 E 74.320000 M 4
2075 325.868535 N 457.367144 E 74.340000 M 4
2083 325.776990 N 456.616884 E 74.370000 M 4
2088 325.713499 N 456.094390 E 74.390000 M 4
2095 325.629891 N 455.419652 E 74.420000 M 4
2102 325.549420 N 454.751935 E 74.460000 M 4
2109 325.467657 N 454.115558 E 74.500000 M 4
2115 325.401398 N 453.546484 E 74.540000 M 4
2121 325.324064 N 452.941617 E 74.580000 M 4
2127 325.260389 N 452.397203 E 74.620000 M 4
2133 325.185086 N 451.818709 E 74.660000 M 4
2138 325.118458 N 451.303580 E 74.700000 M 4
2144 325.046477 N 450.752658 E 74.740000 M 4
2150 324.968590 N 450.132378 E 74.790000 M 4
2156 324.906391 N 449.584197 E 74.830000 M 4
2161 324.841424 N 449.067868 E 74.860000 M 4
2167 324.772211 N 448.522769 E 74.890000 M 4
2173 324.706506 N 447.933486 E 74.920000 M 4
2180 324.619760 N 447.229293 E 74.950000 M 4
2188 324.521939 N 446.459509 E 74.980000 M 4
2196 324.433532 N 445.725175 E 75.010000 M 4
2202 324.354538 N 445.151991 E 75.030000 M 4
2207 324.279973 N 444.623503 E 75.050000 M 4
2213 324.215006 N 444.098612 E 75.070000 M 4
2218 324.136750 N 443.580571 E 75.090000 M 4
2224 324.054618 N 443.029991 E 75.110000 M 4
2229 323.978207 N 442.528562 E 75.130000 M 4
2234 323.908072 N 442.032955 E 75.150000 M 4
2239 323.831662 N 441.538204 E 75.170000 M 4
2245 323.745285 N 440.994475 E 75.190000 M 4
2250 323.664814 N 440.483455 E 75.210000 M 4
2256 323.584158 N 439.957194 E 75.230000 M 4
2261 323.497228 N 439.420829 E 75.250000 M 4
2267 323.397377 N 438.828635 E 75.270000 M 4
2273 323.303987 N 438.280111 E 75.290000 M 4
2279 323.224070 N 437.729874 E 75.310000 M 4
2284 323.137693 N 437.191111 E 75.330000 M 4
2290 323.050024 N 436.618783 E 75.350000 M 4
2296 322.972875 N 436.097830 E 75.370000 M 4
2301 322.909199 N 435.584584 E 75.390000 M 4
2306 322.834450 N 435.076304 E 75.410000 M 4
2312 322.757301 N 434.526924 E 75.430000 M 4
2317 322.691042 N 434.019843 E 75.450000 M 4
2323 322.641948 N 433.507796 E 75.470000 M 4
2328 322.576057 N 432.988213 E 75.490000 M 4
2334 322.505184 N 432.415200 E 75.510000 M 4
2339 322.445015 N 431.878835 E 75.530000 M 4
2345 322.397028 N 431.335448 E 75.550000 M 4
2351 322.335198 N 430.793431 E 75.570000 M 4
2357 322.276875 N 430.208601 E 75.590000 M 4
2362 322.220952 N 429.675490 E 75.610000 M 4
2368 322.180901 N 429.153167 E 75.630000 M 4
2373 322.130145 N 428.643689 E 75.650000 M 4
2378 322.063517 N 428.106125 E 75.670000 M 4
2386 321.937827 N 427.349186 E 75.700000 M 4
2393 321.795158 N 426.707671 E 75.730000 M 4
2400 321.570356 N 426.036358 E 75.760000 M 4
2407 321.267852 N 425.470366 E 75.790000 M 4
2414 320.834490 N 424.917218 E 75.820000 M 4
2421 320.322688 N 424.475213 E 75.850000 M 4
2428 319.740012 N 424.136474 E 75.880000 M 4
2435 319.108057 N 423.819998 E 75.910000 M 4
2443 318.415196 N 423.547020 E 75.940000 M 4
2451 317.681545 N 423.384158 E 75.970000 M 4
2457 317.082996 N 423.281578 E 75.990000 M 4
2463 316.485555 N 423.237394 E 76.010000 M 4
2470 315.810781 N 423.289113 E 76.030000 M 4
2477 315.149295 N 423.366348 E 76.050000 M 4
2485 314.418228 N 423.528354 E 76.070000 M 4
2492 313.752313 N 423.752182 E 76.090000 M 4
2500 313.075509 N 424.110272 E 76.110000 M 4
2507 312.489142 N 424.474699 E 76.130000 M 4
2514 311.918278 N 424.924410 E 76.150000 M 4
2521 311.451510 N 425.368813 E 76.170000 M 4
2528 311.022578 N 425.910829 E 76.190000 M 4
2534 310.677070 N 426.397360 E 76.210000 M 4
2540 310.360908 N 426.942973 E 76.230000 M 4
2546 310.138137 N 427.436011 E 76.250000 M 4
2552 309.975165 N 427.957135 E 76.270000 M 4
2558 309.806471 N 428.539738 E 76.300000 M 4
2590 312.454813 N 427.648879 E 77.810000 M 4
2591 312.442077 N 427.651961 E 77.860000 M 4

View File

@ -1,34 +0,0 @@
DATE:2022-03-10
START_TIME:10:48
STOP_TIME:
UNITS:m
MODE:距离模式
ANTENNAS:200 MHz shielded
FREQUENCY:5351.611816
STACKS:2
LAST_TRACE:41456
POSITIVE_DIRECTION:1
SAMPLES:516
TIME_INTERVAL:0.000000
TIMEWINDOW:96.419553
DEPTH:
ZERO_POSITION:
DIELECTRIC:
SOIL_TYPE:
BITS:16
MARK:
DISTANCE_INTERVAL:0.097424
START_POSITION:0.000000
STOP_POSITION:252.327351
WHEEL_GPS:
WHEEL_CALIBRATION:84.4270000000
SCAN_SECOND:
NUMBER_OF_CH:16
CH_X_OFFSETS:0.080 0.160 0.240 0.320 0.400 0.480 0.560 0.640 0.720 0.800 0.880 0.960 1.040 1.120 1.200 1.280
RTK_X_OFFSET:
RTK_Y_OFFSET:0.350
RTK_Z_OFFSET:
GAIN:
FILTER:
SMOOTH:
ENDIAN_TYPE:1

View File

@ -1,346 +0,0 @@
VERSION:1
1 320.505224 N 474.820250 E 80.150000 M 4
6 320.535862 N 475.242047 E 80.420000 M 4
12 320.603598 N 475.800162 E 80.470000 M 4
19 320.687944 N 476.438080 E 80.510000 M 4
25 320.765647 N 477.048427 E 80.540000 M 4
33 320.863467 N 477.741146 E 80.570000 M 4
39 320.958334 N 478.306624 E 80.590000 M 4
45 321.046372 N 478.856176 E 80.610000 M 4
51 321.162649 N 479.497006 E 80.630000 M 4
58 321.279110 N 480.146398 E 80.650000 M 4
67 321.422149 N 480.924916 E 80.670000 M 4
75 321.550607 N 481.696240 E 80.690000 M 4
84 321.703613 N 482.604568 E 80.710000 M 4
94 321.861048 N 483.510840 E 80.730000 M 4
100 321.957207 N 484.071523 E 80.740000 M 4
105 322.049859 N 484.582029 E 80.750000 M 4
111 322.130145 N 485.104523 E 80.760000 M 4
117 322.209693 N 485.646197 E 80.770000 M 4
124 322.305114 N 486.304324 E 80.780000 M 4
130 322.389645 N 486.892921 E 80.790000 M 4
136 322.477684 N 487.503610 E 80.800000 M 4
143 322.568306 N 488.136220 E 80.810000 M 4
151 322.677015 N 488.905661 E 80.820000 M 4
158 322.785540 N 489.594613 E 80.830000 M 4
166 322.880223 N 490.297436 E 80.840000 M 4
173 322.975459 N 491.012932 E 80.850000 M 4
182 323.086752 N 491.842826 E 80.860000 M 4
190 323.181435 N 492.548560 E 80.870000 M 4
197 323.275010 N 493.251384 E 80.880000 M 4
204 323.369692 N 493.952323 E 80.890000 M 4
213 323.476002 N 494.771085 E 80.900000 M 4
220 323.581021 N 495.475793 E 80.910000 M 4
228 323.669797 N 496.172965 E 80.920000 M 4
235 323.761342 N 496.868424 E 80.930000 M 4
244 323.870236 N 497.677596 E 80.940000 M 4
251 323.962335 N 498.369117 E 80.950000 M 4
258 324.052587 N 499.057555 E 80.960000 M 4
266 324.142287 N 499.745650 E 80.970000 M 4
274 324.246013 N 500.545917 E 80.980000 M 4
281 324.350662 N 501.228704 E 80.990000 M 4
288 324.439254 N 501.898989 E 81.000000 M 4
295 324.523785 N 502.565678 E 81.010000 M 4
304 324.624005 N 503.349333 E 81.020000 M 4
311 324.710566 N 504.022872 E 81.030000 M 4
318 324.797128 N 504.702234 E 81.040000 M 4
325 324.883505 N 505.386904 E 81.050000 M 4
334 324.985755 N 506.198816 E 81.060000 M 4
341 325.086159 N 506.905921 E 81.070000 M 4
349 325.174012 N 507.620047 E 81.080000 M 4
356 325.265188 N 508.337941 E 81.090000 M 4
365 325.372052 N 509.173828 E 81.100000 M 4
372 325.459905 N 509.880248 E 81.110000 M 4
380 325.546651 N 510.581188 E 81.120000 M 4
387 325.632844 N 511.279901 E 81.130000 M 4
396 325.734909 N 512.095238 E 81.140000 M 4
403 325.833467 N 512.793951 E 81.150000 M 4
411 325.920029 N 513.489239 E 81.160000 M 4
418 326.006406 N 514.186583 E 81.170000 M 4
427 326.106441 N 515.003118 E 81.180000 M 4
434 326.191157 N 515.710223 E 81.190000 M 4
442 326.276057 N 516.424863 E 81.200000 M 4
449 326.362619 N 517.147209 E 81.210000 M 4
458 326.466714 N 517.997653 E 81.220000 M 4
466 326.566011 N 518.736953 E 81.230000 M 4
474 326.655341 N 519.478480 E 81.240000 M 4
482 326.744671 N 520.227884 E 81.250000 M 4
491 326.853011 N 521.110866 E 81.260000 M 4
499 326.948432 N 521.872601 E 81.270000 M 4
507 327.047360 N 522.642213 E 81.280000 M 4
515 327.146656 N 523.416963 E 81.290000 M 4
525 327.261457 N 524.324434 E 81.300000 M 4
533 327.371458 N 525.111514 E 81.310000 M 4
541 327.474077 N 525.895340 E 81.320000 M 4
550 327.579095 N 526.679851 E 81.330000 M 4
559 327.701647 N 527.599139 E 81.340000 M 4
568 327.804820 N 528.389130 E 81.350000 M 4
576 327.908915 N 529.178779 E 81.360000 M 4
584 328.013010 N 529.973394 E 81.370000 M 4
594 328.134455 N 530.901416 E 81.380000 M 4
603 328.246856 N 531.705964 E 81.390000 M 4
611 328.351320 N 532.509142 E 81.400000 M 4
620 328.457077 N 533.315060 E 81.410000 M 4
630 328.580183 N 534.260035 E 81.420000 M 4
638 328.682801 N 535.075372 E 81.430000 M 4
647 328.783205 N 535.897730 E 81.440000 M 4
656 328.878811 N 536.722828 E 81.450000 M 4
666 328.992504 N 537.694006 E 81.460000 M 4
675 329.098075 N 538.537942 E 81.470000 M 4
684 329.194419 N 539.379309 E 81.480000 M 4
693 329.286518 N 540.224958 E 81.490000 M 4
703 329.386183 N 541.215316 E 81.500000 M 4
712 329.469607 N 542.063190 E 81.510000 M 4
721 329.559307 N 542.915518 E 81.520000 M 4
730 329.652882 N 543.771271 E 81.530000 M 4
740 329.762329 N 544.772075 E 81.540000 M 4
750 329.870670 N 545.638788 E 81.550000 M 4
759 329.958339 N 546.506185 E 81.560000 M 4
768 330.046561 N 547.367418 E 81.570000 M 4
778 330.154717 N 548.373189 E 81.580000 M 4
787 330.254937 N 549.239388 E 81.590000 M 4
796 330.359955 N 550.101134 E 81.600000 M 4
806 330.464235 N 550.967162 E 81.610000 M 4
816 330.574975 N 551.984406 E 81.620000 M 4
826 330.684607 N 552.861052 E 81.630000 M 4
835 330.773937 N 553.738211 E 81.640000 M 4
844 330.866589 N 554.618624 E 81.650000 M 4
855 330.979175 N 555.647343 E 81.660000 M 4
864 331.074965 N 556.531523 E 81.670000 M 4
874 331.168171 N 557.418615 E 81.680000 M 4
883 331.256209 N 558.309475 E 81.690000 M 4
894 331.351814 N 559.353606 E 81.700000 M 4
903 331.450188 N 560.253371 E 81.710000 M 4
913 331.524568 N 561.154505 E 81.720000 M 4
922 331.595811 N 562.059065 E 81.730000 M 4
933 331.676651 N 563.119808 E 81.740000 M 4
943 331.742356 N 564.034300 E 81.750000 M 4
953 331.806216 N 564.952389 E 81.760000 M 4
962 331.870999 N 565.872876 E 81.770000 M 4
974 331.951654 N 566.949374 E 81.780000 M 4
983 332.040800 N 567.874826 E 81.790000 M 4
993 332.114442 N 568.802848 E 81.800000 M 4
1003 332.190852 N 569.730527 E 81.810000 M 4
1014 332.283505 N 570.815930 E 81.820000 M 4
1024 332.366559 N 571.747205 E 81.830000 M 4
1034 332.453859 N 572.679337 E 81.840000 M 4
1044 332.544481 N 573.613181 E 81.850000 M 4
1055 332.654483 N 574.700982 E 81.860000 M 4
1065 332.766330 N 575.633285 E 81.870000 M 4
1075 332.865811 N 576.568327 E 81.880000 M 4
1085 332.967507 N 577.504227 E 81.890000 M 4
1096 333.090059 N 578.596480 E 81.900000 M 4
1106 333.200061 N 579.533578 E 81.910000 M 4
1116 333.310247 N 580.471018 E 81.920000 M 4
1126 333.420986 N 581.408630 E 81.930000 M 4
1137 333.547968 N 582.505507 E 81.940000 M 4
1147 333.672550 N 583.444146 E 81.950000 M 4
1157 333.784767 N 584.384841 E 81.960000 M 4
1167 333.895875 N 585.328617 E 81.970000 M 4
1179 334.022119 N 586.432345 E 81.980000 M 4
1189 334.127875 N 587.382115 E 81.990000 M 4
1199 334.233816 N 588.333085 E 82.000000 M 4
1209 334.340126 N 589.282684 E 82.010000 M 4
1220 334.463417 N 590.387439 E 82.020000 M 4
1230 334.579693 N 591.325222 E 82.030000 M 4
1240 334.682312 N 592.262320 E 82.040000 M 4
1250 334.785485 N 593.196335 E 82.050000 M 4
1261 334.903792 N 594.284821 E 82.060000 M 4
1271 335.003089 N 595.216439 E 82.070000 M 4
1281 335.100539 N 596.146173 E 82.080000 M 4
1291 335.197437 N 597.074366 E 82.090000 M 4
1302 335.308730 N 598.153604 E 82.100000 M 4
1312 335.419101 N 599.076830 E 82.110000 M 4
1321 335.513968 N 599.998515 E 82.120000 M 4
1331 335.609942 N 600.917289 E 82.130000 M 4
1342 335.719575 N 601.982484 E 82.140000 M 4
1352 335.811858 N 602.886873 E 82.150000 M 4
1361 335.905064 N 603.788179 E 82.160000 M 4
1371 336.000854 N 604.684176 E 82.170000 M 4
1382 336.115469 N 605.727280 E 82.180000 M 4
1391 336.226209 N 606.619167 E 82.190000 M 4
1401 336.325506 N 607.513965 E 82.200000 M 4
1410 336.424249 N 608.409620 E 82.210000 M 4
1421 336.541633 N 609.457861 E 82.220000 M 4
1431 336.643698 N 610.360195 E 82.230000 M 4
1440 336.744656 N 611.269378 E 82.240000 M 4
1450 336.847828 N 612.181473 E 82.250000 M 4
1461 336.970011 N 613.253005 E 82.260000 M 4
1471 337.088503 N 614.173320 E 82.270000 M 4
1480 337.195367 N 615.103054 E 82.280000 M 4
1490 337.302230 N 616.038439 E 82.290000 M 4
1502 337.429581 N 617.139427 E 82.300000 M 4
1512 337.537552 N 618.090225 E 82.310000 M 4
1522 337.645155 N 619.045133 E 82.320000 M 4
1532 337.752941 N 620.007063 E 82.330000 M 4
1544 337.879000 N 621.136649 E 82.340000 M 4
1554 338.002290 N 622.106114 E 82.350000 M 4
1565 338.111000 N 623.086710 E 82.360000 M 4
1575 338.220078 N 624.074157 E 82.370000 M 4
1587 338.345584 N 625.233884 E 82.380000 M 4
1598 338.454478 N 626.231948 E 82.390000 M 4
1608 338.564110 N 627.234465 E 82.400000 M 4
1619 338.673373 N 628.244345 E 82.410000 M 4
1631 338.798878 N 629.427363 E 82.420000 M 4
1642 338.917739 N 630.443580 E 82.430000 M 4
1653 339.024787 N 631.463564 E 82.440000 M 4
1664 339.130175 N 632.489542 E 82.450000 M 4
1676 339.255126 N 633.691569 E 82.460000 M 4
1687 339.364574 N 634.724911 E 82.470000 M 4
1698 339.476421 N 635.763049 E 82.480000 M 4
1709 339.595651 N 636.806152 E 82.490000 M 4
1722 339.734998 N 638.029072 E 82.500000 M 4
1733 339.868809 N 639.080225 E 82.510000 M 4
1744 339.987485 N 640.136001 E 82.520000 M 4
1755 340.113359 N 641.191778 E 82.530000 M 4
1768 340.273194 N 642.431480 E 82.540000 M 4
1780 340.414940 N 643.495477 E 82.550000 M 4
1791 340.552996 N 644.570776 E 82.560000 M 4
1802 340.678501 N 645.645390 E 82.570000 M 4
1815 340.825047 N 646.908041 E 82.580000 M 4
1827 340.962918 N 647.991731 E 82.590000 M 4
1838 341.101896 N 649.079875 E 82.600000 M 4
1850 341.245673 N 650.168703 E 82.610000 M 4
1863 341.409937 N 651.444882 E 82.620000 M 4
1875 341.548731 N 652.543129 E 82.630000 M 4
1887 341.682357 N 653.648569 E 82.640000 M 4
1898 341.816537 N 654.757092 E 82.650000 M 4
1912 341.975264 N 656.051938 E 82.660000 M 4
1924 342.124762 N 657.156693 E 82.670000 M 4
1935 342.264848 N 658.273093 E 82.680000 M 4
1947 342.404934 N 659.390349 E 82.690000 M 4
1961 342.564768 N 660.697183 E 82.700000 M 4
1973 342.699133 N 661.818378 E 82.710000 M 4
1985 342.829436 N 662.944711 E 82.720000 M 4
1996 342.963616 N 664.069502 E 82.730000 M 4
2010 343.123266 N 665.383529 E 82.740000 M 4
2022 343.274979 N 666.500443 E 82.750000 M 4
2034 343.408790 N 667.620781 E 82.760000 M 4
2046 343.540016 N 668.741463 E 82.770000 M 4
2059 343.692099 N 670.047098 E 82.780000 M 4
2071 343.824802 N 671.162642 E 82.790000 M 4
2083 343.959720 N 672.275788 E 82.800000 M 4
2095 344.097222 N 673.387564 E 82.810000 M 4
2108 344.255580 N 674.682410 E 82.820000 M 4
2120 344.410062 N 675.784082 E 82.830000 M 4
2132 344.544242 N 676.887125 E 82.840000 M 4
2143 344.678421 N 677.984687 E 82.850000 M 4
2157 344.832165 N 679.257099 E 82.860000 M 4
2168 344.960808 N 680.340961 E 82.870000 M 4
2179 345.089450 N 681.418144 E 82.880000 M 4
2191 345.216616 N 682.489162 E 82.890000 M 4
2204 345.365931 N 683.729721 E 82.900000 M 4
2215 345.514691 N 684.783099 E 82.910000 M 4
2226 345.639089 N 685.833910 E 82.920000 M 4
2237 345.760533 N 686.878212 E 82.930000 M 4
2250 345.903203 N 688.088630 E 82.940000 M 4
2260 346.022802 N 689.118547 E 82.950000 M 4
2271 346.141847 N 690.143669 E 82.960000 M 4
2282 346.259047 N 691.161941 E 82.970000 M 4
2294 346.394887 N 692.341020 E 82.980000 M 4
2305 346.530728 N 693.339427 E 82.990000 M 4
2315 346.644421 N 694.337320 E 83.000000 M 4
2326 346.757006 N 695.328191 E 83.010000 M 4
2338 346.885280 N 696.475245 E 83.020000 M 4
2348 346.996573 N 697.451732 E 83.030000 M 4
2359 347.106206 N 698.422053 E 83.040000 M 4
2369 347.215285 N 699.385181 E 83.050000 M 4
2380 347.340420 N 700.501067 E 83.060000 M 4
2390 347.469063 N 701.444844 E 83.070000 M 4
2400 347.575927 N 702.386737 E 83.080000 M 4
2410 347.680022 N 703.322294 E 83.090000 M 4
2422 347.801098 N 704.405128 E 83.100000 M 4
2431 347.903532 N 705.326128 E 83.110000 M 4
2441 348.005413 N 706.240107 E 83.120000 M 4
2450 348.107293 N 707.148948 E 83.130000 M 4
2461 348.223385 N 708.200101 E 83.140000 M 4
2471 348.342800 N 709.089933 E 83.150000 M 4
2480 348.438590 N 709.975655 E 83.160000 M 4
2489 348.531796 N 710.856410 E 83.170000 M 4
2500 348.638475 N 711.875710 E 83.180000 M 4
2509 348.729651 N 712.743108 E 83.190000 M 4
2518 348.823595 N 713.603484 E 83.200000 M 4
2527 348.918462 N 714.458209 E 83.210000 M 4
2538 349.029940 N 715.446512 E 83.220000 M 4
2547 349.154338 N 716.275720 E 83.230000 M 4
2555 349.249020 N 717.108696 E 83.240000 M 4
2564 349.344072 N 717.936363 E 83.250000 M 4
2574 349.456104 N 718.892641 E 83.260000 M 4
2583 349.550786 N 719.701642 E 83.270000 M 4
2591 349.649344 N 720.508416 E 83.280000 M 4
2600 349.750118 N 721.307141 E 83.290000 M 4
2609 349.865471 N 722.231053 E 83.300000 M 4
2618 349.995406 N 723.006145 E 83.310000 M 4
2626 350.094334 N 723.783464 E 83.320000 M 4
2634 350.196030 N 724.555131 E 83.330000 M 4
2643 350.318028 N 725.442223 E 83.340000 M 4
2651 350.418247 N 726.198306 E 83.350000 M 4
2659 350.516437 N 726.948567 E 83.360000 M 4
2667 350.611857 N 727.696601 E 83.370000 M 4
2676 350.725919 N 728.571020 E 83.380000 M 4
2684 350.851609 N 729.309635 E 83.390000 M 4
2692 350.944815 N 730.064349 E 83.400000 M 4
2700 351.034145 N 730.824371 E 83.410000 M 4
2709 351.142116 N 731.712319 E 83.420000 M 4
2718 351.241044 N 732.477821 E 83.430000 M 4
2726 351.339233 N 733.241268 E 83.440000 M 4
2734 351.435946 N 733.997180 E 83.450000 M 4
2743 351.546501 N 734.872284 E 83.460000 M 4
2750 351.671452 N 735.596001 E 83.470000 M 4
2758 351.772041 N 736.335301 E 83.480000 M 4
2766 351.871153 N 737.071176 E 83.490000 M 4
2775 351.982631 N 737.926758 E 83.500000 M 4
2783 352.074176 N 738.656125 E 83.510000 M 4
2790 352.167567 N 739.381383 E 83.520000 M 4
2798 352.262434 N 740.105271 E 83.530000 M 4
2807 352.371881 N 740.942870 E 83.540000 M 4
2814 352.497202 N 741.636960 E 83.550000 M 4
2822 352.589854 N 742.346291 E 83.560000 M 4
2829 352.680845 N 743.051854 E 83.570000 M 4
2838 352.784941 N 743.869589 E 83.580000 M 4
2845 352.872979 N 744.567275 E 83.590000 M 4
2853 352.961571 N 745.261707 E 83.600000 M 4
2860 353.049240 N 745.951857 E 83.610000 M 4
2868 353.146137 N 746.750069 E 83.620000 M 4
2875 353.261675 N 747.414189 E 83.630000 M 4
2882 353.336609 N 748.094578 E 83.640000 M 4
2890 353.408405 N 748.769830 E 83.650000 M 4
2898 353.491091 N 749.553656 E 83.660000 M 4
2905 353.561780 N 750.221373 E 83.670000 M 4
2912 353.631731 N 750.885835 E 83.680000 M 4
2919 353.697621 N 751.544989 E 83.690000 M 4
2927 353.771263 N 752.310663 E 83.700000 M 4
2934 353.862992 N 752.946868 E 83.710000 M 4
2940 353.922792 N 753.595576 E 83.720000 M 4
2947 353.981484 N 754.240858 E 83.730000 M 4
2955 354.049035 N 754.990948 E 83.740000 M 4
2962 354.105328 N 755.629551 E 83.750000 M 4
2968 354.159221 N 756.263873 E 83.760000 M 4
2975 354.213484 N 756.895113 E 83.770000 M 4
2983 354.279189 N 757.627392 E 83.780000 M 4
2989 354.359291 N 758.235683 E 83.790000 M 4
2996 354.417060 N 758.851510 E 83.800000 M 4
3002 354.475752 N 759.464083 E 83.810000 M 4
3009 354.544780 N 760.170845 E 83.820000 M 4
3016 354.603103 N 760.768177 E 83.830000 M 4
3022 354.664194 N 761.359685 E 83.840000 M 4
3028 354.725470 N 761.942974 E 83.850000 M 4
3035 354.798559 N 762.611718 E 83.860000 M 4
3041 354.879768 N 763.160585 E 83.870000 M 4
3047 354.943812 N 763.716987 E 83.880000 M 4
3053 355.009518 N 764.264655 E 83.890000 M 4
3059 355.089989 N 764.890585 E 83.900000 M 4
3065 355.160493 N 765.416847 E 83.910000 M 4
3070 355.232289 N 765.931634 E 83.920000 M 4
3075 355.303163 N 766.433064 E 83.930000 M 4
3081 355.383634 N 767.001625 E 83.940000 M 4
3091 355.532763 N 767.918857 E 83.960000 M 4
3101 355.665651 N 768.860579 E 83.980000 M 4
3110 355.769193 N 769.663414 E 84.000000 M 4
3118 355.865905 N 770.454776 E 84.020000 M 4
3125 355.965387 N 771.089611 E 84.040000 M 4
3131 356.036260 N 771.704068 E 84.060000 M 4
3139 356.114885 N 772.402268 E 84.090000 M 4
3144 356.182990 N 772.921850 E 84.120000 M 4
3154 356.120976 N 773.145336 E 85.080000 M 4
3155 356.108241 N 773.148076 E 85.140000 M 4

View File

@ -1,34 +0,0 @@
DATE:2022-03-10
START_TIME:10:50
STOP_TIME:
UNITS:m
MODE:距离模式
ANTENNAS:200 MHz shielded
FREQUENCY:5351.611816
STACKS:2
LAST_TRACE:50480
POSITIVE_DIRECTION:1
SAMPLES:516
TIME_INTERVAL:0.000000
TIMEWINDOW:96.419553
DEPTH:
ZERO_POSITION:
DIELECTRIC:
SOIL_TYPE:
BITS:16
MARK:
DISTANCE_INTERVAL:0.095571
START_POSITION:0.000000
STOP_POSITION:301.431752
WHEEL_GPS:
WHEEL_CALIBRATION:84.4270000000
SCAN_SECOND:
NUMBER_OF_CH:16
CH_X_OFFSETS:0.080 0.160 0.240 0.320 0.400 0.480 0.560 0.640 0.720 0.800 0.880 0.960 1.040 1.120 1.200 1.280
RTK_X_OFFSET:
RTK_Y_OFFSET:0.350
RTK_Z_OFFSET:
GAIN:
FILTER:
SMOOTH:
ENDIAN_TYPE:1

View File

@ -1,245 +0,0 @@
VERSION:1
1 351.615898 N 669.138257 E 91.260000 M 4
7 351.568280 N 668.631689 E 91.460000 M 4
13 351.512172 N 668.086077 E 91.500000 M 4
18 351.454772 N 667.544745 E 91.530000 M 4
25 351.381868 N 666.861787 E 91.560000 M 4
31 351.321515 N 666.310351 E 91.580000 M 4
37 351.263561 N 665.759087 E 91.600000 M 4
44 351.196379 N 665.117744 E 91.620000 M 4
50 351.134365 N 664.493012 E 91.640000 M 4
59 351.057400 N 663.720659 E 91.660000 M 4
67 350.976191 N 662.934436 E 91.680000 M 4
77 350.879663 N 661.999221 E 91.700000 M 4
87 350.791440 N 661.079249 E 91.720000 M 4
92 350.741238 N 660.577648 E 91.730000 M 4
98 350.678486 N 659.964733 E 91.740000 M 4
104 350.620717 N 659.416551 E 91.750000 M 4
110 350.561655 N 658.846106 E 91.760000 M 4
116 350.500564 N 658.252542 E 91.770000 M 4
124 350.427845 N 657.528655 E 91.780000 M 4
131 350.367123 N 656.904608 E 91.790000 M 4
138 350.299387 N 656.236378 E 91.800000 M 4
145 350.229067 N 655.547254 E 91.810000 M 4
154 350.145643 N 654.732603 E 91.820000 M 4
161 350.073293 N 654.041253 E 91.830000 M 4
168 350.004450 N 653.356411 E 91.840000 M 4
175 349.935238 N 652.672083 E 91.850000 M 4
184 349.852552 N 651.873015 E 91.860000 M 4
191 349.789799 N 651.210094 E 91.870000 M 4
198 349.719110 N 650.521827 E 91.880000 M 4
205 349.647868 N 649.833731 E 91.890000 M 4
214 349.565367 N 649.029355 E 91.900000 M 4
221 349.494124 N 648.340232 E 91.910000 M 4
228 349.421036 N 647.650252 E 91.920000 M 4
236 349.348501 N 646.959588 E 91.930000 M 4
244 349.265447 N 646.152642 E 91.940000 M 4
251 349.205278 N 645.485611 E 91.950000 M 4
258 349.134774 N 644.796316 E 91.960000 M 4
266 349.065192 N 644.112502 E 91.970000 M 4
274 348.985460 N 643.327820 E 91.980000 M 4
281 348.916801 N 642.662158 E 91.990000 M 4
288 348.847035 N 641.989475 E 92.000000 M 4
295 348.777638 N 641.312854 E 92.010000 M 4
304 348.700120 N 640.513615 E 92.020000 M 4
311 348.635522 N 639.845042 E 92.030000 M 4
318 348.562618 N 639.138793 E 92.040000 M 4
326 348.483808 N 638.417988 E 92.050000 M 4
335 348.386727 N 637.561551 E 92.060000 M 4
343 348.304041 N 636.811461 E 92.070000 M 4
351 348.218771 N 636.048185 E 92.080000 M 4
359 348.131841 N 635.271723 E 92.090000 M 4
369 348.031067 N 634.354833 E 92.100000 M 4
377 347.944690 N 633.583337 E 92.110000 M 4
385 347.851854 N 632.776049 E 92.120000 M 4
394 347.756064 N 631.955061 E 92.130000 M 4
404 347.644217 N 630.983884 E 92.140000 M 4
413 347.549349 N 630.136523 E 92.150000 M 4
422 347.451345 N 629.282996 E 92.160000 M 4
431 347.347619 N 628.415085 E 92.170000 M 4
442 347.218238 N 627.391333 E 92.180000 M 4
451 347.107498 N 626.525990 E 92.190000 M 4
461 346.992698 N 625.623656 E 92.200000 M 4
471 346.879005 N 624.711219 E 92.210000 M 4
482 346.745379 N 623.633693 E 92.220000 M 4
492 346.627072 N 622.701390 E 92.230000 M 4
502 346.503412 N 621.759155 E 92.240000 M 4
512 346.374770 N 620.805103 E 92.250000 M 4
524 346.223056 N 619.677229 E 92.260000 M 4
534 346.101243 N 618.721978 E 92.270000 M 4
545 345.973338 N 617.732648 E 92.280000 M 4
555 345.841927 N 616.731501 E 92.290000 M 4
568 345.683754 N 615.547627 E 92.300000 M 4
579 345.546067 N 614.521820 E 92.310000 M 4
590 345.407643 N 613.482655 E 92.320000 M 4
601 345.268110 N 612.431503 E 92.330000 M 4
614 345.104400 N 611.189060 E 92.340000 M 4
625 344.970405 N 610.135510 E 92.350000 M 4
637 344.830688 N 609.043428 E 92.360000 M 4
648 344.689311 N 607.938502 E 92.370000 M 4
662 344.523201 N 606.635778 E 92.380000 M 4
674 344.382377 N 605.511329 E 92.390000 M 4
686 344.244321 N 604.385682 E 92.400000 M 4
698 344.108296 N 603.259863 E 92.410000 M 4
712 343.951230 N 601.950118 E 92.420000 M 4
723 343.827386 N 600.852384 E 92.430000 M 4
735 343.695237 N 599.730675 E 92.440000 M 4
747 343.565487 N 598.610850 E 92.450000 M 4
761 343.415803 N 597.307613 E 92.460000 M 4
773 343.287714 N 596.192240 E 92.470000 M 4
784 343.162394 N 595.079950 E 92.480000 M 4
796 343.035228 N 593.965434 E 92.490000 M 4
810 342.888682 N 592.670416 E 92.500000 M 4
821 342.773144 N 591.588781 E 92.510000 M 4
833 342.648377 N 590.484197 E 92.520000 M 4
845 342.522318 N 589.383039 E 92.530000 M 4
858 342.373927 N 588.099324 E 92.540000 M 4
870 342.245838 N 587.001933 E 92.550000 M 4
881 342.113135 N 585.910193 E 92.560000 M 4
893 341.979509 N 584.822735 E 92.570000 M 4
906 341.822074 N 583.561969 E 92.580000 M 4
917 341.694354 N 582.512015 E 92.590000 M 4
929 341.558513 N 581.440141 E 92.600000 M 4
940 341.421196 N 580.369636 E 92.610000 M 4
953 341.258224 N 579.126338 E 92.620000 M 4
964 341.119430 N 578.062512 E 92.630000 M 4
976 340.975837 N 577.000399 E 92.640000 M 4
987 340.831691 N 575.937430 E 92.650000 M 4
1000 340.663182 N 574.697899 E 92.660000 M 4
1011 340.529556 N 573.660961 E 92.670000 M 4
1022 340.393715 N 572.596964 E 92.680000 M 4
1034 340.262120 N 571.527830 E 92.690000 M 4
1047 340.111883 N 570.275112 E 92.700000 M 4
1058 339.984347 N 569.196730 E 92.710000 M 4
1070 339.858842 N 568.112012 E 92.720000 M 4
1081 339.734260 N 567.020615 E 92.730000 M 4
1095 339.592513 N 565.738613 E 92.740000 M 4
1106 339.474760 N 564.656977 E 92.750000 M 4
1118 339.354423 N 563.542975 E 92.760000 M 4
1130 339.231871 N 562.424006 E 92.770000 M 4
1144 339.090862 N 561.112720 E 92.780000 M 4
1156 338.968679 N 559.982105 E 92.790000 M 4
1168 338.849080 N 558.843100 E 92.800000 M 4
1180 338.731512 N 557.701697 E 92.810000 M 4
1194 338.594010 N 556.366093 E 92.820000 M 4
1206 338.471458 N 555.247637 E 92.830000 M 4
1218 338.346691 N 554.102467 E 92.840000 M 4
1230 338.217679 N 552.954556 E 92.850000 M 4
1244 338.065597 N 551.614499 E 92.860000 M 4
1256 337.929571 N 550.468472 E 92.870000 M 4
1268 337.793730 N 549.321932 E 92.880000 M 4
1280 337.653275 N 548.178474 E 92.890000 M 4
1294 337.486797 N 546.847836 E 92.900000 M 4
1306 337.348187 N 545.743937 E 92.910000 M 4
1318 337.200350 N 544.610925 E 92.920000 M 4
1330 337.054358 N 543.482880 E 92.930000 M 4
1344 336.887879 N 542.173649 E 92.940000 M 4
1356 336.748532 N 541.058448 E 92.950000 M 4
1367 336.606416 N 539.950781 E 92.960000 M 4
1379 336.465961 N 538.841745 E 92.970000 M 4
1393 336.301328 N 537.547927 E 92.980000 M 4
1404 336.174531 N 536.475196 E 92.990000 M 4
1416 336.045150 N 535.382429 E 93.000000 M 4
1427 335.917430 N 534.298738 E 93.010000 M 4
1441 335.760364 N 533.041226 E 93.020000 M 4
1452 335.608281 N 531.959932 E 93.030000 M 4
1463 335.463212 N 530.883434 E 93.040000 M 4
1475 335.319251 N 529.814985 E 93.050000 M 4
1488 335.157386 N 528.576653 E 93.060000 M 4
1499 335.015824 N 527.549647 E 93.070000 M 4
1510 334.864110 N 526.497638 E 93.080000 M 4
1521 334.712028 N 525.447684 E 93.090000 M 4
1534 334.540381 N 524.228875 E 93.100000 M 4
1545 334.395681 N 523.187483 E 93.110000 M 4
1556 334.251350 N 522.150202 E 93.120000 M 4
1567 334.106835 N 521.117716 E 93.130000 M 4
1580 333.937403 N 519.920827 E 93.140000 M 4
1590 333.797317 N 518.929100 E 93.150000 M 4
1601 333.652617 N 517.909458 E 93.160000 M 4
1612 333.512347 N 516.893926 E 93.170000 M 4
1624 333.353435 N 515.716559 E 93.180000 M 4
1635 333.218148 N 514.710104 E 93.190000 M 4
1645 333.082677 N 513.707930 E 93.200000 M 4
1656 332.948312 N 512.708667 E 93.210000 M 4
1668 332.794753 N 511.546885 E 93.220000 M 4
1679 332.669248 N 510.583414 E 93.230000 M 4
1689 332.539129 N 509.589289 E 93.240000 M 4
1700 332.407718 N 508.594650 E 93.250000 M 4
1712 332.258404 N 507.433552 E 93.260000 M 4
1722 332.129945 N 506.438228 E 93.270000 M 4
1733 332.001672 N 505.442048 E 93.280000 M 4
1744 331.873952 N 504.445354 E 93.290000 M 4
1756 331.724453 N 503.282716 E 93.300000 M 4
1766 331.605408 N 502.315306 E 93.310000 M 4
1777 331.481010 N 501.317413 E 93.320000 M 4
1787 331.358274 N 500.318321 E 93.330000 M 4
1800 331.214312 N 499.153628 E 93.340000 M 4
1810 331.092498 N 498.154023 E 93.350000 M 4
1821 330.972161 N 497.155616 E 93.360000 M 4
1831 330.852193 N 496.158579 E 93.370000 M 4
1844 330.714507 N 494.995598 E 93.380000 M 4
1854 330.599153 N 494.034525 E 93.390000 M 4
1864 330.482692 N 493.043825 E 93.400000 M 4
1875 330.367707 N 492.057406 E 93.410000 M 4
1887 330.235188 N 490.910009 E 93.420000 M 4
1897 330.121126 N 489.931468 E 93.430000 M 4
1907 330.007249 N 488.957208 E 93.440000 M 4
1918 329.894848 N 487.986202 E 93.450000 M 4
1930 329.761960 N 486.857472 E 93.460000 M 4
1939 329.655466 N 485.927738 E 93.470000 M 4
1950 329.543249 N 484.971288 E 93.480000 M 4
1960 329.431218 N 484.018949 E 93.490000 M 4
1971 329.295561 N 482.912995 E 93.500000 M 4
1981 329.181130 N 481.965964 E 93.510000 M 4
1991 329.067991 N 481.026983 E 93.520000 M 4
2001 328.951899 N 480.092967 E 93.530000 M 4
2013 328.814213 N 479.003625 E 93.540000 M 4
2022 328.700335 N 478.112423 E 93.550000 M 4
2032 328.584058 N 477.197759 E 93.560000 M 4
2041 328.467966 N 476.293885 E 93.570000 M 4
2052 328.329542 N 475.255919 E 93.580000 M 4
2062 328.209389 N 474.381499 E 93.590000 M 4
2071 328.090159 N 473.523007 E 93.600000 M 4
2080 327.971483 N 472.680783 E 93.610000 M 4
2090 327.835088 N 471.721251 E 93.620000 M 4
2098 327.714751 N 470.953009 E 93.630000 M 4
2106 327.599767 N 470.168155 E 93.640000 M 4
2115 327.486258 N 469.397344 E 93.650000 M 4
2124 327.358354 N 468.514876 E 93.660000 M 4
2132 327.255920 N 467.774034 E 93.670000 M 4
2140 327.157177 N 467.044838 E 93.680000 M 4
2147 327.060648 N 466.330541 E 93.690000 M 4
2156 326.951385 N 465.512806 E 93.700000 M 4
2163 326.863347 N 464.862386 E 93.710000 M 4
2170 326.773094 N 464.183196 E 93.720000 M 4
2177 326.687455 N 463.510855 E 93.730000 M 4
2185 326.591666 N 462.736277 E 93.740000 M 4
2192 326.513963 N 462.076095 E 93.750000 M 4
2199 326.439768 N 461.420880 E 93.760000 M 4
2206 326.369632 N 460.771146 E 93.770000 M 4
2214 326.290638 N 460.017974 E 93.780000 M 4
2220 326.231023 N 459.415676 E 93.790000 M 4
2227 326.167717 N 458.778271 E 93.800000 M 4
2234 326.104964 N 458.145490 E 93.810000 M 4
2241 326.033168 N 457.411841 E 93.820000 M 4
2248 325.975399 N 456.788308 E 93.830000 M 4
2254 325.922428 N 456.170769 E 93.840000 M 4
2261 325.870011 N 455.558881 E 93.850000 M 4
2268 325.805598 N 454.852119 E 93.860000 M 4
2274 325.758164 N 454.291435 E 93.870000 M 4
2281 325.703164 N 453.699927 E 93.880000 M 4
2287 325.647794 N 453.113727 E 93.890000 M 4
2294 325.584303 N 452.440530 E 93.900000 M 4
2300 325.530040 N 451.872312 E 93.910000 M 4
2306 325.474117 N 451.314369 E 93.920000 M 4
2311 325.415425 N 450.764817 E 93.930000 M 4
2318 325.346397 N 450.137859 E 93.940000 M 4
2329 325.238610 N 449.143905 E 93.960000 M 4
2340 325.119380 N 448.100116 E 93.980000 M 4
2349 325.015654 N 447.226039 E 94.000000 M 4
2358 324.915250 N 446.391351 E 94.020000 M 4
2364 324.840870 N 445.768674 E 94.040000 M 4
2371 324.767966 N 445.168774 E 94.060000 M 4
2377 324.691002 N 444.541815 E 94.090000 M 4
2383 324.676791 N 444.146220 E 94.520000 M 4
2384 324.679928 N 444.131664 E 94.570000 M 4

View File

@ -1,34 +0,0 @@
DATE:2022-03-10
START_TIME:10:52
STOP_TIME:
UNITS:m
MODE:距离模式
ANTENNAS:200 MHz shielded
FREQUENCY:5351.611816
STACKS:2
LAST_TRACE:38144
POSITIVE_DIRECTION:1
SAMPLES:516
TIME_INTERVAL:0.000000
TIMEWINDOW:96.419553
DEPTH:
ZERO_POSITION:
DIELECTRIC:
SOIL_TYPE:
BITS:16
MARK:
DISTANCE_INTERVAL:0.095224
START_POSITION:0.000000
STOP_POSITION:226.917805
WHEEL_GPS:
WHEEL_CALIBRATION:84.4270000000
SCAN_SECOND:
NUMBER_OF_CH:16
CH_X_OFFSETS:0.080 0.160 0.240 0.320 0.400 0.480 0.560 0.640 0.720 0.800 0.880 0.960 1.040 1.120 1.200 1.280
RTK_X_OFFSET:
RTK_Y_OFFSET:0.350
RTK_Z_OFFSET:
GAIN:
FILTER:
SMOOTH:
ENDIAN_TYPE:1

View File

@ -1,313 +0,0 @@
VERSION:1
1 319.500445 N 468.116541 E 103.040000 M 4
7 319.535328 N 468.424626 E 103.500000 M 4
12 319.612846 N 468.954826 E 103.560000 M 4
19 319.706790 N 469.530750 E 103.600000 M 4
25 319.803134 N 470.120718 E 103.630000 M 4
32 319.933438 N 470.825768 E 103.660000 M 4
38 320.037348 N 471.349289 E 103.680000 M 4
45 320.152149 N 471.971110 E 103.700000 M 4
51 320.260489 N 472.603548 E 103.720000 M 4
59 320.409249 N 473.312879 E 103.740000 M 4
67 320.556718 N 474.027519 E 103.760000 M 4
76 320.738147 N 474.871455 E 103.780000 M 4
85 320.908870 N 475.733887 E 103.800000 M 4
96 321.136071 N 476.723217 E 103.820000 M 4
106 321.362165 N 477.709122 E 103.840000 M 4
112 321.482133 N 478.227334 E 103.850000 M 4
119 321.624987 N 478.858402 E 103.860000 M 4
125 321.744217 N 479.432615 E 103.870000 M 4
131 321.879689 N 480.010937 E 103.880000 M 4
137 322.021989 N 480.609981 E 103.890000 M 4
145 322.194374 N 481.334725 E 103.900000 M 4
152 322.350702 N 481.975383 E 103.910000 M 4
159 322.513674 N 482.631797 E 103.920000 M 4
167 322.683659 N 483.303110 E 103.930000 M 4
175 322.883176 N 484.108857 E 103.940000 M 4
183 323.044302 N 484.826065 E 103.950000 M 4
191 323.214103 N 485.540363 E 103.960000 M 4
198 323.382981 N 486.265620 E 103.970000 M 4
208 323.577145 N 487.113324 E 103.980000 M 4
215 323.743439 N 487.835157 E 103.990000 M 4
223 323.906226 N 488.554763 E 104.000000 M 4
231 324.065692 N 489.273342 E 104.010000 M 4
240 324.242875 N 490.111113 E 104.020000 M 4
248 324.380008 N 490.840651 E 104.030000 M 4
255 324.518064 N 491.554606 E 104.040000 M 4
263 324.654643 N 492.267362 E 104.050000 M 4
272 324.806725 N 493.096913 E 104.060000 M 4
279 324.930938 N 493.806587 E 104.070000 M 4
287 325.050353 N 494.515918 E 104.080000 M 4
294 325.164415 N 495.223023 E 104.090000 M 4
303 325.293980 N 496.045381 E 104.100000 M 4
311 325.395122 N 496.754883 E 104.110000 M 4
318 325.499402 N 497.442636 E 104.120000 M 4
325 325.602206 N 498.119258 E 104.130000 M 4
333 325.721436 N 498.890925 E 104.140000 M 4
340 325.824608 N 499.549737 E 104.150000 M 4
347 325.929257 N 500.215056 E 104.160000 M 4
354 326.033353 N 500.880717 E 104.170000 M 4
363 326.158858 N 501.667797 E 104.180000 M 4
370 326.257970 N 502.360174 E 104.190000 M 4
378 326.366126 N 503.055120 E 104.200000 M 4
385 326.477234 N 503.762225 E 104.210000 M 4
394 326.609753 N 504.602564 E 104.220000 M 4
402 326.724000 N 505.337755 E 104.230000 M 4
410 326.839169 N 506.087844 E 104.240000 M 4
418 326.953969 N 506.852661 E 104.250000 M 4
427 327.089994 N 507.762872 E 104.260000 M 4
436 327.203872 N 508.566907 E 104.270000 M 4
445 327.326239 N 509.376592 E 104.280000 M 4
453 327.448238 N 510.199806 E 104.290000 M 4
464 327.590907 N 511.176464 E 104.300000 M 4
473 327.713644 N 512.026736 E 104.310000 M 4
482 327.835827 N 512.889510 E 104.320000 M 4
491 327.957271 N 513.763245 E 104.330000 M 4
502 328.096065 N 514.796073 E 104.340000 M 4
512 328.206251 N 515.703373 E 104.350000 M 4
521 328.320682 N 516.610673 E 104.360000 M 4
531 328.434006 N 517.527734 E 104.370000 M 4
542 328.563756 N 518.606458 E 104.380000 M 4
552 328.672835 N 519.538419 E 104.390000 M 4
562 328.782836 N 520.479627 E 104.400000 M 4
572 328.899298 N 521.428199 E 104.410000 M 4
584 329.043259 N 522.546311 E 104.420000 M 4
594 329.159351 N 523.522455 E 104.430000 M 4
605 329.284118 N 524.489351 E 104.440000 M 4
615 329.415345 N 525.459672 E 104.450000 M 4
627 329.570381 N 526.592512 E 104.460000 M 4
637 329.709359 N 527.563690 E 104.470000 M 4
647 329.845569 N 528.540690 E 104.480000 M 4
658 329.983255 N 529.520601 E 104.490000 M 4
670 330.141244 N 530.663716 E 104.500000 M 4
680 330.268410 N 531.657670 E 104.510000 M 4
691 330.402405 N 532.638952 E 104.520000 M 4
701 330.537323 N 533.622630 E 104.530000 M 4
713 330.691990 N 534.770370 E 104.540000 M 4
724 330.821740 N 535.756789 E 104.550000 M 4
734 330.946322 N 536.739097 E 104.560000 M 4
745 331.073673 N 537.726373 E 104.570000 M 4
757 331.217450 N 538.882161 E 104.580000 M 4
768 331.334834 N 539.885363 E 104.590000 M 4
778 331.453141 N 540.863219 E 104.600000 M 4
788 331.564619 N 541.836451 E 104.610000 M 4
800 331.698799 N 542.969977 E 104.620000 M 4
810 331.825596 N 543.945264 E 104.630000 M 4
821 331.968081 N 544.934765 E 104.640000 M 4
831 332.100599 N 545.920842 E 104.650000 M 4
844 332.240685 N 547.073376 E 104.660000 M 4
854 332.349026 N 548.068186 E 104.670000 M 4
864 332.465672 N 549.056489 E 104.680000 M 4
875 332.593391 N 550.048388 E 104.690000 M 4
887 332.748981 N 551.219075 E 104.700000 M 4
898 332.875962 N 552.226730 E 104.710000 M 4
909 332.991316 N 553.235754 E 104.720000 M 4
919 333.101871 N 554.251457 E 104.730000 M 4
932 333.231252 N 555.441839 E 104.740000 M 4
943 333.335532 N 556.475695 E 104.750000 M 4
954 333.450517 N 557.502358 E 104.760000 M 4
965 333.567163 N 558.535872 E 104.770000 M 4
977 333.702635 N 559.751085 E 104.780000 M 4
989 333.816697 N 560.798641 E 104.790000 M 4
1000 333.930020 N 561.852020 E 104.800000 M 4
1011 334.045743 N 562.910023 E 104.810000 M 4
1024 334.179554 N 564.144416 E 104.820000 M 4
1035 334.285864 N 565.212009 E 104.830000 M 4
1046 334.397342 N 566.263162 E 104.840000 M 4
1057 334.509189 N 567.311574 E 104.850000 M 4
1070 334.638201 N 568.532439 E 104.860000 M 4
1081 334.746172 N 569.574344 E 104.870000 M 4
1092 334.853221 N 570.615050 E 104.880000 M 4
1103 334.958423 N 571.651646 E 104.890000 M 4
1116 335.082452 N 572.857440 E 104.900000 M 4
1127 335.178242 N 573.900373 E 104.910000 M 4
1138 335.285475 N 574.928406 E 104.920000 M 4
1148 335.394000 N 575.951987 E 104.930000 M 4
1161 335.521535 N 577.138943 E 104.940000 M 4
1172 335.633751 N 578.147796 E 104.950000 M 4
1182 335.748736 N 579.147059 E 104.960000 M 4
1193 335.865013 N 580.142897 E 104.970000 M 4
1205 335.999193 N 581.298857 E 104.980000 M 4
1215 336.108456 N 582.294866 E 104.990000 M 4
1226 336.223256 N 583.273578 E 105.000000 M 4
1236 336.341009 N 584.248694 E 105.010000 M 4
1248 336.480911 N 585.382733 E 105.020000 M 4
1258 336.601248 N 586.351684 E 105.030000 M 4
1268 336.719739 N 587.322519 E 105.040000 M 4
1279 336.837123 N 588.293183 E 105.050000 M 4
1291 336.972411 N 589.427222 E 105.060000 M 4
1301 337.076690 N 590.409531 E 105.070000 M 4
1311 337.187430 N 591.384989 E 105.080000 M 4
1322 337.295955 N 592.365757 E 105.090000 M 4
1334 337.423306 N 593.516921 E 105.100000 M 4
1344 337.530170 N 594.510533 E 105.110000 M 4
1355 337.635557 N 595.508083 E 105.120000 M 4
1366 337.739837 N 596.512826 E 105.130000 M 4
1378 337.860913 N 597.691391 E 105.140000 M 4
1389 337.960947 N 598.712061 E 105.150000 M 4
1399 338.069657 N 599.732559 E 105.160000 M 4
1410 338.181504 N 600.760250 E 105.170000 M 4
1423 338.314023 N 601.966900 E 105.180000 M 4
1434 338.429007 N 603.004181 E 105.190000 M 4
1445 338.547868 N 604.046258 E 105.200000 M 4
1456 338.669682 N 605.093471 E 105.210000 M 4
1469 338.813644 N 606.320159 E 105.220000 M 4
1480 338.931951 N 607.379189 E 105.230000 M 4
1491 339.056164 N 608.437877 E 105.240000 M 4
1503 339.181853 N 609.500161 E 105.250000 M 4
1516 339.330429 N 610.742432 E 105.260000 M 4
1527 339.460548 N 611.808312 E 105.270000 M 4
1538 339.592882 N 612.877618 E 105.280000 M 4
1550 339.724478 N 613.948464 E 105.290000 M 4
1563 339.876930 N 615.199298 E 105.300000 M 4
1574 340.000774 N 616.278023 E 105.310000 M 4
1586 340.131816 N 617.354349 E 105.320000 M 4
1597 340.263596 N 618.431019 E 105.330000 M 4
1610 340.416971 N 619.683908 E 105.340000 M 4
1622 340.544137 N 620.756467 E 105.350000 M 4
1633 340.664474 N 621.830054 E 105.360000 M 4
1644 340.777429 N 622.906723 E 105.370000 M 4
1658 340.900350 N 624.165263 E 105.380000 M 4
1669 340.990418 N 625.252208 E 105.390000 M 4
1680 341.082517 N 626.336412 E 105.400000 M 4
1692 341.171847 N 627.425754 E 105.410000 M 4
1705 341.278711 N 628.700906 E 105.420000 M 4
1717 341.371547 N 629.799154 E 105.430000 M 4
1728 341.467337 N 630.899285 E 105.440000 M 4
1740 341.565157 N 632.004553 E 105.450000 M 4
1754 341.688632 N 633.298200 E 105.460000 M 4
1766 341.784053 N 634.414087 E 105.470000 M 4
1777 341.903098 N 635.527233 E 105.480000 M 4
1789 342.021959 N 636.640037 E 105.490000 M 4
1803 342.176072 N 637.944130 E 105.500000 M 4
1815 342.305822 N 639.064641 E 105.510000 M 4
1826 342.436126 N 640.179328 E 105.520000 M 4
1838 342.552033 N 641.295728 E 105.530000 M 4
1852 342.683444 N 642.600678 E 105.540000 M 4
1864 342.773513 N 643.729066 E 105.550000 M 4
1876 342.890712 N 644.854371 E 105.560000 M 4
1888 343.013449 N 645.980019 E 105.570000 M 4
1901 343.164793 N 647.282571 E 105.580000 M 4
1913 343.304510 N 648.404965 E 105.590000 M 4
1925 343.443119 N 649.528901 E 105.600000 M 4
1937 343.584682 N 650.654035 E 105.610000 M 4
1951 343.739533 N 651.967719 E 105.620000 M 4
1963 343.854702 N 653.094051 E 105.630000 M 4
1975 343.987959 N 654.224323 E 105.640000 M 4
1987 344.126384 N 655.353053 E 105.650000 M 4
2001 344.296184 N 656.672217 E 105.660000 M 4
2013 344.439039 N 657.805914 E 105.670000 M 4
2025 344.584846 N 658.939439 E 105.680000 M 4
2037 344.730100 N 660.077589 E 105.690000 M 4
2051 344.900454 N 661.407713 E 105.700000 M 4
2063 345.026513 N 662.550315 E 105.710000 M 4
2075 345.173428 N 663.694629 E 105.720000 M 4
2087 345.321819 N 664.839628 E 105.730000 M 4
2101 345.496234 N 666.177630 E 105.740000 M 4
2113 345.645364 N 667.327767 E 105.750000 M 4
2125 345.796339 N 668.477733 E 105.760000 M 4
2138 345.947314 N 669.630096 E 105.770000 M 4
2152 346.126159 N 670.974777 E 105.780000 M 4
2164 346.260708 N 672.130736 E 105.790000 M 4
2176 346.416851 N 673.287381 E 105.800000 M 4
2189 346.572625 N 674.446081 E 105.810000 M 4
2203 346.755345 N 675.799153 E 105.820000 M 4
2215 346.912596 N 676.961791 E 105.830000 M 4
2228 347.070215 N 678.126142 E 105.840000 M 4
2240 347.222483 N 679.293062 E 105.850000 M 4
2254 347.393760 N 680.655724 E 105.860000 M 4
2267 347.517973 N 681.827439 E 105.870000 M 4
2279 347.655475 N 683.002408 E 105.880000 M 4
2292 347.788363 N 684.181144 E 105.890000 M 4
2306 347.936385 N 685.559391 E 105.900000 M 4
2319 348.059860 N 686.743436 E 105.910000 M 4
2331 348.179090 N 687.931248 E 105.920000 M 4
2344 348.294813 N 689.122315 E 105.930000 M 4
2358 348.429361 N 690.515460 E 105.940000 M 4
2371 348.531427 N 691.709781 E 105.950000 M 4
2384 348.656193 N 692.907183 E 105.960000 M 4
2396 348.786866 N 694.105100 E 105.970000 M 4
2411 348.946147 N 695.504924 E 105.980000 M 4
2424 349.085864 N 696.704725 E 105.990000 M 4
2436 349.232594 N 697.903498 E 106.000000 M 4
2449 349.385415 N 699.102442 E 106.010000 M 4
2464 349.568135 N 700.500896 E 106.020000 M 4
2476 349.715235 N 701.697614 E 106.030000 M 4
2489 349.880237 N 702.898784 E 106.040000 M 4
2502 350.048746 N 704.099955 E 106.050000 M 4
2517 350.251030 N 705.500635 E 106.060000 M 4
2530 350.428214 N 706.701463 E 106.070000 M 4
2542 350.611857 N 707.903490 E 106.080000 M 4
2555 350.796977 N 709.105174 E 106.090000 M 4
2570 351.013843 N 710.502943 E 106.100000 M 4
2583 351.186966 N 711.694181 E 106.110000 M 4
2595 351.371532 N 712.883535 E 106.120000 M 4
2608 351.556837 N 714.067409 E 106.130000 M 4
2622 351.774440 N 715.437264 E 106.140000 M 4
2635 351.960852 N 716.603328 E 106.150000 M 4
2647 352.142281 N 717.762370 E 106.160000 M 4
2659 352.320018 N 718.912335 E 106.170000 M 4
2673 352.529132 N 720.247597 E 106.180000 M 4
2685 352.697825 N 721.387459 E 106.190000 M 4
2698 352.877777 N 722.524752 E 106.200000 M 4
2709 353.058099 N 723.649544 E 106.210000 M 4
2723 353.270719 N 724.943704 E 106.220000 M 4
2735 353.460269 N 726.036814 E 106.230000 M 4
2747 353.659416 N 727.128383 E 106.240000 M 4
2758 353.851549 N 728.224575 E 106.250000 M 4
2772 354.058817 N 729.494246 E 106.260000 M 4
2783 354.214776 N 730.578793 E 106.270000 M 4
2795 354.375348 N 731.648613 E 106.280000 M 4
2806 354.538136 N 732.715521 E 106.290000 M 4
2819 354.737098 N 733.949743 E 106.300000 M 4
2830 354.901362 N 734.996614 E 106.310000 M 4
2841 355.063042 N 736.035265 E 106.320000 M 4
2852 355.223615 N 737.067237 E 106.330000 M 4
2865 355.414087 N 738.270634 E 106.340000 M 4
2876 355.566170 N 739.294044 E 106.350000 M 4
2887 355.723236 N 740.307007 E 106.360000 M 4
2897 355.879748 N 741.316716 E 106.370000 M 4
2910 356.059146 N 742.492199 E 106.380000 M 4
2920 356.208645 N 743.494373 E 106.390000 M 4
2931 356.353714 N 744.492437 E 106.400000 M 4
2941 356.495276 N 745.487418 E 106.410000 M 4
2954 356.656956 N 746.643207 E 106.420000 M 4
2964 356.780431 N 747.622262 E 106.430000 M 4
2974 356.910735 N 748.604399 E 106.440000 M 4
2985 357.036055 N 749.582427 E 106.450000 M 4
2997 357.180571 N 750.716466 E 106.460000 M 4
3007 357.299062 N 751.683019 E 106.470000 M 4
3017 357.415155 N 752.644606 E 106.480000 M 4
3027 357.526079 N 753.598316 E 106.490000 M 4
3039 357.651215 N 754.701529 E 106.500000 M 4
3049 357.745528 N 755.632976 E 106.510000 M 4
3059 357.845194 N 756.559114 E 106.520000 M 4
3068 357.941538 N 757.471551 E 106.530000 M 4
3079 358.047294 N 758.520135 E 106.540000 M 4
3089 358.133117 N 759.404658 E 106.550000 M 4
3098 358.215249 N 760.273768 E 106.560000 M 4
3107 358.295720 N 761.125240 E 106.570000 M 4
3117 358.389111 N 762.096417 E 106.580000 M 4
3125 358.456108 N 762.897711 E 106.590000 M 4
3134 358.531596 N 763.687874 E 106.600000 M 4
3142 358.607268 N 764.460397 E 106.610000 M 4
3151 358.693091 N 765.335159 E 106.620000 M 4
3159 358.764149 N 766.063670 E 106.630000 M 4
3166 358.831331 N 766.770261 E 106.640000 M 4
3173 358.893346 N 767.455788 E 106.650000 M 4
3182 358.961635 N 768.226770 E 106.660000 M 4
3188 359.010730 N 768.851674 E 106.670000 M 4
3195 359.064808 N 769.463390 E 106.680000 M 4
3201 359.115748 N 770.048220 E 106.690000 M 4
3208 359.172964 N 770.699154 E 106.700000 M 4
3213 359.220951 N 771.227984 E 106.710000 M 4
3218 359.265247 N 771.731811 E 106.720000 M 4
3229 359.351070 N 772.736554 E 106.740000 M 4
3238 359.417514 N 773.546411 E 106.760000 M 4
3246 359.488941 N 774.324928 E 106.780000 M 4
3252 359.543019 N 774.944865 E 106.800000 M 4
3258 359.591744 N 775.511885 E 106.820000 M 4
3265 359.639363 N 776.139185 E 106.850000 M 4
3271 359.689011 N 776.684456 E 106.880000 M 4
3276 359.732569 N 777.210717 E 106.920000 M 4
3281 359.757300 N 777.645015 E 107.070000 M 4

View File

@ -1,34 +0,0 @@
DATE:2022-03-10
START_TIME:10:54
STOP_TIME:
UNITS:m
MODE:距离模式
ANTENNAS:200 MHz shielded
FREQUENCY:5351.611816
STACKS:2
LAST_TRACE:52496
POSITIVE_DIRECTION:1
SAMPLES:516
TIME_INTERVAL:0.000000
TIMEWINDOW:96.419553
DEPTH:
ZERO_POSITION:
DIELECTRIC:
SOIL_TYPE:
BITS:16
MARK:
DISTANCE_INTERVAL:0.095304
START_POSITION:0.000000
STOP_POSITION:312.597339
WHEEL_GPS:
WHEEL_CALIBRATION:84.4270000000
SCAN_SECOND:
NUMBER_OF_CH:16
CH_X_OFFSETS:0.080 0.160 0.240 0.320 0.400 0.480 0.560 0.640 0.720 0.800 0.880 0.960 1.040 1.120 1.200 1.280
RTK_X_OFFSET:
RTK_Y_OFFSET:0.350
RTK_Z_OFFSET:
GAIN:
FILTER:
SMOOTH:
ENDIAN_TYPE:1

View File

@ -1,342 +0,0 @@
VERSION:1
1 324.297322 N 503.979203 E 139.800000 M 4
7 324.256718 N 504.322737 E 140.200000 M 4
12 324.396434 N 504.852766 E 140.270000 M 4
18 324.513819 N 505.340837 E 140.310000 M 4
24 324.607209 N 505.990744 E 140.350000 M 4
31 324.726439 N 506.597665 E 140.380000 M 4
38 324.842716 N 507.268635 E 140.410000 M 4
44 324.893841 N 507.812365 E 140.430000 M 4
49 324.984463 N 508.360546 E 140.450000 M 4
56 325.087635 N 509.010795 E 140.470000 M 4
63 325.191361 N 509.661729 E 140.490000 M 4
71 325.270725 N 510.411133 E 140.510000 M 4
79 325.384602 N 511.133137 E 140.530000 M 4
87 325.509000 N 511.944535 E 140.550000 M 4
95 325.621770 N 512.717572 E 140.570000 M 4
104 325.712761 N 513.580517 E 140.590000 M 4
113 325.840112 N 514.406129 E 140.610000 M 4
118 325.912462 N 514.903963 E 140.620000 M 4
128 326.032799 N 515.786945 E 140.640000 M 4
138 326.176391 N 516.779871 E 140.660000 M 4
148 326.294514 N 517.721079 E 140.680000 M 4
159 326.463392 N 518.779938 E 140.700000 M 4
164 326.533896 N 519.284108 E 140.710000 M 4
170 326.602370 N 519.796840 E 140.720000 M 4
175 326.672136 N 520.320190 E 140.730000 M 4
182 326.773094 N 520.942867 E 140.740000 M 4
188 326.828280 N 521.484883 E 140.750000 M 4
193 326.927022 N 522.032551 E 140.760000 M 4
199 327.013215 N 522.588782 E 140.770000 M 4
206 327.096454 N 523.251875 E 140.780000 M 4
212 327.160868 N 523.828484 E 140.790000 M 4
218 327.229896 N 524.407834 E 140.800000 M 4
224 327.303169 N 524.992493 E 140.810000 M 4
232 327.395267 N 525.683842 E 140.820000 M 4
238 327.427566 N 526.277920 E 140.830000 M 4
244 327.495671 N 526.876964 E 140.840000 M 4
250 327.560638 N 527.469158 E 140.850000 M 4
258 327.636495 N 528.152972 E 140.860000 M 4
264 327.707553 N 528.735233 E 140.870000 M 4
270 327.780272 N 529.314412 E 140.880000 M 4
276 327.851884 N 529.893590 E 140.890000 M 4
283 327.932724 N 530.566958 E 140.900000 M 4
289 327.957456 N 531.136033 E 140.910000 M 4
295 328.024453 N 531.709731 E 140.920000 M 4
301 328.092928 N 532.282231 E 140.930000 M 4
308 328.171553 N 532.947207 E 140.940000 M 4
314 328.238366 N 533.514398 E 140.950000 M 4
320 328.304441 N 534.081418 E 140.960000 M 4
326 328.371069 N 534.647239 E 140.970000 M 4
333 328.448033 N 535.303481 E 140.980000 M 4
338 328.473319 N 535.856287 E 140.990000 M 4
344 328.536625 N 536.415943 E 141.000000 M 4
350 328.597532 N 536.974913 E 141.010000 M 4
357 328.665637 N 537.623278 E 141.020000 M 4
363 328.725252 N 538.177454 E 141.030000 M 4
368 328.786897 N 538.731116 E 141.040000 M 4
374 328.848173 N 539.283407 E 141.050000 M 4
381 328.920154 N 539.923895 E 141.060000 M 4
387 328.947100 N 540.463342 E 141.070000 M 4
392 329.012806 N 541.009640 E 141.080000 M 4
398 329.080357 N 541.554397 E 141.090000 M 4
405 329.162120 N 542.187178 E 141.100000 M 4
410 329.234470 N 542.727482 E 141.110000 M 4
416 329.308666 N 543.266587 E 141.120000 M 4
422 329.382677 N 543.802781 E 141.130000 M 4
428 329.470161 N 544.426657 E 141.140000 M 4
434 329.503752 N 544.948808 E 141.150000 M 4
439 329.578501 N 545.477467 E 141.160000 M 4
445 329.653251 N 546.003215 E 141.170000 M 4
451 329.743873 N 546.612705 E 141.180000 M 4
457 329.823421 N 547.130232 E 141.190000 M 4
462 329.903523 N 547.640910 E 141.200000 M 4
467 329.980487 N 548.146792 E 141.210000 M 4
474 330.069448 N 548.731450 E 141.220000 M 4
484 330.180003 N 549.693723 E 141.240000 M 4
494 330.343898 N 550.697780 E 141.260000 M 4
503 330.471618 N 551.564493 E 141.280000 M 4
513 330.563347 N 552.437714 E 141.300000 M 4
520 330.586602 N 553.192256 E 141.320000 M 4
529 330.636989 N 553.992865 E 141.340000 M 4
536 330.668734 N 554.708361 E 141.360000 M 4
544 330.679070 N 555.453655 E 141.380000 M 4
551 330.607643 N 556.085922 E 141.400000 M 4
557 330.548582 N 556.728465 E 141.420000 M 4
563 330.477524 N 557.269796 E 141.440000 M 4
569 330.380626 N 557.826369 E 141.460000 M 4
574 330.217285 N 558.309988 E 141.480000 M 4
580 330.050068 N 558.821522 E 141.500000 M 4
587 329.796659 N 559.454303 E 141.530000 M 4
593 329.506336 N 559.996148 E 141.560000 M 4
599 329.281719 N 560.470006 E 141.590000 M 4
606 328.978661 N 561.040108 E 141.630000 M 4
612 328.768809 N 561.613807 E 141.670000 M 4
619 328.545299 N 562.222783 E 141.710000 M 4
624 328.412412 N 562.743736 E 141.740000 M 4
630 328.319206 N 563.274278 E 141.770000 M 4
636 328.202191 N 563.836674 E 141.800000 M 4
642 328.143868 N 564.435547 E 141.830000 M 4
649 328.134455 N 565.052744 E 141.860000 M 4
655 328.124304 N 565.625415 E 141.890000 M 4
661 328.168969 N 566.254942 E 141.920000 M 4
668 328.201822 N 566.863233 E 141.950000 M 4
674 328.281185 N 567.492589 E 141.980000 M 4
680 328.370515 N 568.086324 E 142.010000 M 4
687 328.450248 N 568.687594 E 142.040000 M 4
693 328.574092 N 569.315580 E 142.070000 M 4
700 328.736510 N 569.931407 E 142.100000 M 4
706 328.901143 N 570.446194 E 142.130000 M 4
711 329.110072 N 570.936321 E 142.160000 M 4
718 329.388398 N 571.514472 E 142.200000 M 4
723 329.652513 N 571.959559 E 142.230000 M 4
729 329.957601 N 572.392316 E 142.260000 M 4
736 330.395207 N 572.934675 E 142.300000 M 4
742 330.780951 N 573.325647 E 142.330000 M 4
748 331.181090 N 573.686477 E 142.360000 M 4
753 331.607438 N 574.046280 E 142.390000 M 4
759 332.004994 N 574.416359 E 142.420000 M 4
766 332.495756 N 574.925495 E 142.460000 M 4
772 332.843294 N 575.334790 E 142.490000 M 4
778 333.141922 N 575.785015 E 142.520000 M 4
784 333.431138 N 576.283191 E 142.550000 M 4
790 333.676426 N 576.827091 E 142.580000 M 4
796 333.836261 N 577.356606 E 142.610000 M 4
802 333.995541 N 577.948629 E 142.640000 M 4
808 334.082657 N 578.543049 E 142.670000 M 4
815 334.153530 N 579.169665 E 142.700000 M 4
821 334.193027 N 579.780868 E 142.730000 M 4
828 334.172171 N 580.425807 E 142.760000 M 4
835 334.156483 N 581.103799 E 142.790000 M 4
842 334.126030 N 581.798916 E 142.820000 M 4
849 334.068260 N 582.456529 E 142.850000 M 4
854 334.032085 N 582.957445 E 142.870000 M 4
862 333.952353 N 583.704623 E 142.900000 M 4
870 333.825740 N 584.414810 E 142.930000 M 4
875 333.746931 N 584.952203 E 142.950000 M 4
881 333.674396 N 585.458427 E 142.970000 M 4
887 333.585804 N 586.007123 E 142.990000 M 4
892 333.538371 N 586.538522 E 143.010000 M 4
898 333.481893 N 587.125749 E 143.030000 M 4
904 333.457715 N 587.681124 E 143.050000 M 4
910 333.416926 N 588.282394 E 143.070000 M 4
916 333.420986 N 588.857120 E 143.090000 M 4
923 333.447195 N 589.493839 E 143.110000 M 4
929 333.478940 N 590.105214 E 143.130000 M 4
937 333.475249 N 590.788343 E 143.150000 M 4
944 333.499612 N 591.468218 E 143.170000 M 4
952 333.523790 N 592.247592 E 143.190000 M 4
960 333.554428 N 593.009155 E 143.210000 M 4
969 333.558857 N 593.868504 E 143.230000 M 4
977 333.597986 N 594.704563 E 143.250000 M 4
983 333.621610 N 595.204794 E 143.260000 M 4
992 333.671074 N 596.080583 E 143.280000 M 4
1002 333.739548 N 597.041142 E 143.300000 M 4
1011 333.775538 N 597.913678 E 143.320000 M 4
1021 333.844012 N 598.860195 E 143.340000 M 4
1030 333.916916 N 599.722969 E 143.360000 M 4
1039 334.008830 N 600.644483 E 143.380000 M 4
1048 334.056633 N 601.469067 E 143.400000 M 4
1058 334.150392 N 602.378936 E 143.420000 M 4
1066 334.243414 N 603.208487 E 143.440000 M 4
1076 334.345479 N 604.098490 E 143.460000 M 4
1084 334.406939 N 604.887111 E 143.480000 M 4
1093 334.517310 N 605.755023 E 143.500000 M 4
1101 334.628234 N 606.545699 E 143.520000 M 4
1110 334.742850 N 607.363434 E 143.540000 M 4
1117 334.783270 N 608.038856 E 143.560000 M 4
1125 334.874261 N 608.776616 E 143.580000 M 4
1132 334.959900 N 609.447415 E 143.600000 M 4
1139 335.050337 N 610.177981 E 143.620000 M 4
1147 335.103677 N 610.857343 E 143.640000 M 4
1155 335.207588 N 611.674563 E 143.660000 M 4
1164 335.311130 N 612.489729 E 143.680000 M 4
1174 335.434604 N 613.434533 E 143.700000 M 4
1183 335.524119 N 614.337209 E 143.720000 M 4
1194 335.663282 N 615.393328 E 143.740000 M 4
1199 335.725665 N 615.898525 E 143.750000 M 4
1205 335.788049 N 616.414169 E 143.760000 M 4
1210 335.850986 N 616.938375 E 143.770000 M 4
1217 335.925366 N 617.560025 E 143.780000 M 4
1222 335.953789 N 618.077381 E 143.790000 M 4
1228 336.021710 N 618.626761 E 143.800000 M 4
1234 336.090368 N 619.180594 E 143.810000 M 4
1241 336.171208 N 619.832556 E 143.820000 M 4
1247 336.241344 N 620.397863 E 143.830000 M 4
1253 336.314986 N 620.968650 E 143.840000 M 4
1259 336.388074 N 621.544746 E 143.850000 M 4
1266 336.474266 N 622.222052 E 143.860000 M 4
1272 336.507119 N 622.781194 E 143.870000 M 4
1278 336.577070 N 623.371847 E 143.880000 M 4
1284 336.648312 N 623.965068 E 143.890000 M 4
1291 336.733766 N 624.661042 E 143.900000 M 4
1298 336.808885 N 625.263511 E 143.910000 M 4
1304 336.888064 N 625.869233 E 143.920000 M 4
1310 336.967243 N 626.478039 E 143.930000 M 4
1318 337.052143 N 627.193192 E 143.940000 M 4
1324 337.089241 N 627.785900 E 143.950000 M 4
1331 337.163252 N 628.406179 E 143.960000 M 4
1337 337.240585 N 629.030055 E 143.970000 M 4
1345 337.331023 N 629.761307 E 143.980000 M 4
1351 337.407802 N 630.391176 E 143.990000 M 4
1358 337.482552 N 631.020361 E 144.000000 M 4
1365 337.550657 N 631.639099 E 144.010000 M 4
1372 337.621530 N 632.337127 E 144.020000 M 4
1378 337.648477 N 632.907058 E 144.030000 M 4
1384 337.715290 N 633.490347 E 144.040000 M 4
1390 337.782472 N 634.063531 E 144.050000 M 4
1397 337.859621 N 634.719774 E 144.060000 M 4
1403 337.922927 N 635.273778 E 144.070000 M 4
1408 337.984203 N 635.816137 E 144.080000 M 4
1414 338.041234 N 636.350961 E 144.090000 M 4
1420 338.105647 N 636.964219 E 144.100000 M 4
1431 338.182058 N 637.977867 E 144.120000 M 4
1436 338.239458 N 638.494710 E 144.130000 M 4
1443 338.306271 N 639.098378 E 144.140000 M 4
1448 338.364040 N 639.615391 E 144.150000 M 4
1454 338.420887 N 640.133432 E 144.160000 M 4
1459 338.477918 N 640.650275 E 144.170000 M 4
1465 338.546023 N 641.256169 E 144.180000 M 4
1471 338.567432 N 641.757256 E 144.190000 M 4
1476 338.630923 N 642.290025 E 144.200000 M 4
1482 338.696444 N 642.828959 E 144.210000 M 4
1489 338.773777 N 643.466706 E 144.220000 M 4
1494 338.840221 N 644.019854 E 144.230000 M 4
1500 338.908695 N 644.581394 E 144.240000 M 4
1506 338.977354 N 645.148584 E 144.250000 M 4
1513 339.054503 N 645.817328 E 144.260000 M 4
1519 339.083110 N 646.369278 E 144.270000 M 4
1525 339.152876 N 646.949998 E 144.280000 M 4
1531 339.225042 N 647.534485 E 144.290000 M 4
1538 339.310311 N 648.206654 E 144.300000 M 4
1544 339.375279 N 648.764255 E 144.310000 M 4
1550 339.441723 N 649.315348 E 144.320000 M 4
1556 339.509274 N 649.866270 E 144.330000 M 4
1562 339.591775 N 650.507613 E 144.340000 M 4
1568 339.623520 N 651.027024 E 144.350000 M 4
1573 339.691625 N 651.566815 E 144.360000 M 4
1579 339.758992 N 652.103865 E 144.370000 M 4
1586 339.835772 N 652.726028 E 144.380000 M 4
1591 339.900001 N 653.255201 E 144.390000 M 4
1597 339.970505 N 653.785229 E 144.400000 M 4
1602 340.045808 N 654.311491 E 144.410000 M 4
1609 340.133108 N 654.919954 E 144.420000 M 4
1619 340.217455 N 655.922299 E 144.440000 M 4
1625 340.268764 N 656.434004 E 144.450000 M 4
1631 340.332624 N 657.030479 E 144.460000 M 4
1636 340.394454 N 657.534649 E 144.470000 M 4
1641 340.466065 N 658.031283 E 144.480000 M 4
1647 340.546536 N 658.526034 E 144.490000 M 4
1653 340.622947 N 659.099904 E 144.500000 M 4
1662 340.679793 N 660.032549 E 144.520000 M 4
1673 340.785919 N 661.079249 E 144.540000 M 4
1684 340.914561 N 662.055050 E 144.560000 M 4
1695 341.040989 N 663.133432 E 144.580000 M 4
1705 341.127920 N 664.116939 E 144.600000 M 4
1711 341.186797 N 664.633439 E 144.610000 M 4
1717 341.258962 N 665.246184 E 144.620000 M 4
1723 341.322822 N 665.779466 E 144.630000 M 4
1728 341.389820 N 666.319770 E 144.640000 M 4
1734 341.455894 N 666.866753 E 144.650000 M 4
1741 341.535442 N 667.513406 E 144.660000 M 4
1746 341.590628 N 668.044291 E 144.670000 M 4
1752 341.659471 N 668.616448 E 144.680000 M 4
1758 341.725176 N 669.194256 E 144.690000 M 4
1766 341.805094 N 669.881325 E 144.700000 M 4
1772 341.875782 N 670.477286 E 144.710000 M 4
1778 341.955146 N 671.077700 E 144.720000 M 4
1784 342.031187 N 671.677258 E 144.730000 M 4
1792 342.120333 N 672.377341 E 144.740000 M 4
1798 342.177364 N 672.942306 E 144.750000 M 4
1804 342.251744 N 673.536041 E 144.760000 M 4
1810 342.324832 N 674.116418 E 144.770000 M 4
1817 342.411763 N 674.784477 E 144.780000 M 4
1823 342.483744 N 675.349784 E 144.790000 M 4
1829 342.554433 N 675.908584 E 144.800000 M 4
1835 342.621061 N 676.460533 E 144.810000 M 4
1841 342.697287 N 677.091772 E 144.820000 M 4
1847 342.741029 N 677.589777 E 144.830000 M 4
1852 342.806919 N 678.115524 E 144.840000 M 4
1858 342.871702 N 678.634764 E 144.850000 M 4
1864 342.942945 N 679.229185 E 144.860000 M 4
1869 342.999053 N 679.730100 E 144.870000 M 4
1879 343.111269 N 680.711895 E 144.890000 M 4
1885 343.176606 N 681.270866 E 144.900000 M 4
1895 343.266674 N 682.177823 E 144.920000 M 4
1905 343.368739 N 683.173319 E 144.940000 M 4
1914 343.462130 N 684.072398 E 144.960000 M 4
1925 343.557181 N 685.028163 E 144.980000 M 4
1933 343.623071 N 685.862166 E 145.000000 M 4
1943 343.724029 N 686.782139 E 145.020000 M 4
1952 343.817973 N 687.615457 E 145.040000 M 4
1961 343.927237 N 688.503577 E 145.060000 M 4
1969 344.015275 N 689.275587 E 145.080000 M 4
1978 344.136350 N 690.134079 E 145.100000 M 4
1986 344.244137 N 690.919104 E 145.120000 M 4
1995 344.358568 N 691.766808 E 145.140000 M 4
2003 344.429441 N 692.523062 E 145.160000 M 4
2012 344.538705 N 693.381726 E 145.180000 M 4
2021 344.648521 N 694.184048 E 145.200000 M 4
2030 344.766459 N 695.064975 E 145.220000 M 4
2038 344.850068 N 695.857706 E 145.240000 M 4
2048 344.971882 N 696.756786 E 145.260000 M 4
2056 345.083360 N 697.593358 E 145.280000 M 4
2066 345.212371 N 698.503398 E 145.300000 M 4
2075 345.295057 N 699.315995 E 145.320000 M 4
2084 345.415948 N 700.229117 E 145.340000 M 4
2093 345.530748 N 701.065861 E 145.360000 M 4
2102 345.650163 N 701.963228 E 145.380000 M 4
2111 345.722143 N 702.756302 E 145.400000 M 4
2120 345.834729 N 703.632605 E 145.420000 M 4
2128 345.931811 N 704.432186 E 145.440000 M 4
2137 346.036091 N 705.284171 E 145.460000 M 4
2145 346.099028 N 706.030322 E 145.480000 M 4
2154 346.194264 N 706.851139 E 145.500000 M 4
2161 346.282117 N 707.594720 E 145.520000 M 4
2170 346.375323 N 708.387452 E 145.540000 M 4
2177 346.426633 N 709.081370 E 145.560000 M 4
2185 346.518178 N 709.844988 E 145.580000 M 4
2192 346.596618 N 710.535653 E 145.600000 M 4
2200 346.675982 N 711.267761 E 145.620000 M 4
2207 346.722308 N 711.903282 E 145.640000 M 4
2214 346.802041 N 712.606790 E 145.660000 M 4
2221 346.868484 N 713.244708 E 145.680000 M 4
2228 346.939173 N 713.919618 E 145.700000 M 4
2234 346.979593 N 714.501879 E 145.720000 M 4
2241 347.051205 N 715.143051 E 145.740000 M 4
2247 347.117834 N 715.718804 E 145.760000 M 4
2253 347.185754 N 716.324527 E 145.780000 M 4
2258 347.219160 N 716.838972 E 145.800000 M 4
2264 347.284497 N 717.408047 E 145.820000 M 4
2270 347.341712 N 717.917011 E 145.840000 M 4
2275 347.401327 N 718.448924 E 145.860000 M 4
2282 347.459650 N 719.123662 E 145.890000 M 4
2290 347.539937 N 719.825629 E 145.920000 M 4
2296 347.587001 N 720.451902 E 145.950000 M 4
2303 347.656582 N 721.061735 E 145.980000 M 4
2308 347.717858 N 721.590052 E 146.010000 M 4
2315 347.766215 N 722.213585 E 146.050000 M 4
2320 347.828967 N 722.764678 E 146.090000 M 4
2326 347.871418 N 723.292309 E 146.140000 M 4
2332 347.919036 N 723.817200 E 146.220000 M 4
2333 347.924942 N 723.934680 E 146.300000 M 4

View File

@ -1,34 +0,0 @@
DATE:2022-03-10
START_TIME:11:02
STOP_TIME:
UNITS:m
MODE:距离模式
ANTENNAS:200 MHz shielded
FREQUENCY:5351.611816
STACKS:2
LAST_TRACE:37328
POSITIVE_DIRECTION:1
SAMPLES:516
TIME_INTERVAL:0.000000
TIMEWINDOW:96.419553
DEPTH:
ZERO_POSITION:
DIELECTRIC:
SOIL_TYPE:
BITS:16
MARK:
DISTANCE_INTERVAL:0.095987
START_POSITION:0.000000
STOP_POSITION:223.841500
WHEEL_GPS:
WHEEL_CALIBRATION:84.4270000000
SCAN_SECOND:
NUMBER_OF_CH:16
CH_X_OFFSETS:0.080 0.160 0.240 0.320 0.400 0.480 0.560 0.640 0.720 0.800 0.880 0.960 1.040 1.120 1.200 1.280
RTK_X_OFFSET:
RTK_Y_OFFSET:0.350
RTK_Z_OFFSET:
GAIN:
FILTER:
SMOOTH:
ENDIAN_TYPE:1

View File

@ -4,28 +4,30 @@
#include "EmptyAwareComboBox.hpp"
#include <QFormLayout>
#include <QJsonArray>
#include <QJsonObject>
#include <QLabel>
#include <QLineEdit>
#include <QPainter>
#include <QPen>
#include <QPixmap>
#include <QPlainTextEdit>
#include <QPointer>
#include <algorithm>
#include "FormKit.hpp"
#include "Theme.hpp"
#include "repo/IDatasetCommandRepository.hpp"
namespace geopro::app {
namespace {
// 异常类型 mock 列表label, id。真实 exceptionType 端点只读、后续接。
struct TypeItem { const char* label; const char* id; };
const TypeItem kMockTypes[] = {
{"断层", "mock-fault"},
{"破碎带", "mock-fracture"},
{"含水构造", "mock-water"},
{"其它", "mock-other"},
};
} // namespace
AnomalySaveDialog::AnomalySaveDialog(const QString& screenshotPath, int shotW, int shotH,
geopro::data::IDatasetCommandRepository* cmdRepo,
const QString& projectId, int remarkSourceType,
QWidget* parent)
: QDialog(parent), cmdRepo_(cmdRepo), remarkSourceType_(remarkSourceType) {
: QDialog(parent) {
setWindowTitle(QStringLiteral("保存异常"));
setModal(true);
@ -40,17 +42,10 @@ AnomalySaveDialog::AnomalySaveDialog(const QString& screenshotPath, int shotW, i
form->addRow(formkit::editLabel(QStringLiteral("名称")), name_);
type_ = new EmptyAwareComboBox();
type_->setPlaceholderText(QStringLiteral("请选择异常类型")); // 空(如该形态平台无类型)时显灰占位+「暂无数据」
for (const auto& t : kMockTypes)
type_->addItem(QString::fromUtf8(t.label), QString::fromUtf8(t.id));
formkit::capField(type_);
form->addRow(formkit::editLabel(QStringLiteral("异常类型")), type_);
// 样式预览:选中类型的 legend 派生样式可视化(点=色球/线=线型/面=描边矩形)。
stylePreview_ = new QLabel(QStringLiteral(""));
stylePreview_->setMinimumWidth(geopro::app::scaledPx(92));
form->addRow(formkit::editLabel(QStringLiteral("样式")), stylePreview_);
// 选中类型变化 → 拉其平台样式(legend),使保存的异常按平台类型样式渲染 + 刷新预览。
connect(type_, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this](int) { loadStyleForCurrent(); });
loadTypes(cmdRepo, projectId, remarkSourceType); // 异步拉平台异常类型填充(与平台一致)
remark_ = new QPlainTextEdit();
remark_->setFixedHeight(geopro::app::scaledPx(60));
@ -74,78 +69,6 @@ AnomalySaveDialog::AnomalySaveDialog(const QString& screenshotPath, int shotW, i
formkit::addDialogButtons(root, this);
}
void AnomalySaveDialog::loadTypes(geopro::data::IDatasetCommandRepository* cmdRepo,
const QString& projectId, int remarkSourceType) {
if (cmdRepo == nullptr || projectId.isEmpty()) return; // 无仓储/项目 → 下拉留空(空态提示)
QPointer<AnomalySaveDialog> self(this);
cmdRepo->listExceptionTypes(
projectId, QString::number(remarkSourceType),
[self](bool ok, QJsonArray list, const QString&) {
if (!self || !ok) return; // 对话框已关 / 失败 → 留空
for (const QJsonValue& v : list) {
const QJsonObject o = v.toObject();
// 平台响应项:{label:类型名, value:类型id}(实测扁平 data 数组net 层已归一 value
self->type_->addItem(o.value(QStringLiteral("label")).toString(),
o.value(QStringLiteral("value")).toString());
}
self->loadStyleForCurrent(); // 首项自动选中 → 预取其样式
});
}
void AnomalySaveDialog::loadStyleForCurrent() {
if (cmdRepo_ == nullptr) return;
const QString typeId = type_->currentData().toString();
if (typeId.isEmpty()) return;
QPointer<AnomalySaveDialog> self(this);
cmdRepo_->getExceptionTypeDetail(typeId, [self](bool ok, QJsonObject detail, const QString&) {
if (!self || !ok) return;
const QJsonObject lg = detail.value(QStringLiteral("legend")).toObject();
// 按形态(1点/2线/3面)从 legend 派生样式:点用 pointColor线/面用 polyline*。
if (self->remarkSourceType_ == 1) {
self->styleColor_ = lg.value(QStringLiteral("pointColor")).toString();
} else {
self->styleColor_ = lg.value(QStringLiteral("polylineColor")).toString();
self->styleWidth_ = lg.value(QStringLiteral("polylineWidth")).toDouble();
self->styleDashed_ =
lg.value(QStringLiteral("polylineShape")).toString().contains(QStringLiteral("dash"));
}
self->updateStylePreview();
});
}
void AnomalySaveDialog::updateStylePreview() {
if (stylePreview_ == nullptr) return;
const QColor col(styleColor_);
if (!col.isValid()) { // 未取到样式 → 占位
stylePreview_->setPixmap(QPixmap());
stylePreview_->setText(QStringLiteral(""));
return;
}
const int w = geopro::app::scaledPx(92), h = geopro::app::scaledPx(22);
QPixmap pm(w, h);
pm.fill(Qt::transparent);
QPainter p(&pm);
p.setRenderHint(QPainter::Antialiasing, true);
QPen pen(col);
pen.setWidthF(std::clamp(styleWidth_ > 0.0 ? styleWidth_ : 2.0, 1.0, 4.0));
if (styleDashed_) pen.setStyle(Qt::DashLine);
if (remarkSourceType_ == 1) { // 点:实心色球
p.setPen(Qt::NoPen);
p.setBrush(col);
p.drawEllipse(QPointF(w / 2.0, h / 2.0), h * 0.3, h * 0.3);
} else if (remarkSourceType_ == 2) { // 线:按线宽/虚实画线
p.setPen(pen);
p.drawLine(QPointF(w * 0.1, h / 2.0), QPointF(w * 0.9, h / 2.0));
} else { // 面:描边矩形 + 淡填充
p.setPen(pen);
p.setBrush(QColor(col.red(), col.green(), col.blue(), 40));
p.drawRect(QRectF(w * 0.12, h * 0.22, w * 0.76, h * 0.56));
}
p.end();
stylePreview_->setText(QString());
stylePreview_->setPixmap(pm);
}
QString AnomalySaveDialog::anomalyName() const {
const QString n = name_->text().trimmed();
return n.isEmpty() ? QStringLiteral("异常") : n;

View File

@ -7,52 +7,24 @@ class QComboBox;
class QPlainTextEdit;
class QLabel;
namespace geopro::data {
class IDatasetCommandRepository;
}
namespace geopro::app {
// 异常保存对话框(#4b需求 R50名称 + 异常类型 + 备注 + 截图预览/大小。
// 异常类型从平台按标注形态(remarkSourceType)异步拉取,与平台保持一致。accept 后取 name/typeName/typeId/remark。
// 异常类型本期 mock 列表(真实 exceptionType 端点只读、后续可接。accept 后取 name/typeName/typeId/remark。
class AnomalySaveDialog : public QDialog {
Q_OBJECT
public:
// screenshotPath圈定结束截图的本地路径为空则不显示预览w/h截图像素尺寸R50「确定截图大小」
// cmdRepo/projectId异步拉取平台异常类型填充下拉与平台一致remarkSourceType标注形态 1点/2线/3面
// 决定查询哪一类平台异常类型。cmdRepo 为空则下拉留空(空态由 EmptyAwareComboBox 提示)。
AnomalySaveDialog(const QString& screenshotPath, int shotW, int shotH,
geopro::data::IDatasetCommandRepository* cmdRepo, const QString& projectId,
int remarkSourceType, QWidget* parent = nullptr);
AnomalySaveDialog(const QString& screenshotPath, int shotW, int shotH, QWidget* parent = nullptr);
QString anomalyName() const;
QString typeName() const;
QString typeId() const;
QString remark() const;
// 选中类型的平台样式(从 legend 按形态派生与平台一致。styleColor 空 = 未取到,调用方用默认样式。
QString styleColor() const { return styleColor_; }
double styleWidth() const { return styleWidth_; }
bool styleDashed() const { return styleDashed_; }
private:
// 异步拉平台异常类型(label→显示, value→id)填充下拉;空/失败时下拉留空EmptyAwareComboBox 提示)。
void loadTypes(geopro::data::IDatasetCommandRepository* cmdRepo, const QString& projectId,
int remarkSourceType);
// 拉当前选中类型的详情 legend → 按形态(点/线/面)派生 styleColor/Width/Dashed。
void loadStyleForCurrent();
// 据当前样式按形态画预览(点=色球/线=线型/面=描边矩形),画到 stylePreview_。
void updateStylePreview();
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
int remarkSourceType_ = 3;
QString styleColor_; // legend 派生线/点色(空=未取到)
double styleWidth_ = 0.0; // legend.polylineWidth
bool styleDashed_ = false; // legend.polylineShape 含 "dash"
QLineEdit* name_ = nullptr;
QComboBox* type_ = nullptr;
QLabel* stylePreview_ = nullptr; // 选中类型样式预览(色块/线型)
QPlainTextEdit* remark_ = nullptr;
};

View File

@ -48,7 +48,7 @@ AxesSettingsPanel::AxesSettingsPanel(QWidget* parent) : QFrame(parent) {
setFrameShape(QFrame::StyledPanel);
applyTokenizedStyleSheet(
this, QStringLiteral("QFrame{background:{{bg/panel}};border:1px solid {{border/default}};"
"border-radius:8px;}")); // radius/lg=8规范§3.2 画布浮窗)
"border-radius:10px;}"));
setFixedWidth(320);
auto* v = new QVBoxLayout(this);
@ -63,12 +63,9 @@ AxesSettingsPanel::AxesSettingsPanel(QWidget* parent) : QFrame(parent) {
close->setFixedSize(24, 24);
close->setCursor(Qt::PointingHandCursor);
// 显式覆盖全局 QPushButton 的 padding(6px 14px)/border——否则 24×24 容不下 padding× 被挤出不可见。
// 颜色走 token避免深色模式失效
applyTokenizedStyleSheet(
close, QStringLiteral(
"QPushButton{border:none;background:transparent;padding:0;margin:0;font-size:16px;"
"color:{{text/secondary}};}"
"QPushButton:hover{color:{{accent/primary}};}"));
close->setStyleSheet(QStringLiteral(
"QPushButton{border:none;background:transparent;padding:0;margin:0;font-size:16px;color:#888;}"
"QPushButton:hover{color:#2f6fed;}"));
connect(close, &QPushButton::clicked, this, &AxesSettingsPanel::closed);
titleRow->addWidget(title);
titleRow->addStretch(1);
@ -103,8 +100,7 @@ AxesSettingsPanel::AxesSettingsPanel(QWidget* parent) : QFrame(parent) {
scaleSlider_->setSingleStep(1);
scaleSlider_->setPageStep(1); // 点击轨道按 1 步移动(默认 pageStep=10 → 点一下直接跳到 10 的 bug
scaleLabel_ = new QLabel(QStringLiteral("1.0×"), this);
applyTokenizedStyleSheet(scaleLabel_,
QStringLiteral("border:none;color:{{text/secondary}};min-width:36px;"));
scaleLabel_->setStyleSheet(QStringLiteral("border:none;color:#888;min-width:36px;"));
connect(scaleSlider_, &QSlider::valueChanged, this,
[this](int v) { scaleLabel_->setText(QStringLiteral("%1.0×").arg(v)); });
scaleRow->addWidget(scaleSlider_, 1);
@ -115,7 +111,9 @@ AxesSettingsPanel::AxesSettingsPanel(QWidget* parent) : QFrame(parent) {
auto* btns = new QHBoxLayout();
auto* cancel = new QPushButton(QStringLiteral("取消"), this);
auto* apply = new QPushButton(QStringLiteral("应用"), this);
apply->setDefault(true); // 主按钮:走全局 QPushButton:default 样式(accent/primary,随主题),不再硬编码蓝
apply->setStyleSheet(QStringLiteral(
"QPushButton{background:#2f6fed;color:white;border:none;border-radius:6px;padding:6px 18px;}"
"QPushButton:hover{background:#2a63d4;}"));
connect(cancel, &QPushButton::clicked, this, &AxesSettingsPanel::closed);
connect(apply, &QPushButton::clicked, this, [this] {
auto rd = [](const Row& r) {
@ -149,7 +147,7 @@ AxesSettingsPanel::Row AxesSettingsPanel::addAxisRow(QVBoxLayout* col, const QSt
auto* range = new QHBoxLayout();
auto* loCol = new QVBoxLayout();
auto* loLbl = new QLabel(QStringLiteral("最小值"), this);
applyTokenizedStyleSheet(loLbl, QStringLiteral("border:none;color:{{text/tertiary}};"));
loLbl->setStyleSheet(QStringLiteral("border:none;color:#888;"));
loCol->addWidget(loLbl);
r.lo = new QDoubleSpinBox(this);
r.lo->setRange(-1e6, 1e6);
@ -158,7 +156,7 @@ AxesSettingsPanel::Row AxesSettingsPanel::addAxisRow(QVBoxLayout* col, const QSt
range->addLayout(loCol);
auto* hiCol = new QVBoxLayout();
auto* hiLbl = new QLabel(QStringLiteral("最大值"), this);
applyTokenizedStyleSheet(hiLbl, QStringLiteral("border:none;color:{{text/tertiary}};"));
hiLbl->setStyleSheet(QStringLiteral("border:none;color:#888;"));
hiCol->addWidget(hiLbl);
r.hi = new QDoubleSpinBox(this);
r.hi->setRange(-1e6, 1e6);

View File

@ -5,10 +5,8 @@ find_package(VTK REQUIRED COMPONENTS
GUISupportQt
RenderingOpenGL2
RenderingVolumeOpenGL2
RenderingAnnotation # vtkAxesActor gnomon +
InteractionStyle
InteractionWidgets
FiltersSources # vtkSphereSourcegnomon 6
FiltersGeometry
FiltersModeling
IOImage # vtkPNGWriter
@ -43,7 +41,6 @@ add_executable(geopro_desktop WIN32
panels/DescriptionPanel.cpp
panels/QuillDelta.cpp
panels/chart/RawDataChartView.cpp
panels/chart/ColorScaleProperties.cpp
panels/chart/InversionFormDialog.cpp
panels/chart/ScatterDataOps.cpp
panels/chart/SaveAsDialog.cpp
@ -67,7 +64,6 @@ add_executable(geopro_desktop WIN32
panels/chart/BarChartView.cpp
panels/chart/LineChartView.cpp
panels/chart/TrajectoryMapView.cpp
panels/web/ProjectWebView.cpp
panels/chart/DetailViewFactory.cpp
resources/map/map.qrc
resources/keys.qrc
@ -82,11 +78,11 @@ add_executable(geopro_desktop WIN32
panels/chart/ChartPickGeometry.cpp
panels/chart/ScatterMarqueePicker.cpp
panels/chart/ContourDrawTool.cpp
panels/columns/Column2DDataset.cpp
panels/columns/CategorySection.cpp
panels/columns/CategoryAnalysisTab.cpp
panels/columns/DateRangeEdit.cpp
panels/columns/ColumnDrawer.cpp
panels/columns/SectionIconBar.cpp
panels/AnomalyTablePanel.cpp
panels/LoadingOverlay.cpp
panels/DatasetDetailPage.cpp
@ -111,6 +107,7 @@ add_executable(geopro_desktop WIN32
VolumeParamsDialog.cpp
VolumePropertiesDialog.cpp
Logging.cpp
DatasetDimension.cpp
DatasetCategory.cpp
VtkViewToolbar.cpp
AxesSettingsDialog.cpp

View File

@ -205,11 +205,11 @@ ColorGradientDialog::ColorGradientDialog(const std::vector<Stop>& init, double m
// ── 整体透明度滑块0~1, step 0.01 ───────────────────────────────────
{
auto* opRow = new QHBoxLayout();
opRow->addWidget(new QLabel(QStringLiteral("透明度:")));
opRow->addWidget(new QLabel(QStringLiteral("整体透明度:")));
opacitySlider_ = new QSlider(Qt::Horizontal, this);
opacitySlider_->setRange(0, 100);
opacitySlider_->setValue(static_cast<int>(opacity_ * 100 + 0.5));
opacityLabel_ = new QLabel(QString::number(opacity_ * 100, 'f', 0), this); // 0~100 显示
opacityLabel_ = new QLabel(QString::number(opacity_, 'f', 2), this);
opRow->addWidget(opacitySlider_, 1);
opRow->addWidget(opacityLabel_);
root->addLayout(opRow);
@ -247,7 +247,7 @@ ColorGradientDialog::ColorGradientDialog(const std::vector<Stop>& init, double m
[this](double) { onMinMaxChanged(); });
connect(opacitySlider_, &QSlider::valueChanged, this, [this](int v) {
opacity_ = v / 100.0;
opacityLabel_->setText(QString::number(v)); // 0~100 显示(内部仍存 0~1
opacityLabel_->setText(QString::number(opacity_, 'f', 2));
});
// 配色方案:内置预设打底,再异步拉取后端 .clr 列表(若有 api

View File

@ -112,7 +112,6 @@ ColorScaleConfigDialog::ColorScaleConfigDialog(const geopro::core::ColorScale& i
resize(560, 420);
// 用初始色阶的升序断点填模型;空色阶兜底成 vmin/vmax 两端蓝红。
globalOpacity_ = init.globalOpacity(); // 回显真实整体透明度(两级第二级),不再硬编码 1
for (const auto& [value, color] : init.stops()) rows_.push_back({value, color});
if (rows_.empty()) {
rows_.push_back({vmin_, geopro::core::Rgba{0, 0, 255, 255}});
@ -355,15 +354,15 @@ void ColorScaleConfigDialog::onColorScheme() {
std::vector<GradientEditWidget::Stop> seed;
for (const auto& r : rows_) seed.push_back({(r.value - lo) / span, r.color});
ColorGradientDialog dlg(seed, lo, hi, vmin_, vmax_, samples_, globalOpacity_, tplRepo_, projectId_,
this);
ColorGradientDialog dlg(seed, lo, hi, vmin_, vmax_, samples_, 1.0, tplRepo_, projectId_, this);
if (dlg.exec() != QDialog::Accepted) return;
const auto grad = dlg.stops();
if (grad.size() < 2) return;
globalOpacity_ = dlg.opacity(); // 两级第二级:整体透明度单独存,不烘焙进每色 alpha
const double opacity = dlg.opacity();
const unsigned char alpha = static_cast<unsigned char>(opacity * 255.0 + 0.5);
// 在新渐变上按各层级位置连续采样回填颜色(复刻 mapColors,含每色自有 alpha)。
// 在新渐变上按各层级位置连续采样回填颜色(复刻 mapColors + addAlphaToColor 整体透明度)。
auto sampleGrad = [&](double pos) -> geopro::core::Rgba {
if (pos <= grad.front().pos) return grad.front().color;
if (pos >= grad.back().pos) return grad.back().color;
@ -373,8 +372,11 @@ void ColorScaleConfigDialog::onColorScheme() {
const double t = (x1 > x0) ? (pos - x0) / (x1 - x0) : 0.0;
return lerp(grad[i].color, grad[i + 1].color, t);
};
// 只回填颜色(含每色自有 alpha整体透明度单独存于 globalOpacity_、渲染时才相乘两级
for (auto& r : rows_) r.color = sampleGrad((r.value - lo) / span);
for (auto& r : rows_) {
geopro::core::Rgba c = sampleGrad((r.value - lo) / span);
if (opacity < 1.0) c.a = alpha; // 整体透明度覆盖 alpha
r.color = c;
}
rebuildTable();
}
@ -557,7 +559,6 @@ void ColorScaleConfigDialog::onOpen() {
geopro::core::ColorScale ColorScaleConfigDialog::colorScale() const {
geopro::core::ColorScale cs;
for (const auto& row : rows_) cs.addStop(row.value, row.color); // 内部按 value 升序
cs.setGlobalOpacity(globalOpacity_); // 整体透明度独立带出(渲染时与每色 alpha 相乘)
return cs;
}

View File

@ -44,11 +44,6 @@ public:
// 线形/标注配置(线形⚙ 编辑后2D 消费3D 忽略)。
ContourLineConfig lineConfig() const { return lineCfg_; }
// 层级方案透传字段(复刻原版 properties保存色阶时一并写回避免覆盖清空 web 设过的值)。
QString lvlSchemeType() const { return lvlSchemeType_; }
int logLinesCount() const { return logLinesCount_; }
int equalAreaLayerCount() const { return equalAreaLayerCount_; }
private:
struct Row {
double value;
@ -75,7 +70,6 @@ private:
QTableWidget* table_ = nullptr;
std::vector<Row> rows_; // 始终按 value 升序维护
double globalOpacity_ = 1.0; // 整体透明度(两级第二级,独立存储,不烘焙进 rows_ 的 alpha
double vmin_ = 0.0;
double vmax_ = 0.0;
std::vector<double> samples_; // 数据原始标量(等积分层 + 直方图)

View File

@ -1,15 +1,21 @@
#include "DatasetCategory.hpp"
#include "repo/CategoryDescriptor.hpp"
namespace geopro::app {
CategoryBuckets splitByCategory(const std::vector<geopro::data::DsRow>& rows) {
const auto& cat = geopro::data::categoryCatalog();
const auto& cfg = categoryConfigs();
CategoryBuckets b;
b.segments.resize(cat.size());
for (const auto& r : rows)
for (std::size_t i = 0; i < cat.size(); ++i)
if (cat[i].classify && cat[i].classify(r)) { b.segments[i].push_back(r); break; }
b.segments.resize(cfg.size());
for (const auto& r : rows) {
int hit = -1;
// 先按 ddCode三维体/切片)——它们无 dsTypeCode来自 Api3dRepository mock 行)。
for (std::size_t i = 0; i < cfg.size() && hit < 0; ++i)
if (!cfg[i].ddCode.empty() && r.ddCode == cfg[i].ddCode) hit = static_cast<int>(i);
// 再按 dsTypeCode。
for (std::size_t i = 0; i < cfg.size() && hit < 0; ++i)
if (!cfg[i].dsTypeCode.empty() && r.dsTypeCode == cfg[i].dsTypeCode) hit = static_cast<int>(i);
if (hit >= 0) b.segments[static_cast<std::size_t>(hit)].push_back(r);
}
return b;
}

View File

@ -1,14 +1,15 @@
#pragma once
#include <vector>
#include "repo/CategoryConfig.hpp"
#include "repo/RepoTypes.hpp"
namespace geopro::app {
struct CategoryBuckets {
std::vector<std::vector<geopro::data::DsRow>> segments; // 与 categoryCatalog() 同序同长
std::vector<std::vector<geopro::data::DsRow>> segments; // 与 categoryConfigs() 同序同长
};
// 按 categoryCatalog() 把 ds 分入大类段:遍历目录,命中首个 classify(row)==true 的描述符即归入该段
// 按 CategoryConfig 把 ds 分入大类段:先判 ddCode 白名单(三维体/切片),否则按 dsTypeCode 匹配
// 不在表内的丢弃(接地电阻/原始数据/白化/坐标等)。保留原顺序。
CategoryBuckets splitByCategory(const std::vector<geopro::data::DsRow>& rows);

View File

@ -0,0 +1,31 @@
#include "DatasetDimension.hpp"
namespace geopro::app {
namespace {
// 与 LocalSample3dRepository::dimensionOf 同一映射spec §6.1)。
enum class Dim { D3, D2, Analysis, Other };
Dim dimOf(const std::string& c) {
if (c == "dd_voxel" || c == "dd_Structual3D" || c == "dd_Property3D" ||
c == "dd_section" || c == "dd_inversion_data")
return Dim::D3;
if (c == "dd_slice") return Dim::Analysis;
if (c == "dd_trajectory_data") return Dim::D2;
return Dim::Other;
}
} // namespace
DimBuckets splitByDimension(const std::vector<geopro::data::DsRow>& rows) {
DimBuckets b;
for (const auto& r : rows) {
switch (dimOf(r.ddCode)) {
case Dim::D3: b.dim3D.push_back(r); break;
case Dim::D2: b.dim2D.push_back(r); break;
case Dim::Analysis: b.analysis.push_back(r); break;
case Dim::Other: break;
}
}
return b;
}
} // namespace geopro::app

View File

@ -0,0 +1,17 @@
#pragma once
#include <vector>
#include "repo/RepoTypes.hpp"
namespace geopro::app {
struct DimBuckets {
std::vector<geopro::data::DsRow> dim3D;
std::vector<geopro::data::DsRow> dim2D;
std::vector<geopro::data::DsRow> analysis;
};
// 按 ddCode 把 ds 分流到 三维数据集 / 二维数据集 / 三维分析 三栏。
// Other 维度不入任何栏(保留原顺序)。
DimBuckets splitByDimension(const std::vector<geopro::data::DsRow>& rows);
} // namespace geopro::app

View File

@ -23,14 +23,12 @@ EmptyAwareComboBox::EmptyAwareComboBox(QWidget* parent) : QComboBox(parent) {
int EmptyAwareComboBox::realItemCount() const {
int n = 0;
auto* m = model();
for (int i = 0; i < count(); ++i) {
// 排除临时「暂无数据」占位项。
if (itemData(i, kEmptyHintRole).toBool()) continue;
// 排除不可选项(禁用 / NoItemFlags。用 model()->flags() 正确取项标志——
// 原 itemData(i, UserRole-1) 不是 Qt 的 flags 角色,对正常项恒返回不可选 →
// realItemCount 恒 0 → 有真实项也误插「暂无数据」(用户实测:异常区下方多一条暂无数据)。
if (m && !(m->flags(m->index(i, modelColumn())) & Qt::ItemIsSelectable)) continue;
// 排除不可选项(禁用 / NoItemFlags它们不构成「真实可选数据」。
if (!(itemData(i, Qt::UserRole - 1).value<Qt::ItemFlags>() & Qt::ItemIsSelectable))
continue;
++n;
}
return n;

View File

@ -55,11 +55,9 @@ QVBoxLayout* dialogRoot(QDialog* dlg);
// 与「数据详情 / 属性面板」同款的卡片面。返回 QFrame其 layout() 即 QVBoxLayout向内 addSection/addLayout。
QFrame* formCard(QWidget* parent);
QVBoxLayout* cardBody(QFrame* card); // 取 formCard 的内层 QVBoxLayout便捷器
// 标准底部按钮栏QDialogButtonBox(Ok|Cancel),已接 accept/reject。
// 默认中文「确定/取消」(不依赖 Qt 翻译是否就位);调用方可覆盖(如「生成/取消」)。
QDialogButtonBox* addDialogButtons(QVBoxLayout* root, QDialog* dlg,
const QString& okText = QStringLiteral("确定"),
const QString& cancelText = QStringLiteral("取消"));
// 标准底部按钮栏QDialogButtonBox(Ok|Cancel),已接 accept/rejectokText/cancelText 可定制文案。
QDialogButtonBox* addDialogButtons(QVBoxLayout* root, QDialog* dlg, const QString& okText = QString(),
const QString& cancelText = QString());
// ── 可编辑表单§7.0 统一度量DynamicFormEditor 与各参数对话框共用,单一真相)──────
QFormLayout* makeEditForm(); // 右对齐标签 + 标准行距/列距

View File

@ -1,27 +1,13 @@
#include "SliceExport.hpp"
#include <algorithm>
#include <cmath>
#include <fstream>
#include <QColor>
#include <QImage>
#include <QPainter>
#include <QPainterPath>
#include <QPainterPathStroker>
#include <QPointF>
#include <QPolygonF>
#include <QRect>
#include <vtkCamera.h>
#include <vtkDataArray.h>
#include <vtkImageData.h>
#include <vtkNew.h>
#include <vtkPNGWriter.h>
#include <vtkPointData.h>
#include <vtkRenderWindow.h>
#include <vtkRenderer.h>
#include <vtkRendererCollection.h>
#include <vtkWindowToImageFilter.h>
namespace geopro::app {
@ -55,178 +41,6 @@ bool captureRenderWindowPng(vtkRenderWindow* win, const std::string& path, int&
return writer->GetErrorCode() == 0;
}
bool captureFramedRegionPng(vtkRenderWindow* win, const double regionBounds[6], double padFactor,
double minExtent, const std::string& path, int& outW, int& outH) {
outW = outH = 0;
if (win == nullptr || path.empty()) return false;
vtkRenderer* ren =
win->GetRenderers() ? win->GetRenderers()->GetFirstRenderer() : nullptr;
vtkCamera* cam = ren ? ren->GetActiveCamera() : nullptr;
if (ren == nullptr || cam == nullptr)
return captureRenderWindowPng(win, path, outW, outH); // 无渲染器 → 退回整窗
// 1) 区域包围盒minExtent 兜底(点零体积/共面零厚度) → padFactor 以中心外扩留边距。
double b[6];
for (int i = 0; i < 3; ++i) {
const double lo = regionBounds[2 * i], hi = regionBounds[2 * i + 1];
const double c = 0.5 * (lo + hi);
double half = 0.5 * (hi - lo);
if (2.0 * half < minExtent) half = 0.5 * minExtent; // 退化轴兜底
half *= padFactor; // 外扩边距
b[2 * i] = c - half;
b[2 * i + 1] = c + half;
}
// 2) 存相机现场ResetCamera 改 position/focalPoint/clipping/parallelScale
double pos[3], fp[3], up[3], clip[2];
cam->GetPosition(pos);
cam->GetFocalPoint(fp);
cam->GetViewUp(up);
cam->GetClippingRange(clip);
const double va = cam->GetViewAngle();
const double ps = cam->GetParallelScale();
// 3) 重构图:保持视角方向,仅推近/缩放框住外扩区域。
ren->ResetCamera(b);
// 4) 截图(后台缓冲 + 关交换 → 屏幕不闪)。
vtkNew<vtkWindowToImageFilter> w2i;
w2i->SetInput(win);
w2i->ReadFrontBufferOff();
w2i->Update();
if (auto* img = w2i->GetOutput()) {
int dims[3];
img->GetDimensions(dims);
outW = dims[0];
outH = dims[1];
}
vtkNew<vtkPNGWriter> writer;
writer->SetFileName(path.c_str());
writer->SetInputConnection(w2i->GetOutputPort());
writer->Write();
const bool ok = writer->GetErrorCode() == 0;
// 5) 还原相机 + 重绘回原视图。
cam->SetPosition(pos);
cam->SetFocalPoint(fp);
cam->SetViewUp(up);
cam->SetViewAngle(va);
cam->SetParallelScale(ps);
cam->SetClippingRange(clip);
win->Render();
return ok;
}
bool captureAnomalyShotFromSlice(vtkImageData* colorImg, const double o[3], const double p1[3],
const double p2[3],
const std::vector<std::array<double, 3>>& worldPts, int markType,
const std::string& outlineHex, const std::string& path, int& outW,
int& outH) {
outW = outH = 0;
if (colorImg == nullptr || worldPts.empty() || path.empty()) return false;
int dims[3];
colorImg->GetDimensions(dims);
const int nx = dims[0], ny = dims[1];
if (nx < 2 || ny < 2) return false;
// 平面两轴(image i↔e1=p1-o, j↔e2=p2-o);世界点 → 归一(u,v) → 像素(QImage 顶左原点,需翻 j)。
const double e1[3] = {p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]};
const double e2[3] = {p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]};
const double L1 = e1[0] * e1[0] + e1[1] * e1[1] + e1[2] * e1[2];
const double L2 = e2[0] * e2[0] + e2[1] * e2[1] + e2[2] * e2[2];
if (L1 < 1e-12 || L2 < 1e-12) return false;
QPolygonF poly;
poly.reserve(static_cast<int>(worldPts.size()));
for (const auto& P : worldPts) {
const double d[3] = {P[0] - o[0], P[1] - o[1], P[2] - o[2]};
const double u = (d[0] * e1[0] + d[1] * e1[1] + d[2] * e1[2]) / L1;
const double v = (d[0] * e2[0] + d[1] * e2[1] + d[2] * e2[2]) / L2;
poly << QPointF(u * (nx - 1), (ny - 1) - v * (ny - 1)); // 翻 j 到 QImage 坐标
}
// 缓冲半径:异常包围盒对角的 15%,最小取图长边 4%(点/小异常也有可见外扩)。
const QRectF pb = poly.boundingRect();
const double diag = std::hypot(pb.width(), pb.height());
const double buffer = std::max(0.04 * std::max(nx, ny), 0.15 * diag);
// 按形态构 buffer 后的裁剪形状:点→圆、线→胶囊带、面→外扩多边形(填充 描边)。
QPainterPath shape;
if (markType == 1 || poly.size() == 1) {
shape.addEllipse(poly.first(), buffer, buffer);
} else if (markType == 2) {
QPainterPath line;
line.moveTo(poly.first());
for (int i = 1; i < poly.size(); ++i) line.lineTo(poly[i]);
QPainterPathStroker st;
st.setWidth(2.0 * buffer);
st.setCapStyle(Qt::RoundCap);
st.setJoinStyle(Qt::RoundJoin);
shape = st.createStroke(line);
} else {
QPainterPath fill;
fill.addPolygon(poly);
fill.closeSubpath();
QPainterPath outline = fill;
QPainterPathStroker st;
st.setWidth(2.0 * buffer);
st.setJoinStyle(Qt::RoundJoin);
shape = fill.united(st.createStroke(outline)); // 向外扩 buffer
}
// 裁剪区 = 形状包围盒(夹到图内)。
const QRect crop = shape.boundingRect().toAlignedRect().intersected(QRect(0, 0, nx, ny));
if (crop.width() < 1 || crop.height() < 1) return false;
// 切片着色图(vtk, j=0 在底) → QImage(顶左原点,翻行)。RGBA 保留外区透明(消除血缘外蓝边)。
const int comps = colorImg->GetNumberOfScalarComponents();
const bool rgba = comps >= 4;
QImage src(nx, ny, rgba ? QImage::Format_RGBA8888 : QImage::Format_RGB888);
for (int j = 0; j < ny; ++j) {
uchar* row = src.scanLine(ny - 1 - j);
for (int i = 0; i < nx; ++i) {
const auto* px = static_cast<unsigned char*>(colorImg->GetScalarPointer(i, j, 0));
if (rgba) {
row[i * 4] = px[0];
row[i * 4 + 1] = px[1];
row[i * 4 + 2] = px[2];
row[i * 4 + 3] = px[3];
} else {
row[i * 3] = px[0];
row[i * 3 + 1] = px[1];
row[i * 3 + 2] = px[2];
}
}
}
// 输出buffer 形状内贴剖面像素(外透明),再描异常轮廓。
QImage out(crop.size(), QImage::Format_ARGB32);
out.fill(Qt::transparent);
QPainter pr(&out);
pr.setRenderHint(QPainter::Antialiasing, true);
pr.translate(-crop.topLeft());
pr.setClipPath(shape);
pr.drawImage(0, 0, src);
pr.setClipping(false);
QColor oc(QString::fromStdString(outlineHex));
if (!oc.isValid()) oc = QColor(255, 48, 48);
QPen pen(oc);
pen.setWidthF(2.0);
pr.setPen(pen);
pr.setBrush(Qt::NoBrush);
if (markType == 1 || poly.size() == 1)
pr.drawEllipse(poly.first(), 4.0, 4.0); // 点:小标记
else if (markType == 2)
pr.drawPolyline(poly);
else
pr.drawPolygon(poly);
pr.end();
if (!out.save(QString::fromStdString(path), "PNG")) return false;
outW = out.width();
outH = out.height();
return true;
}
bool exportSliceDat(vtkImageData* slice, const std::string& path) {
if (slice == nullptr || path.empty()) return false;
vtkDataArray* arr = slice->GetPointData() ? slice->GetPointData()->GetScalars() : nullptr;

View File

@ -1,7 +1,5 @@
#pragma once
#include <array>
#include <string>
#include <vector>
class vtkImageData;
class vtkRenderWindow;
@ -14,26 +12,6 @@ bool exportSliceImagePng(vtkImageData* colorImage, const std::string& path);
// 截整个渲染窗口为 PNG异常标识截图需求 R88成功返回 true并填回截图像素宽高。
bool captureRenderWindowPng(vtkRenderWindow* win, const std::string& path, int& outW, int& outH);
// 「相机重构图」截图方案A把相机临时重新取景到 regionBounds圈定范围外扩后的区域
// 使异常框在画面中央带周边语境,再截图、还原相机。业界 frame/zoom-to-fit selection 范式。
// regionBounds: {xmin,xmax,ymin,ymax,zmin,zmax} 世界系圈定包围盒;
// padFactor: 以盒中心外扩的倍数1.4≈异常占画面~70%
// minExtent: 退化兜底(点=零体积、线/面共面=某轴零厚度)时各轴的最小世界尺寸。
// 视角方向不变(仅推近/缩放);屏幕无闪(后台缓冲+关交换)。失败回退整窗截图。
bool captureFramedRegionPng(vtkRenderWindow* win, const double regionBounds[6], double padFactor,
double minExtent, const std::string& path, int& outW, int& outH);
// 异常截图(正确做法):**只从切片那张 2D 剖面彩图**上,按异常几何**向外缓冲(buffer)一圈后裁剪**输出。
// 业界范式 = GIS「几何缓冲 + 按掩膜裁剪栅格」:点→圆、线→胶囊带、面→外扩多边形;缓冲外透明。
// colorImgselectedSliceColorImage() 的剖面 RGB 图o/p1/p2该切片平面三点(image i↔p1-o, j↔p2-o)
// worldPts异常顶点(世界系,落在该平面)markType1点/2线/3面outlineHex在裁图上描异常轮廓的颜色。
// 成功返回 true填回输出像素宽高。失败(无图/几何退化)返回 false调用方可回退。
bool captureAnomalyShotFromSlice(vtkImageData* colorImg, const double o[3], const double p1[3],
const double p2[3],
const std::vector<std::array<double, 3>>& worldPts, int markType,
const std::string& outlineHex, const std::string& path, int& outW,
int& outH);
// 把切片重采样 2D 标量影像写为 .dat 文本网格(行=j、列=i空格分隔每格取标量首分量成功返回 true。
bool exportSliceDat(vtkImageData* slice, const std::string& path);

View File

@ -52,9 +52,11 @@ constexpr double kRangeCeil = 30000.0; // 最多 30km(防远裁剪面失控)
constexpr int kMaxConcurrent = 12; // 瓦片请求最大并发(天地图 8 子域+Mapbox适度提高吞吐)
constexpr int kMinZoom = 3;
constexpr int kMaxZoom = 18;
constexpr double kGroundZ = 0.0; // 底图置于 z=0 地面参考(剖面深度向下为负,落其下)
constexpr double kZEps = 0.02; // 每层级 Z 微偏移:高层级压上面,避免共面瓦片 z-fighting
constexpr int kHardCap = 400; // 瓦片硬上限:超过则即便未落地也强制清理,兜底内存
constexpr double kPi = 3.14159265358979323846;
constexpr double kTerrainOpacity = 0.55; // 地形半透明:地下剖面可从任意角度透过地面看到(不再被遮挡)
// 地面起伏Mapbox terrain-RGB DEM 瓦片(原版 web 同款源,全球 CDN比 AWS Terrarium 快)。
// 公式 elev(米) = -10000 + (R*65536 + G*256 + B)*0.1。数据到 z15更高层级取祖先块。
@ -119,9 +121,8 @@ long long TileBasemap::tileKey(int z, int x, int y) {
}
TileBasemap::TileBasemap(geopro::render::Scene& scene, vtkRenderWindow* rw,
std::shared_ptr<geopro::core::GeoLocalFrame> frame, QObject* parent,
double groundZ)
: QObject(parent), scene_(scene), rw_(rw), frame_(std::move(frame)), groundZ_(groundZ) {}
std::shared_ptr<geopro::core::GeoLocalFrame> frame, QObject* parent)
: QObject(parent), scene_(scene), rw_(rw), frame_(std::move(frame)) {}
void TileBasemap::requestRender() {
// 合并渲染:同一事件循环轮次内的多次请求只渲染一帧(标准做法,避免逐瓦片重复 Render 卡顿)。
@ -139,10 +140,6 @@ void TileBasemap::requestRender() {
}
TileBasemap::~TileBasemap() {
// 移除本实例所有已贴瓦片:多实例(每 2D 平面一份)动态建销时,析构须撤回瓦片,否则渲染器仍持引用、
// 底图不随平面消失。共享 3D 底图存活至退出故旧码无此清理也无碍,但 per-plane 实例必须清。
if (auto* ren = scene_.renderer())
for (auto& kv : placed_) ren->RemoveViewProp(kv.second);
if (styleObs_ && observer_) styleObs_->RemoveObserver(observer_);
}
@ -211,23 +208,6 @@ void TileBasemap::setVerticalExaggeration(double ve) {
if (kind_ != Hidden) show(kind_); // 重建地形(高程×新VE),与剖面 VE 保持一致
}
void TileBasemap::setOpacity(double o) {
o = std::clamp(o, 0.0, 1.0);
if (o == opacity_) return;
opacity_ = o;
if (kind_ != Hidden) show(kind_); // 重建套用新透明度
}
void TileBasemap::setGroundZ(double z) {
// 直接平移平面高程:瓦片几何建于相对 z(仅含逐层级 z-fighting 偏移),平面高程 groundZ_ 经 actor position 施加。
// 拖 z 值滑块时只改所有已贴瓦片的 SetPosition无需重下载/重建;后续 refresh() 新瓦片经 placeActor 自动取新 groundZ_。
if (z == groundZ_) return;
groundZ_ = z;
for (auto& kv : placed_)
if (kv.second) kv.second->SetPosition(0.0, 0.0, groundZ_);
requestRender();
}
void TileBasemap::refineTile(int z, int x, int y, std::set<long long>& out, int& count) {
if (count >= kMaxLeaves) { out.insert(tileKey(z, x, y)); return; } // 安全上限:停止细分
const int n = 1 << z;
@ -252,11 +232,9 @@ void TileBasemap::refineTile(int z, int x, int y, std::set<long long>& out, int&
const double cx = (sw.x + ne.x) * 0.5, cy = (sw.y + ne.y) * 0.5; // 瓦片中心(局部米)
const double g = std::max(std::abs(ne.x - sw.x), std::abs(ne.y - sw.y)); // 瓦片地面尺寸(米)
// 距离上限(按剖面范围动态):以覆盖中心(相机焦点 cenX_,cenY_)为心,瓦片离它太远则不加载——
// 远裁剪面有界(剖面不被近裁剪面切),也避免拉远无限铺。叶块本身可大于此距离(近端仍在范围内即保留)。
// 心改用焦点而非原点(0,0):否则 frame 锚在别处数据(如深圳)时,看台湾数据全被剔除→底图空。
const double rx = cx - cenX_, ry = cy - cenY_;
if (std::sqrt(rx * rx + ry * ry) - g * 0.5 > maxTileDist_) return;
// 距离上限(按剖面范围动态):数据中心在局部原点(0,0);瓦片离它太远则不加载——远裁剪面有界
// (剖面不被近裁剪面切),也避免拉远无限铺。叶块本身可大于此距离(其近端仍在范围内即保留)。
if (std::sqrt(cx * cx + cy * cy) - g * 0.5 > maxTileDist_) return;
// 该瓦片投影到屏幕的近似像素尺寸 > 阈值且未到最大层级 → 细分为 4 子块(近处更细)。
double screenPx;
@ -308,10 +286,6 @@ void TileBasemap::refresh() {
if (pl[0] * fp[0] + pl[1] * fp[1] + pl[2] * fp[2] + pl[3] < 0.0)
for (int k = 0; k < 4; ++k) pl[k] = -pl[k];
}
// 底图覆盖中心 = 相机焦点(用户正看处)的局部 XY而非世界原点frame 锚在首个数据集,看远处别处
// 数据时原点离视野很远会把全部瓦片距离剔除→底图空。焦点为心则底图随视野走(同 frame 仍与数据对齐)。
cenX_ = fp[0];
cenY_ = fp[1];
// 底图最大距离按当前剖面合并范围动态定(随勾选增删自动伸缩);无数据用下限。
maxTileDist_ = kRangeFloor;
@ -320,10 +294,10 @@ void TileBasemap::refresh() {
if (r > 0.0) maxTileDist_ = std::clamp(r * kRangeFactor, kRangeFloor, kRangeCeil);
}
// 四叉树:从覆盖中心(相机焦点经纬)一圈粗根块出发,按屏幕误差细分 → 近细远粗、铺满视野,无盲区。
// 四叉树:从数据中心一圈粗根块出发,按屏幕误差细分 → 近细远粗、铺满视野,无单层级盲区。
desired_.clear();
int count = 0;
const auto c = frame_->toLatLon(cenX_, cenY_); // 覆盖中心 = 相机焦点(非世界原点)
const auto c = frame_->toLatLon(0.0, 0.0); // 数据中心
const geopro::render::TileXY root = geopro::render::lonLatToTile(c.lon, c.lat, kRootZoom);
for (int dy = -1; dy <= 1; ++dy)
for (int dx = -1; dx <= 1; ++dx)
@ -449,7 +423,6 @@ void TileBasemap::fetchTile(int z, int x, int y, long long key) {
void TileBasemap::placeActor(long long key, vtkSmartPointer<vtkActor> actor) {
if (!actor) return;
actor->SetPosition(0.0, 0.0, groundZ_); // 平面高程经 position 施加:几何建于相对 z此处抬到当前 groundZ_
scene_.addActor(actor);
placed_[key] = actor;
}
@ -460,7 +433,7 @@ vtkSmartPointer<vtkActor> TileBasemap::buildFlat(int z, int x, int y,
const auto sw = frame_->toLocal(b.south, b.west);
const auto se = frame_->toLocal(b.south, b.east);
const auto nw = frame_->toLocal(b.north, b.west);
const double gz = (z - kMinZoom) * kZEps; // 仅逐层级 z-fighting 偏移(相对 z);平面高程由 actor position 施加
const double gz = kGroundZ + (z - kMinZoom) * kZEps; // 高层级略抬高,压在旧层之上防共面闪烁
// PlaneSource 自动 tcoordorigin=SW→u 西0东1、v 南0北1与翻转后纹理对齐
vtkNew<vtkPlaneSource> plane;
@ -473,7 +446,7 @@ vtkSmartPointer<vtkActor> TileBasemap::buildFlat(int z, int x, int y,
actor->SetMapper(mapper);
actor->SetTexture(tex);
actor->GetProperty()->LightingOff(); // 底图不受场景光照
actor->GetProperty()->SetOpacity(opacity_); // 半透明:不遮挡地下剖面
actor->GetProperty()->SetOpacity(kTerrainOpacity); // 半透明:不遮挡地下剖面
// 注意UseBounds 保持默认 true → 参与相机裁剪面计算,否则底图会被裁剪面"蒙版"切掉。
// 坐标轴/取景不被底图撑大,由 VtkSceneView 改用"数据自身包围盒"解决(非靠 UseBounds=false
return actor;
@ -549,7 +522,7 @@ vtkSmartPointer<vtkActor> TileBasemap::buildWarped(int sz, int sx, int sy, int d
const auto sw = frame_->toLocal(sb.south, sb.west);
const auto se = frame_->toLocal(sb.south, sb.east);
const auto nw = frame_->toLocal(sb.north, sb.west);
const double base = (sz - kMinZoom) * kZEps; // 仅逐层级 z-fighting 偏移(相对 z);平面高程由 actor position 施加
const double base = kGroundZ + (sz - kMinZoom) * kZEps;
// PlaneSource(等距圆柱下平面插值即正确 x/y) + 自动 tcoord再按各点真实经纬采 DEM 位移 Z。
vtkNew<vtkPlaneSource> plane;
@ -587,7 +560,7 @@ vtkSmartPointer<vtkActor> TileBasemap::buildWarped(int sz, int sx, int sy, int d
actor->SetMapper(mapper);
actor->SetTexture(tex);
actor->GetProperty()->LightingOff();
actor->GetProperty()->SetOpacity(opacity_); // 半透明:不遮挡地下剖面
actor->GetProperty()->SetOpacity(kTerrainOpacity); // 半透明:不遮挡地下剖面
return actor; // UseBounds 默认 true参与裁剪面避免被"蒙版"切掉
}

View File

@ -33,16 +33,13 @@ class TileBasemap : public QObject {
public:
enum Kind { Street = 0, Satellite = 1, Hidden = 2 };
TileBasemap(geopro::render::Scene& scene, vtkRenderWindow* rw,
std::shared_ptr<geopro::core::GeoLocalFrame> frame, QObject* parent = nullptr,
double groundZ = 0.0);
std::shared_ptr<geopro::core::GeoLocalFrame> frame, QObject* parent = nullptr);
~TileBasemap() override;
void show(Kind kind); // 显示某底图Hidden 等同 hide记住类型供 LOD 刷新复用
void hide(); // 移除全部瓦片
void refresh(); // 按当前相机重算层级+覆盖,增量更新瓦片(交互结束回调)
void setVerticalExaggeration(double ve); // 地形垂向夸张(须与剖面 VE 一致才对齐)
void setOpacity(double o); // 底图半透明度[0,1],供渲染工具栏底图弹窗调节
void setGroundZ(double z); // 直接平移底图平面 z(拖 z 值滑块):改所有已贴瓦片 actor 的 position无重铺
// 数据半径提供者:刷新时查询当前所有勾选剖面的合并范围(半径,米),据此动态定底图最大范围。
void setDataRadiusProvider(std::function<double()> fn) { dataRadiusProvider_ = std::move(fn); }
@ -78,9 +75,7 @@ private:
std::set<long long> inFlight_; // 在途瓦片(续到起伏/平面最终落地)
std::map<long long, QImage> demCache_; // DEM 块缓存(key=DEMz/x/y),跨隐藏/重选复用
std::map<long long, vtkSmartPointer<vtkTexture>> texCache_; // 影像纹理缓存,重选/缩放回看免重拉
double groundZ_ = 0.0; // 底图地面参考 z(per-instance)3D 底图=02D 平面底图=各类型平面高程
double ve_ = 1.0; // 地形垂向夸张(与剖面 verticalExaggeration 一致才对齐)
double opacity_ = 0.5; // 底图半透明:地下剖面可从任意角度透过地面看到(不再被遮挡)
double maxTileDist_ = 2000.0; // 底图最大距离(米),每次刷新按剖面范围动态算
std::function<double()> dataRadiusProvider_; // 返回当前勾选剖面合并范围的半径
// 卫星层「学习到的」最大可用层级:天地图卫星影像各区域覆盖深度不同(内地到z18, 台湾等仅到z16),
@ -89,10 +84,6 @@ private:
int satMaxZoom_ = 18;
// 四叉树当前帧相机参数(refresh 写, refineTile 读):相机位置 + 投影系数 + 视锥 6 面。
double camX_ = 0, camY_ = 0, camZ_ = 0;
// 底图覆盖中心(相机焦点的局部 XY):四叉树根块取此处经纬、距离剔除以此为心。
// 关键——不能用世界原点(0,0)frame 锚在首个数据集(如深圳),看远处别处数据(如台湾,相距数百公里)时
// 原点离视野数百公里→全部瓦片被距离剔除→底图空。改用焦点→底图随视野走(瓦片与数据同 frame 仍对齐)。
double cenX_ = 0, cenY_ = 0;
double projK_ = 1.0;
bool projParallel_ = false;
double frustum_[24] = {0}; // 6 个视锥平面(内法向)AABB 全在某面外则剔除

View File

@ -1,37 +0,0 @@
#pragma once
#include <functional>
#include <memory>
#include <utility>
#include "TileBasemap.hpp"
#include "controller/DatasetRenderStrategy.hpp" // geopro::controller::IPlaneBasemap
namespace geopro::render { class Scene; }
namespace geopro::core { class GeoLocalFrame; }
class vtkRenderWindow;
namespace geopro::app {
// 2D 平面底图适配器:把 app 层 TileBasemap 适配到控制器层抽象 IPlaneBasemap
// 使 geopro_controller(仅链 geopro_data+Qt::Core) 不反依赖 app 层与 VTK仿 I3dSceneView/VtkSceneView
// main.cpp 经底图工厂按平面 z 造之;持 TileBasemap 值成员,随适配器析构而析构(移除瓦片→底图随平面消失)。
class TileBasemapPlaneAdapter : public geopro::controller::IPlaneBasemap {
public:
TileBasemapPlaneAdapter(geopro::render::Scene& scene, vtkRenderWindow* rw,
std::shared_ptr<geopro::core::GeoLocalFrame> frame, double groundZ,
std::function<double()> radiusProvider)
: bm_(scene, rw, std::move(frame), nullptr, groundZ) {
bm_.setDataRadiusProvider(std::move(radiusProvider));
}
void show(int kind) override {
bm_.show(kind == 0 ? TileBasemap::Street : TileBasemap::Hidden);
}
void hide() override { bm_.hide(); }
void setOpacity(double o) override { bm_.setOpacity(o); }
void setGroundZ(double z) override { bm_.setGroundZ(z); }
private:
TileBasemap bm_;
};
} // namespace geopro::app

View File

@ -119,6 +119,48 @@ QPixmap renderAvatar(const QString& initials, int px, const QColor& bg, const QC
}
// ── 四个菜单(结构对齐需求;叶子项当前为静态占位,后续接真实页面)──
QMenu* buildViewMenu(QWidget* p)
{
auto* m = new QMenu(QStringLiteral("视图"), p);
m->addAction(QStringLiteral("分析视图"));
m->addAction(QStringLiteral("大屏视图"));
return m;
}
QMenu* buildProjectMenu(QWidget* p)
{
auto* m = new QMenu(QStringLiteral("项目管理"), p);
m->addAction(QStringLiteral("数据视图"));
auto* cfg = m->addMenu(QStringLiteral("项目配置"));
cfg->addAction(QStringLiteral("基本信息"));
cfg->addAction(QStringLiteral("项目结构"));
cfg->addAction(QStringLiteral("视图配置"));
m->addAction(QStringLiteral("数据管理"));
auto* biz = m->addMenu(QStringLiteral("业务管理"));
biz->addAction(QStringLiteral("异常管理"));
biz->addAction(QStringLiteral("异常体管理"));
auto* mon = m->addMenu(QStringLiteral("在线监测"));
mon->addAction(QStringLiteral("项目设备"));
mon->addAction(QStringLiteral("在线任务管理"));
auto* doc = m->addMenu(QStringLiteral("项目资料管理"));
doc->addAction(QStringLiteral("项目资料管理"));
doc->addAction(QStringLiteral("报告列表"));
auto* tools = m->addMenu(QStringLiteral("工具组件"));
tools->addAction(QStringLiteral("装置与脚本"));
tools->addAction(QStringLiteral("色阶配置"));
tools->addAction(QStringLiteral("异常类型管理"));
tools->addAction(QStringLiteral("模型管理"));
auto* exp = m->addMenu(QStringLiteral("批量导出"));
exp->addAction(QStringLiteral("文件导出"));
exp->addAction(QStringLiteral("报告导出"));
auto* alarm = m->addMenu(QStringLiteral("告警管理"));
alarm->addAction(QStringLiteral("设备告警"));
alarm->addAction(QStringLiteral("告警查询"));
m->addAction(QStringLiteral("自动任务"));
m->addAction(QStringLiteral("模板管理"));
return m;
}
QMenu* buildToolsMenu(QWidget* p)
{
auto* m = new QMenu(QStringLiteral("业务工具"), p);
@ -129,6 +171,14 @@ QMenu* buildToolsMenu(QWidget* p)
return m;
}
QMenu* buildDeviceMenu(QWidget* p)
{
auto* m = new QMenu(QStringLiteral("设备"), p);
m->addAction(QStringLiteral("连接设备"));
m->addAction(QStringLiteral("设备管理"));
return m;
}
} // namespace
TopBar::TopBar(QWidget* parent) : QWidget(parent) {
@ -209,10 +259,10 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
// 一级菜单 → 工具条按钮(视图/项目管理/业务工具/设备),纯文字 + 下拉箭头。
// 复用原菜单构造器;菜单作为 popup 挂到按钮(按钮文字取菜单标题)。
lay->addWidget(makeMenuButton(this, buildViewMenu()));
lay->addWidget(makeMenuButton(this, buildProjectMenu()));
lay->addWidget(makeMenuButton(this, buildViewMenu(this)));
lay->addWidget(makeMenuButton(this, buildProjectMenu(this)));
lay->addWidget(makeMenuButton(this, buildToolsMenu(this)));
lay->addWidget(makeMenuButton(this, buildDeviceMenu()));
lay->addWidget(makeMenuButton(this, buildDeviceMenu(this)));
lay->addStretch();
@ -282,64 +332,6 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
lay->addWidget(userRow_);
}
// 视图菜单。「分析视图」=默认工作台emit analysisViewRequested中央区从 web 整窗切回工作台);
// 「大屏视图」当前为占位。
QMenu* TopBar::buildViewMenu() {
auto* m = new QMenu(QStringLiteral("视图"), this);
QObject::connect(m->addAction(QStringLiteral("分析视图")), &QAction::triggered, this,
[this] { emit analysisViewRequested(); });
m->addAction(QStringLiteral("大屏视图"));
return m;
}
// 项目管理菜单。仅保留需「直接嵌入」的 web 页Excel「单个项目」页签第 10~21 行带嵌入地址者):
// 在线监测 / 工具组件 / 批量导出 / 告警管理,点击 emit webPageRequested由 main 独立整窗加载。
// 其余全部隐藏(数据视图、项目配置、数据管理、业务管理、项目资料管理、自动任务、模板管理 …)。
QMenu* TopBar::buildProjectMenu() {
auto* m = new QMenu(QStringLiteral("项目管理"), this);
// web 叶子项:携带 target 路径,点击发信号。
auto addWeb = [this](QMenu* parent, const QString& title, const QString& target) {
auto* a = parent->addAction(title);
QObject::connect(a, &QAction::triggered, this,
[this, title, target] { emit webPageRequested(title, target); });
};
auto* mon = m->addMenu(QStringLiteral("在线监测"));
addWeb(mon, QStringLiteral("项目设备"), QStringLiteral("/projectSpace/onlineMonitor/projectDevice"));
addWeb(mon, QStringLiteral("在线任务管理"), QStringLiteral("/projectSpace/onlineMonitor/onlineTask"));
auto* tools = m->addMenu(QStringLiteral("工具组件"));
addWeb(tools, QStringLiteral("装置与脚本"), QStringLiteral("/projectSpace/toolComponent/deviceScript"));
addWeb(tools, QStringLiteral("色阶配置"), QStringLiteral("/projectSpace/toolComponent/levelConfigure"));
addWeb(tools, QStringLiteral("异常类型管理"), QStringLiteral("/projectSpace/toolComponent/exceptionType"));
addWeb(tools, QStringLiteral("模型管理"), QStringLiteral("/projectSpace/toolComponent/modelManage"));
auto* exp = m->addMenu(QStringLiteral("批量导出"));
addWeb(exp, QStringLiteral("文件导出"), QStringLiteral("/projectSpace/bulkExport/fileExport"));
addWeb(exp, QStringLiteral("报告导出"), QStringLiteral("/projectSpace/bulkExport/templateExport"));
auto* alarm = m->addMenu(QStringLiteral("告警管理"));
addWeb(alarm, QStringLiteral("设备告警"), QStringLiteral("/projectSpace/alarmManage/deviceAlarm"));
addWeb(alarm, QStringLiteral("告警查询"), QStringLiteral("/projectSpace/alarmManage/alarmQuery"));
return m;
}
// 设备菜单。连接设备/设备管理为占位;「导入雷达测线」是后端未就绪期的过渡测试入口,集中到设备菜单。
// 原入口在三维体段头按钮(已移除);子项规范化/Impulse 分别 emit radarImportRequested(false/true)
// 由 main 接到既有导入流程。
QMenu* TopBar::buildDeviceMenu() {
auto* m = new QMenu(QStringLiteral("设备"), this);
m->addAction(QStringLiteral("连接设备"));
m->addAction(QStringLiteral("设备管理"));
QMenu* radar = m->addMenu(QStringLiteral("导入雷达测线"));
radar->addAction(QStringLiteral("规范化测线目录(.head/.data)…"), this,
[this] { emit radarImportRequested(false); });
radar->addAction(QStringLiteral("Impulse 测线目录(.iprb)…"), this,
[this] { emit radarImportRequested(true); });
return m;
}
bool TopBar::eventFilter(QObject* obj, QEvent* event) {
if (obj == userRow_ && event->type() == QEvent::MouseButtonRelease) {
if (userMenu_)

View File

@ -29,17 +29,8 @@ signals:
void allProjectsRequested(); // 点击"全部项目…"
void logoutRequested(); // 头像菜单「退出登录」
void settingsRequested(); // 点击齿轮图标 → 打开设置
// 项目管理菜单中「直接嵌入」的 web 页被点击title=窗口标题target=嵌入页 target 路径。
void webPageRequested(const QString& title, const QString& target);
void analysisViewRequested(); // 视图菜单「分析视图」→ 中央区切回默认工作台
// 设备菜单「导入雷达测线」(后端未就绪的过渡测试入口)false=规范化(.head/.data)true=Impulse(.iprb)。
void radarImportRequested(bool impulse);
private:
QMenu* buildViewMenu(); // 视图菜单(成员:「分析视图」需 emit 信号)
QMenu* buildProjectMenu(); // 项目管理菜单成员webview 叶子项需 emit 信号)
QMenu* buildDeviceMenu(); // 设备菜单(成员:「导入雷达测线」子项需 emit 信号)
QToolButton* wsBtn_ = nullptr;
QToolButton* projBtn_ = nullptr;
QWidget* userRow_ = nullptr; // 用户区整块(头像+姓名/职务+箭头)

View File

@ -31,7 +31,7 @@ namespace {
constexpr double kDefCellXY = 1.0;
constexpr double kDefCellZ = 0.5;
constexpr double kDefPower = 2.0;
constexpr double kDefMaxDist = 0.0; // 0=自动「覆盖测区」(全数据 IDW + 凸包足迹裁剪,对齐 Surfer)
constexpr double kDefMaxDist = 4.0;
constexpr int kRoleDsId = Qt::UserRole + 1; // 源树项存 dsId
constexpr int kRoleMountId = Qt::UserRole + 1; // 生成位置树项存 id
constexpr int kRoleMountConfType = Qt::UserRole + 2; // 生成位置树项存 confType
@ -187,14 +187,11 @@ VolumeParamsDialog::VolumeParamsDialog(const QVector<VolumeSourceItem>& sources,
cellXY_ = makeSpin(kDefCellXY, 0.01, 1000.0, 0.5, 2);
cellZ_ = makeSpin(kDefCellZ, 0.01, 1000.0, 0.5, 2);
power_ = makeSpin(kDefPower, 0.5, 6.0, 0.5, 1);
maxDist_ = makeSpin(kDefMaxDist, 0.0, 10000.0, 1.0, 2);
// maxDist=0=最小值)→ 显示「自动」:全数据 IDW + 凸包足迹裁剪填满测区(对齐客户 Surfer
// >0 → 局部 IDW 半径(剖面附近清晰、跨大空隙可能填不满)。
maxDist_->setSpecialValueText(QStringLiteral("自动 (覆盖测区)"));
maxDist_ = makeSpin(kDefMaxDist, 0.1, 10000.0, 1.0, 2);
form2->addRow(formkit::editLabel(QStringLiteral("水平间距 (米)")), cellXY_);
form2->addRow(formkit::editLabel(QStringLiteral("竖向间距 (米)")), cellZ_);
form2->addRow(formkit::editLabel(QStringLiteral("IDW 幂次")), power_);
form2->addRow(formkit::editLabel(QStringLiteral("最大影响距离 (米, 0=自动)")), maxDist_);
form2->addRow(formkit::editLabel(QStringLiteral("最大影响距离 (米)")), maxDist_);
cardLay->addLayout(form2);
cols->addWidget(card, 1);

View File

@ -40,9 +40,7 @@ VolumePropertiesDialog::VolumePropertiesDialog(const QString& name, const Volume
.row(QStringLiteral("网格间距"), QStringLiteral("XY=%1 m Z=%2 m")
.arg(info.params.cellXY, 0, 'f', 2)
.arg(info.params.cellZ, 0, 'f', 2))
.row(QStringLiteral("超距"), info.params.maxDist > 0.0
? QStringLiteral("%1 m").arg(info.params.maxDist, 0, 'f', 2)
: QStringLiteral("自动 (覆盖测区)"))
.row(QStringLiteral("超距"), QStringLiteral("%1 m").arg(info.params.maxDist, 0, 'f', 2))
.row(QStringLiteral("色阶来源"),
info.params.colorScaleId.empty() ? QStringLiteral("首个源数据集")
: QString::fromStdString(info.params.colorScaleId));

View File

@ -1,7 +1,6 @@
#include "VtkSceneView.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <memory>
#include <utility>
@ -10,28 +9,13 @@
#include <QString>
#include <vtkActor.h>
#include <vtkBillboardTextActor3D.h>
#include <vtkCallbackCommand.h>
#include <vtkCamera.h>
#include <vtkCommand.h>
#include <vtkProperty.h>
#include <vtkBoundingBox.h>
#include <vtkCubeAxesActor.h>
#include <vtkLineSource.h>
#include <vtkNew.h>
#include <vtkPolyDataMapper.h>
#include <vtkProp.h>
#include <vtkPropPicker.h>
#include <vtkTextProperty.h>
#include <vtkPiecewiseFunction.h>
#include <vtkColorTransferFunction.h>
#include <vtkGPUVolumeRayCastMapper.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkSphereSource.h>
#include <vtkVolume.h>
#include <vtkVolumeProperty.h>
#include "CameraPreset.hpp"
#include "Scene.hpp"
@ -87,25 +71,6 @@ VtkSceneView::VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* render
// 近裁剪容差调小:场景含近处剖面 + 远处底图(几十km),默认容差会把近裁剪面随远面推出去、
// 切掉离相机近的剖面。调小后近面可贴近,剖面不被切(代价:远处深度精度略降,不可见层无所谓)。
scene_.renderer()->SetNearClippingPlaneTolerance(1e-5);
ensureGnomon(); // 交互器若已就绪即装配角落方向标(否则首帧 render 时补装)
}
VtkSceneView::~VtkSceneView() {
// 摘除左键/相机观察者clientData=this本对象析构后若留存会悬垂+ 移除叠加渲染器。
// 渲染窗口/交互器可能已在 Qt 拆台中先行析构,全程判空。
if (renderWindow_) {
if (auto* iren = renderWindow_->GetInteractor()) {
if (gnomonClickTag_ != 0) iren->RemoveObserver(gnomonClickTag_);
if (gnomonHoverTag_ != 0) iren->RemoveObserver(gnomonHoverTag_);
}
}
gnomonClickTag_ = 0;
gnomonHoverTag_ = 0;
// 主相机由 scene_ 渲染器持有、生命周期覆盖本对象(构造契约),析构时仍在 → 可安全摘观察者。
if (gnomonObservedCam_ && gnomonCamTag_ != 0) gnomonObservedCam_->RemoveObserver(gnomonCamTag_);
gnomonCamTag_ = 0;
gnomonObservedCam_ = nullptr;
if (renderWindow_ && gnomonRenderer_) renderWindow_->RemoveRenderer(gnomonRenderer_);
}
void VtkSceneView::removeProps(std::vector<vtkSmartPointer<vtkProp>>& props) {
@ -115,14 +80,12 @@ void VtkSceneView::removeProps(std::vector<vtkSmartPointer<vtkProp>>& props) {
}
bool VtkSceneView::computeDataBounds(double out[6]) const {
// 仅计「可见」prop二维分析下 3D 体/帘面已隐藏,取景/坐标轴/底图范围都应只围当前可见维度,
// 否则二维取景被隐藏的远处 3D 体撑歪、坐标轴框错维度。
vtkBoundingBox bb;
for (const auto& kv : dsProps_)
for (const auto& p : kv.second)
if (p && p->GetVisibility()) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
if (p) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
for (const auto& p : miscProps_)
if (p && p->GetVisibility()) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
if (p) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
if (!bb.IsValid()) return false;
bb.GetBounds(out);
return true;
@ -139,7 +102,6 @@ void VtkSceneView::clear() {
// 只移除数据 prop按 ds 跟踪)+ 杂项(地形/测线)+ 坐标轴;不动底图(TileBasemap 自管)→ 重建不丢图。
for (auto& kv : dsProps_) removeProps(kv.second);
dsProps_.clear();
mapLineDs_.clear(); // 2D 足迹归属记录随数据图元一并清
removeProps(miscProps_);
clearAnomalies(); // 异常 actor 随清场一并移除
if (currentAxes_) {
@ -151,17 +113,11 @@ void VtkSceneView::clear() {
volumeOwnerDs_.clear();
volumes_.clear(); // 多体并发:清场移除所有体 image
frameAnchoredToData_ = false; // 新一轮选择重新按其首个真实剖面重锚原点
useFittedAxes_ = false; // 清场:贴合轴复位为全场景轴(选中随数据一并失效,防残留旧盒)
if (onVolumeChanged) onVolumeChanged();
}
void VtkSceneView::setVerticalExaggeration(double ve) { verticalExaggeration_ = ve; }
void VtkSceneView::setVolumeOpacity(double /*maxOpacity*/) {
// 已退役:体不透明度统一由【色阶「不透明度」】控制(每单位 = 单色alpha × 色阶不透明度100%=实心)。
// 旧工具条「透明度」滑块移除;保留空实现仅为满足接口(无调用方)。
}
void VtkSceneView::addSurveyLine(const geopro::core::Grid& grid) {
auto line = geopro::render::buildSurveyLine(grid, *frame_);
if (line) {
@ -200,29 +156,6 @@ void VtkSceneView::addCurtain(const std::string& dsId, const geopro::core::Grid&
void VtkSceneView::addVolume(const std::string& dsId, const geopro::data::VolumeGrid& vol,
const geopro::core::ColorScale& cs) {
// 首次建体时一次性探测 GPU 体绘制支持(此刻 widget 已显示、GL 上下文就绪):不支持则全局回退
// SmartVolumeMapper(CPU),避免无独显/软件 GL/远程桌面上整个体渲不出(空值仍靠传函透明)。
static bool gpuProbed = false;
if (!gpuProbed && renderWindow_) {
gpuProbed = true;
// 关键addVolume 在普通 Qt 槽里跑GL 上下文未必 current → 先 MakeCurrent否则 IsRenderSupported
// 误判为不支持、把有独显的机器错误回退到 CPU体变稠密/分层)。再给真实传函属性供其判定。
renderWindow_->MakeCurrent();
vtkNew<vtkGPUVolumeRayCastMapper> probe;
vtkNew<vtkVolumeProperty> prop;
vtkNew<vtkColorTransferFunction> ctf;
ctf->AddRGBPoint(0.0, 1, 1, 1);
ctf->AddRGBPoint(1.0, 1, 1, 1);
vtkNew<vtkPiecewiseFunction> otf;
otf->AddPoint(0.0, 0.0);
otf->AddPoint(1.0, 1.0);
prop->SetColor(ctf);
prop->SetScalarOpacity(otf);
const bool ok = probe->IsRenderSupported(renderWindow_, prop) != 0;
geopro::render::setVolumeGpuSupported(ok);
qInfo().noquote() << "[volrender] GPU volume ray cast supported=" << ok
<< (ok ? "(GPU+mask 干净白化)" : "(回退 CPU SmartVolumeMapper边缘有细渗色)");
}
// 纵向夸张烤进 image 的 z 原点/间距(与帘面 SetScale 同倍,保证纵向一致)。
// 用暴露 image 的 buildVoxel 重载:保留 currentVolumeImage_ 供 P3 切片附着(几何含 VE
vtkSmartPointer<vtkImageData> image;
@ -241,72 +174,24 @@ void VtkSceneView::addVolume(const std::string& dsId, const geopro::data::Volume
currentVmin_ = vol.vmin;
currentVmax_ = vol.vmax;
volumeOwnerDs_ = dsId;
volumes_[dsId] = VolumeRec{image, cs, vol.vmin, vol.vmax, volume}; // 多体并发:登记本体 image+actor
// G3 等值面:在值域高段(0.7)抽不透明实心异常体(参考图红块)——【反演专属】。
// 雷达体(registerRadarDataset 产的 "radar-" id)跳过:振幅体的 0.7 阈值面=强反射层,
// 既无地球物理含义、又是 SetOpacity(1.0) 实色 actor【不受体不透明度控制】(用户实测:
// 体不透明度调 0 仍见灰色实面=就是它)。"radar-" 是该体唯一生产者指定的稳定 id。
// 注impulse-GPR("vol-")同为振幅体、亦不该有等值面,但 "vol-" 与反演共用前缀,
// 待 ddCode 贯通 addVolume 后再统一按类型门控(见 spec §11)。
const bool isRadarVolume = dsId.rfind("radar-", 0) == 0;
if (!isRadarVolume) {
const double isoVal = vol.vmin + 0.7 * (vol.vmax - vol.vmin);
auto iso = geopro::render::buildIsosurface(image, cs, vol.vmin, vol.vmax, isoVal);
if (iso) {
iso->PickableOff(); // 不参与拾取(同体 actor避免串选
scene_.addActor(iso);
dsProps_[dsId].push_back(iso);
}
}
volumes_[dsId] = VolumeRec{image, cs, vol.vmin, vol.vmax}; // 多体并发:登记本体 image
if (onVolumeChanged) onVolumeChanged();
}
}
bool VtkSceneView::updateVolumeColorInPlace(const std::string& dsId,
const geopro::core::ColorScale& cs) {
auto it = volumes_.find(dsId);
if (it == volumes_.end() || !it->second.volume) return false; // 未渲染 → 调用方回退 remove+add
// 仅换传函image 不变)→ 切片基底保持有效、不被关闭。等值面随阈值色变化较小,暂不重抽。
geopro::render::updateVolumeColors(it->second.volume, cs, it->second.vmin, it->second.vmax);
it->second.cs = cs;
currentColorScale_ = cs;
// onVolumeChanged → InteractionManager.setVolumeImage(同 image, 新 cs):检测 image 未变 → 不关切片,
// 仅更新体色阶并让该体下未保存切片跟随改色(见 InteractionManager::setVolumeImage
if (onVolumeChanged) onVolumeChanged();
if (renderWindow_) renderWindow_->Render();
return true;
}
void VtkSceneView::addMapLine(const std::string& dsId, const geopro::data::MapLine& line,
double worldZ) {
// 2D 足迹:经共享 frame 投影到世界 XY、Z=worldZ。按 dsId 跟踪(与帘面同 dsProps_ → removeDataset 复用)。
// worldZ 已是最终世界高程(含摆放语义),不再施加 VE足迹是水平线非随深度的竖直图元
// 足迹可能是首个(且唯一)带经纬的数据 → 与帘面同样重锚原点,否则按样本默认原点投到数百公里外不可见。
anchorFrameIfNeeded(line.lat, line.lon, static_cast<int>(line.lat.size()));
// 折线几何建于 Z=0平面高程 worldZ 经 actor SetPosition 施加 → 后续拖 z 值滑块只改 position 即直接平移,
// 无需移除+异步重载几何setMapLinesZ 走此)。首勾/后续勾选在当前平面 z 加入者立即摆到该 z。
auto actor = geopro::render::buildMapLine(line.lat, line.lon, 0.0, *frame_);
auto actor = geopro::render::buildMapLine(line.lat, line.lon, worldZ, *frame_);
if (actor) {
actor->SetPosition(0.0, 0.0, worldZ);
scene_.addActor(actor);
dsProps_[dsId].push_back(actor);
mapLineDs_.insert(dsId); // 记录此 ds 为 2D 足迹(供足迹归属识别)
}
}
void VtkSceneView::setMapLinesZ(const std::vector<std::string>& dsIds, double z) {
// 直接平移足迹:仅对属于足迹的 dsId 改其 actor 的 SetPosition(0,0,z),即时渲染,无移除+重载。
for (const auto& dsId : dsIds) {
if (!mapLineDs_.count(dsId)) continue;
auto it = dsProps_.find(dsId);
if (it == dsProps_.end()) continue;
for (auto& prop : it->second)
if (auto* a = vtkActor::SafeDownCast(prop)) a->SetPosition(0.0, 0.0, z);
}
if (renderWindow_) renderWindow_->Render();
}
void VtkSceneView::addTerrain(const geopro::data::TerrainPaths& paths) {
auto terrain = geopro::render::buildTerrain(paths.demPath, paths.imagePath, *frame_, zRefElev_,
verticalExaggeration_);
@ -321,10 +206,6 @@ void VtkSceneView::removeDataset(const std::string& dsId) {
if (it == dsProps_.end()) return;
removeProps(it->second);
dsProps_.erase(it);
mapLineDs_.erase(dsId); // 若是 2D 足迹则同步去除维度记录
// 场景已无任何数据图元 → 复位重锚标志:下个数据(可能在别处)重新把 frame 锚到它,底图随之归位。
// 否则删到空再加远处新数据时,新数据按旧锚点投到偏远世界坐标、底图仍贴在旧位置 → 底图"消失"。
if (dsProps_.empty()) frameAnchoredToData_ = false;
const bool wasVolume = volumes_.erase(dsId) > 0;
if (volumeOwnerDs_ == dsId) { // 移除的是"当前体" → currentImage 回退到剩余某体,无则置空
if (!volumes_.empty()) {
@ -405,39 +286,6 @@ void VtkSceneView::applyCameraView(geopro::controller::ViewDir dir) {
if (onCameraChanged) onCameraChanged(); // 相机变了 → 底图按新视锥重算覆盖
}
void VtkSceneView::focusAlongLongAxis(double t, double windowFrac) {
double b[6];
if (!computeDataBounds(b) || !scene_.renderer()) return;
const double ex = b[1] - b[0], ey = b[3] - b[2], ez = b[5] - b[4];
const int ax = (ex >= ey && ex >= ez) ? 0 : (ey >= ez ? 1 : 2); // 最长轴
const double lo = b[2 * ax], hi = b[2 * ax + 1], len = hi - lo;
if (len <= 0.0) return;
if (t < 0.0) t = 0.0;
if (t > 1.0) t = 1.0;
if (windowFrac <= 0.0) windowFrac = 0.12;
const double half = 0.5 * windowFrac * len;
const double center = lo + t * len;
double sub[6] = {b[0], b[1], b[2], b[3], b[4], b[5]}; // 短轴满幅
sub[2 * ax] = (center - half < lo) ? lo : center - half; // 长轴只取窗口段
sub[2 * ax + 1] = (center + half > hi) ? hi : center + half;
scene_.renderer()->ResetCamera(sub); // 保持朝向,仅重定位+缩放到该窗口
scene_.renderer()->ResetCameraClippingRange();
if (renderWindow_) renderWindow_->Render();
if (onCameraChanged) onCameraChanged(); // 底图随新视锥重算
}
double VtkSceneView::longAxisElongation() const {
double b[6];
if (!computeDataBounds(b)) return 0.0;
const double ex = std::abs(b[1] - b[0]), ey = std::abs(b[3] - b[2]), ez = std::abs(b[5] - b[4]);
double mx = ex, mn = ex;
if (ey > mx) mx = ey;
if (ez > mx) mx = ez;
if (ey < mn) mn = ey;
if (ez < mn) mn = ez;
return (mn > 0.0) ? mx / mn : 0.0;
}
void VtkSceneView::zoom(double factor) {
geopro::render::zoomBy(scene_.renderer(), factor);
if (renderWindow_) renderWindow_->Render();
@ -455,317 +303,6 @@ void VtkSceneView::fitView() {
if (onCameraChanged) onCameraChanged(); // 取景后 → 底图按新视锥重算覆盖(治首帧部分瓦片不出)
}
bool VtkSceneView::datasetBounds(const std::vector<std::string>& dsIds, double outB[6]) const {
// computeDataBounds 的按 dsId 版:只并集给定 dsIds 的已渲染 actor 包围盒(同样仅计可见 prop
vtkBoundingBox bb;
for (const auto& id : dsIds) {
auto it = dsProps_.find(id);
if (it == dsProps_.end()) continue;
for (const auto& p : it->second)
if (p && p->GetVisibility()) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
}
if (!bb.IsValid()) return false;
bb.GetBounds(outB);
return true;
}
void VtkSceneView::fitToBounds(const double b[6]) {
if (!scene_.renderer()) return;
scene_.renderer()->ResetCamera(b); // 保持朝向,仅重定位+缩放到该盒(区别于 fitView 的全场景)
scene_.renderer()->ResetCameraClippingRange(); // 裁剪面含底图 → 不被"蒙版"切掉
if (renderWindow_) renderWindow_->Render();
if (onCameraChanged) onCameraChanged(); // 相机变了 → 底图按新视锥重算覆盖
}
void VtkSceneView::orbitToAxis(geopro::controller::ViewDir dir, const double pivot[3]) {
auto* r = scene_.renderer();
if (!r) return;
auto* c = r->GetActiveCamera();
// 保留当前缩放:取现距离 d=|camfocal|,绕 pivot 转到 dir 轴focal=pivot、pos=pivot+off*d
const double d = c->GetDistance();
const auto pose = geopro::render::orbitPose(toRenderViewDir(dir), pivot, d);
c->SetFocalPoint(pose.focal[0], pose.focal[1], pose.focal[2]);
c->SetPosition(pose.pos[0], pose.pos[1], pose.pos[2]);
c->SetViewUp(pose.up[0], pose.up[1], pose.up[2]);
c->OrthogonalizeViewUp();
r->ResetCameraClippingRange(); // 只转向不改距离 → 不 ResetCamera仅扩裁剪面
if (renderWindow_) renderWindow_->Render();
if (onCameraChanged) onCameraChanged(); // 相机变了 → 底图按新视锥重算覆盖
}
void VtkSceneView::orbitToCurrentPivot(geopro::controller::ViewDir dir) {
// 支点 = 当前坐标轴盒中心(决策 5有选中(贴合轴)→选中子树盒 fittedBounds_否则全场景数据盒。
double b[6];
if (useFittedAxes_) {
for (int i = 0; i < 6; ++i) b[i] = fittedBounds_[i];
} else if (!computeDataBounds(b)) {
return; // 无有效数据包围盒 → 无支点可绕,静默不动
}
const double pivot[3] = {0.5 * (b[0] + b[1]), 0.5 * (b[2] + b[3]), 0.5 * (b[4] + b[5])};
orbitToAxis(dir, pivot); // 复用 T1绕 pivot 转到 dir 轴、保留当前缩放
}
void VtkSceneView::ensureGnomon() {
// 幂等装配:交互器就绪后建一次。用【专用叠加渲染器】(非 vtkOrientationMarkerWidget)图层1、固定
// 右下角视口、InteractiveOff、透明背景 → 无 widget 外框、不可拖动/缩放;相机由 syncGnomonCamera
// 镜像主相机朝向 → gizmo 随场景旋转同步转。三轴线 + 6 方向球(仅球可拾取) + 正向 XYZ 标签。
if (gnomonReady_ || !renderWindow_) return;
auto* iren = renderWindow_->GetInteractor();
if (!iren) return; // QVTK 尚未提供交互器 → 下一帧 render 再补装
// 叠加渲染器图层1 固定右下角(见下 SetViewport) —— 避开底部满宽沿线滑块条(仅雷达体时显示、
// 约占底 46px)。透明背景只显 gizmo 图元FXAA + MSAA 抗锯齿使边缘平滑;非交互不响应任何输入。
renderWindow_->SetNumberOfLayers(2);
gnomonRenderer_ = vtkSmartPointer<vtkRenderer>::New();
gnomonRenderer_->SetLayer(1);
gnomonRenderer_->InteractiveOff();
// 右下角、上抬避开底部满宽「沿线位置」滑块条(约占底 46px)y 从 0.10 抬到 0.15;靠右留 ~1% 边距。
gnomonRenderer_->SetViewport(0.855, 0.15, 0.995, 0.35);
gnomonRenderer_->SetBackgroundAlpha(0.0); // 透明合成到主场景之上,无背景块
gnomonRenderer_->SetUseFXAA(true); // FXAA + 窗口 MSAA 双重:轴线/球/字形边缘平滑
renderWindow_->AddRenderer(gnomonRenderer_);
// 抗锯齿(spec §7):整窗多重采样一次性开(仅当尚未开,不覆盖既有设置) → 平涂盘/白字边缘平滑。
if (renderWindow_->GetMultiSamples() == 0) renderWindow_->SetMultiSamples(8);
const double L = 1.0; // 球心到原点距离(= 轴线长度)
// 业界柔和轴色(非纯 RGB)X 红 / Y 绿 / Z 蓝spec §2。正向球=本色,负向球=本色×0.42(更暗)。
const std::array<double, 3> kColX = {0.90, 0.30, 0.36};
const std::array<double, 3> kColY = {0.55, 0.78, 0.33};
const std::array<double, 3> kColZ = {0.28, 0.45, 0.90};
// 三根过原点的轴线仅连到【正向】球X=红 / Y=绿 / Z=蓝平涂纯色、细、不可拾取spec §5
struct AxisLine { double to[3]; std::array<double, 3> col; };
const AxisLine lines[3] = {
{{L, 0, 0}, kColX}, // X 红
{{0, L, 0}, kColY}, // Y 绿
{{0, 0, L}, kColZ}, // Z 蓝
};
for (const auto& ln : lines) {
vtkNew<vtkLineSource> src;
src->SetPoint1(0.0, 0.0, 0.0);
src->SetPoint2(ln.to[0], ln.to[1], ln.to[2]);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(src->GetOutputPort());
vtkNew<vtkActor> a;
a->SetMapper(mapper);
a->GetProperty()->SetColor(ln.col[0], ln.col[1], ln.col[2]);
a->GetProperty()->SetLineWidth(1.8f); // 细轴线
a->GetProperty()->LightingOff(); // 平涂纯色,无高光
a->SetPickable(0); // 轴线不参与拾取(仅方向球有方向语义)
gnomonRenderer_->AddViewProp(a);
}
// 6 个方向球(平涂实心盘,非高光 3D 球):正向亮盘 + 白色 XYZ 字标、稍大;负向同色更暗、更小、无字标。
// 方向 → ViewDir 与 CameraPreset 语义一致:+Z=Top、Z=Bottom、+Y=Back、Y=Front、+X=Right、X=Left。
struct DirSpec {
geopro::controller::ViewDir dir;
double pos[3];
std::array<double, 3> base; // 该轴柔和本色
bool positive;
const char* label; // 正向字标;负向 nullptr
};
const DirSpec specs[6] = {
{geopro::controller::ViewDir::Right, {L, 0, 0}, kColX, true, "X"}, // +X
{geopro::controller::ViewDir::Left, {-L, 0, 0}, kColX, false, nullptr}, // X
{geopro::controller::ViewDir::Back, {0, L, 0}, kColY, true, "Y"}, // +Y
{geopro::controller::ViewDir::Front, {0, -L, 0}, kColY, false, nullptr}, // Y
{geopro::controller::ViewDir::Top, {0, 0, L}, kColZ, true, "Z"}, // +Z
{geopro::controller::ViewDir::Bottom, {0, 0, -L}, kColZ, false, nullptr}, // Z
};
gnomonDirs_.clear();
gnomonBaseColor_.clear();
gnomonLabels_.clear();
for (const auto& s : specs) {
// 正向盘 r=0.32(稍大);负向盘 r=0.20(更小、更暗 → 呈"淡环/凹陷"观感,仍可拾取)。
const double radius = s.positive ? 0.32 : 0.20;
const std::array<double, 3> col =
s.positive ? s.base
: std::array<double, 3>{s.base[0] * 0.42, s.base[1] * 0.42, s.base[2] * 0.42};
vtkNew<vtkSphereSource> sphere;
sphere->SetRadius(radius);
sphere->SetThetaResolution(48); // 高分辨率 → 轮廓平滑(平涂下尤重要)
sphere->SetPhiResolution(48);
sphere->SetCenter(s.pos[0], s.pos[1], s.pos[2]);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(sphere->GetOutputPort());
auto actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper);
auto* prop = actor->GetProperty();
prop->SetColor(col[0], col[1], col[2]);
prop->LightingOff(); // 关键(spec §1):平涂实心盘,无镜面/渐变 → 干净的 Blender 式 gizmo
actor->SetOrigin(s.pos[0], s.pos[1], s.pos[2]); // 缩放原点=球心 → hover 放大就地不位移
actor->SetPickable(1); // 6 球均可拾取(负向点击仍 orbit 到对侧)
gnomonRenderer_->AddViewProp(actor);
gnomonDirs_[actor.Get()] = s.dir; // 渲染器持 actor 保活;此处仅记裸指针→方向
gnomonBaseColor_[actor.Get()] = col; // 记本色 → hover 提亮后可复原
if (s.positive && s.label) { // 正向轴字标:始终朝相机的公告板文字,白色粗体、居中于球心
auto lbl = vtkSmartPointer<vtkBillboardTextActor3D>::New();
lbl->SetInput(s.label);
lbl->SetPosition(s.pos[0], s.pos[1], s.pos[2]);
auto* tp = lbl->GetTextProperty();
tp->SetFontSize(20);
tp->SetBold(true);
tp->SetColor(1.0, 1.0, 1.0); // 白字
tp->SetJustificationToCentered();
tp->SetVerticalJustificationToCentered();
lbl->SetPickable(0);
gnomonRenderer_->AddViewProp(lbl);
gnomonLabels_.push_back({lbl.Get(),
{s.pos[0], s.pos[1], s.pos[2]}}); // syncGnomonCamera 推到球前避遮挡
}
}
gnomonPicker_ = vtkSmartPointer<vtkPropPicker>::New();
// 左键高优先级(1.0)观察者:先于交互样式(0.0),命中方向球 → orbit + abort 消费(阻止相机旋转/拾取)。
gnomonClickCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
gnomonClickCmd_->SetClientData(this);
gnomonClickCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
static_cast<VtkSceneView*>(client)->handleGnomonClick();
});
gnomonClickTag_ = iren->AddObserver(vtkCommand::LeftButtonPressEvent, gnomonClickCmd_, 1.0);
// 鼠标移动高优先级观察者:仅角落内拾取做 hover 高亮,永不 abort → 不阻塞场景旋转/平移/切片交互。
gnomonHoverCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
gnomonHoverCmd_->SetClientData(this);
gnomonHoverCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
static_cast<VtkSceneView*>(client)->handleGnomonHover();
});
gnomonHoverTag_ = iren->AddObserver(vtkCommand::MouseMoveEvent, gnomonHoverCmd_, 1.0);
// 相机同步:观察主相机 ModifiedEvent每次朝向变化把 gizmo 相机镜像到同朝向 → gizmo 随场景转。
gnomonCamCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
gnomonCamCmd_->SetClientData(this);
gnomonCamCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
static_cast<VtkSceneView*>(client)->syncGnomonCamera();
});
if (auto* mainCam = scene_.renderer() ? scene_.renderer()->GetActiveCamera() : nullptr) {
gnomonObservedCam_ = mainCam;
gnomonCamTag_ = mainCam->AddObserver(vtkCommand::ModifiedEvent, gnomonCamCmd_);
}
syncGnomonCamera(); // 初始一次:装配即与当前朝向对齐
gnomonReady_ = true;
}
void VtkSceneView::syncGnomonCamera() {
if (!gnomonRenderer_ || !scene_.renderer()) return;
auto* mainCam = scene_.renderer()->GetActiveCamera();
auto* gcam = gnomonRenderer_->GetActiveCamera();
if (!mainCam || !gcam) return;
// 复制主相机投影方向 + view-upgizmo 相机置于 -dir*dist、焦点在原点 → 与主相机同朝向看向 gizmo。
double dir[3];
mainCam->GetDirectionOfProjection(dir); // 已归一化(FP),指向场景内(背离相机)
double up[3];
mainCam->GetViewUp(up);
const double dist = 10.0;
gcam->SetParallelProjection(1); // 正交投影gizmo 无透视畸变(业界标准)
// 按视口像素长宽比自适应取景半高balls 到 ±(L+r)≈1.32 + 白字留边 → halfExtent=1.5。
// parallelScale = 视口世界半高;水平可见半宽 = scale×aspect。取 scale=halfExtent/min(1,aspect)
// 保证长/宽两向都容得下所有球 → 任意窗口长宽比不裁切(视口归一化随窗拉伸也不失效)。
const double halfExtent = 1.5;
double aspect = 1.0;
const int* wsz = renderWindow_->GetSize();
const double* vp = gnomonRenderer_->GetViewport();
if (wsz && wsz[0] > 0 && wsz[1] > 0) {
const double vpH = (vp[3] - vp[1]) * wsz[1];
if (vpH > 0.0) aspect = (vp[2] - vp[0]) * wsz[0] / vpH;
}
gcam->SetParallelScale(halfExtent / std::min(1.0, aspect));
gcam->SetFocalPoint(0.0, 0.0, 0.0);
gcam->SetPosition(-dir[0] * dist, -dir[1] * dist, -dir[2] * dist);
gcam->SetViewUp(up[0], up[1], up[2]);
gnomonRenderer_->ResetCameraClippingRange();
// 正向白字标签推到球【前】(朝相机 = dir 方向、偏移略大于球半径)billboard 恒面相机,位于球前
// → 不被球面前半遮挡,读数清晰。相机每次转动都随之更新前向偏移。
const double front = 0.40; // > 正向球半径 0.32 → 字浮于球前表面之外
for (const auto& lp : gnomonLabels_) {
if (!lp.first) continue;
lp.first->SetPosition(lp.second[0] - dir[0] * front, lp.second[1] - dir[1] * front,
lp.second[2] - dir[2] * front);
}
}
void VtkSceneView::handleGnomonClick() {
if (!gnomonRenderer_ || !gnomonPicker_ || !renderWindow_) return;
auto* iren = renderWindow_->GetInteractor();
if (!iren) return;
const int ex = iren->GetEventPosition()[0];
const int ey = iren->GetEventPosition()[1];
// 仅当点击落在 gnomon 角落视口矩形内才拾取(否则放行正常场景交互,且省去全场景每次左键的硬件拾取)。
const double* vp = gnomonRenderer_->GetViewport(); // 归一化 [xmin,ymin,xmax,ymax]
const int* sz = renderWindow_->GetSize();
if (sz[0] <= 0 || sz[1] <= 0) return;
const double fx = static_cast<double>(ex) / sz[0];
const double fy = static_cast<double>(ey) / sz[1];
if (fx < vp[0] || fx > vp[2] || fy < vp[1] || fy > vp[3]) return; // 不在角落 → 不 abort放行
// 角落内硬件拾取(仅方向球可拾):命中某方向球 → 取其 ViewDir → 绕当前轴盒中心转到该轴(保留缩放)。
if (gnomonPicker_->PickProp(ex, ey, gnomonRenderer_)) {
vtkProp* leaf = gnomonPicker_->GetViewProp(); // 叠加渲染器内为裸 actor直取即方向球
auto it = gnomonDirs_.find(leaf);
if (it != gnomonDirs_.end()) {
orbitToCurrentPivot(it->second);
if (gnomonClickCmd_) gnomonClickCmd_->SetAbortFlag(1); // 命中才消费:不触发相机旋转/场景拾取
}
}
// 未命中球 → 不 abort左键继续走正常交互旋转/平移/缩放/切片/拾取),保证非干扰。
}
void VtkSceneView::handleGnomonHover() {
// 非阻塞 hover 高亮:绝不 SetAbortFlag → 鼠标移动照常传给交互样式(旋转/平移/切片)。
if (!gnomonRenderer_ || !gnomonPicker_ || !renderWindow_) return;
auto* iren = renderWindow_->GetInteractor();
if (!iren) return;
// 提亮本色(向白混 0.42);复原用记录的本色。二者共用,避免重复。
auto applyColor = [](vtkProp* p, const std::array<double, 3>& c, bool highlight, double scale) {
auto* a = vtkActor::SafeDownCast(p);
if (!a) return;
if (highlight) {
a->GetProperty()->SetColor(c[0] * 0.58 + 0.42, c[1] * 0.58 + 0.42, c[2] * 0.58 + 0.42);
a->SetScale(scale); // 就地放大(origin=球心)
} else {
a->GetProperty()->SetColor(c[0], c[1], c[2]);
a->SetScale(1.0);
}
};
auto restore = [&]() {
if (!gnomonHovered_) return;
auto it = gnomonBaseColor_.find(gnomonHovered_);
if (it != gnomonBaseColor_.end()) applyColor(gnomonHovered_, it->second, false, 1.0);
gnomonHovered_ = nullptr;
};
const int ex = iren->GetEventPosition()[0];
const int ey = iren->GetEventPosition()[1];
const double* vp = gnomonRenderer_->GetViewport(); // 归一化 [xmin,ymin,xmax,ymax]
const int* sz = renderWindow_->GetSize();
if (sz[0] <= 0 || sz[1] <= 0) { restore(); return; }
const double fx = static_cast<double>(ex) / sz[0];
const double fy = static_cast<double>(ey) / sz[1];
if (fx < vp[0] || fx > vp[2] || fy < vp[1] || fy > vp[3]) { // 光标离开角落 → 复原并跳过拾取(廉价)
if (gnomonHovered_) { restore(); renderWindow_->Render(); }
return;
}
// 角落内才做硬件拾取:命中方向球 → 高亮该球、复原旧的;未命中 → 复原。
vtkProp* hit = nullptr;
if (gnomonPicker_->PickProp(ex, ey, gnomonRenderer_)) {
vtkProp* leaf = gnomonPicker_->GetViewProp();
if (gnomonBaseColor_.find(leaf) != gnomonBaseColor_.end()) hit = leaf;
}
if (hit == gnomonHovered_) return; // 无变化 → 不重绘
restore();
if (hit) {
auto it = gnomonBaseColor_.find(hit);
if (it != gnomonBaseColor_.end()) applyColor(hit, it->second, true, 1.18);
gnomonHovered_ = hit;
}
renderWindow_->Render(); // 高亮/复原变更 → 立即刷新
}
void VtkSceneView::rebuildAxes() {
// 先移除上一次的坐标轴 proprender 可能在一次 rebuild 内多次调用(末尾统一 render +
// 异步回灌 render不先移除会叠加坐标轴评审 HIGH。移除后再算 bounds仅数据图元
@ -775,13 +312,8 @@ void VtkSceneView::rebuildAxes() {
}
// 坐标轴随数据包围盒重建:仅按数据图元算 bounds(不含底图,否则被~公里级底图撑大)
// 再造 vtkCubeAxesActor 入场。None 模式或无数据 → buildAxes 返回 nullptr场景无坐标轴。
// 贴合态(useFittedAxes_):改用选中子树盒 fittedBounds_只框该子树而非全场景spec §3.2)。
double bounds[6];
if (useFittedAxes_) {
for (int i = 0; i < 6; ++i) bounds[i] = fittedBounds_[i];
} else if (!computeDataBounds(bounds)) {
return; // 无数据 → 不建坐标轴
}
if (!computeDataBounds(bounds)) return; // 无数据 → 不建坐标轴
geopro::render::AxesOptions opts;
opts.mode = toRenderMode(axesMode_);
opts.unit = toRenderUnit(axesUnit_);
@ -800,23 +332,7 @@ void VtkSceneView::rebuildAxes() {
}
}
void VtkSceneView::showFittedAxes(const double b[6]) {
// 选中子树盒 → 冻结为贴合轴 bounds隐去全场景轴rebuildAxes 会先移除旧轴再按 fittedBounds_ 重建)。
useFittedAxes_ = true;
for (int i = 0; i < 6; ++i) fittedBounds_[i] = b[i];
rebuildAxes();
if (renderWindow_) renderWindow_->Render();
}
void VtkSceneView::showSceneAxes() {
// 取消选中 → 复位为全场景总览轴(现状默认)。清掉贴合态后 rebuildAxes 走 computeDataBounds。
useFittedAxes_ = false;
rebuildAxes();
if (renderWindow_) renderWindow_->Render();
}
void VtkSceneView::render(bool is2D, bool resetCamera) {
ensureGnomon(); // 构造时交互器未就绪则于此补装(幂等)
// 视图区背景永远深色(规范 §0.5:不随明暗切换),让色阶数据更突出。
double bgR, bgG, bgB;
geopro::app::vtkBackground(bgR, bgG, bgB);
@ -824,7 +340,6 @@ void VtkSceneView::render(bool is2D, bool resetCamera) {
// 坐标轴仅三维视图显示2D 俯视测线不需要立体坐标轴)。
if (!is2D) rebuildAxes();
// 相机预设(朝向)只在取景时应用——保留相机的重建(改放大系数)不重设朝向,否则也会跳视角。
// 朝向按 is2D俯视(Map2D)/三维自由透视。
if (resetCamera) {
if (is2D)
geopro::render::applyTop2D(scene_.renderer());
@ -832,7 +347,7 @@ void VtkSceneView::render(bool is2D, bool resetCamera) {
geopro::render::applyFree3D(scene_.renderer());
double bounds[6];
if (computeDataBounds(bounds))
scene_.renderer()->ResetCamera(bounds); // 取景到"可见"数据(不含底图,否则数据缩成小点)
scene_.renderer()->ResetCamera(bounds); // 取景到数据(不含底图,否则数据缩成小点)
else
scene_.renderer()->ResetCamera();
}
@ -842,7 +357,6 @@ void VtkSceneView::render(bool is2D, bool resetCamera) {
}
void VtkSceneView::renderIncremental() {
ensureGnomon(); // 幂等:交互器就绪后补装角落方向标
// 增量渲染:仅按新包围盒重建坐标轴并提交,不动相机(勾选/取消时视角不跳)。
rebuildAxes();
scene_.renderer()->ResetCameraClippingRange(); // 数据/底图变化后扩裁剪面,防被切

View File

@ -1,11 +1,8 @@
#pragma once
#include <array>
#include <functional>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include <vtkCubeAxesActor.h>
@ -21,11 +18,6 @@ class vtkRenderer;
class vtkRenderWindow;
class vtkProp;
class vtkActor;
class vtkVolume;
class vtkPropPicker;
class vtkCallbackCommand;
class vtkCamera;
class vtkBillboardTextActor3D;
namespace geopro::app {
@ -38,22 +30,17 @@ public:
// 入参生命周期须覆盖本对象由调用方保证。zRefElev地形 z 基准(测线地表高程)。
VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* renderWindow,
std::shared_ptr<geopro::core::GeoLocalFrame> frame, double zRefElev);
~VtkSceneView() override; // 摘除 gnomon 左键/相机观察者(clientData=this),移除叠加渲染器
void clear() override;
void setVerticalExaggeration(double ve) override;
void setVolumeOpacity(double maxOpacity) override; // 运行时调已渲染体 + 后续新体的最大不透明度
double zRefElev() const override { return zRefElev_; }
void addSurveyLine(const geopro::core::Grid& grid) override;
void addCurtain(const std::string& dsId, const geopro::core::Grid& grid,
const geopro::core::ColorScale& cs) override;
void addVolume(const std::string& dsId, const geopro::data::VolumeGrid& vol,
const geopro::core::ColorScale& cs) override;
bool updateVolumeColorInPlace(const std::string& dsId,
const geopro::core::ColorScale& cs) override;
void addMapLine(const std::string& dsId, const geopro::data::MapLine& line,
double worldZ) override;
void setMapLinesZ(const std::vector<std::string>& dsIds, double z) override;
void addTerrain(const geopro::data::TerrainPaths& paths) override;
void removeDataset(const std::string& dsId) override;
void addAnomaly(const geopro::core::Anomaly& a) override;
@ -69,24 +56,6 @@ public:
void applyCameraView(geopro::controller::ViewDir dir) override;
void zoom(double factor) override;
void fitView() override;
// ── 视图导航基元spec §3.1T1──────────────────────────────────────────────
// 给定 dsIds 的已渲染 actor 世界包围盒并集;无有效返回 false否则填 out=[xmin,xmax,…,zmax]。
bool datasetBounds(const std::vector<std::string>& dsIds, double outB[6]) const;
// 相机适配到指定包围盒保持当前朝向ResetCamera(b)),用于双击适配/贴合。
void fitToBounds(const double b[6]);
// 绕 pivot 转到沿 dir 轴看向 pivot保留当前 focal-to-camera 距离(缩放不变)。
void orbitToAxis(geopro::controller::ViewDir dir, const double pivot[3]);
// ── 贴合轴 / 全景轴spec §3.2T2───────────────────────────────────────────
// 选中某 ds → 用其子树盒 b 显示贴合 cube axes、隐去全场景总览轴立即提交渲染
void showFittedAxes(const double b[6]);
// 取消选中 → 恢复全场景总览轴(现状默认行为,立即提交渲染)。
void showSceneAxes();
// ── 可点击方向标 gnomonspec §3.3T3─────────────────────────────────────────
// 绕【当前坐标轴盒中心】转到 dir 轴、保留当前缩放:支点 = 有选中(useFittedAxes_)→选中子树盒
// fittedBounds_ 中心,否则全场景数据盒 computeDataBounds 中心。无有效数据 → no-op。
// 封装决策 5 的支点规则,调用方只需给方向(角落 gnomon 点击即调此)。
void orbitToCurrentPivot(geopro::controller::ViewDir dir);
void render(bool is2D, bool resetCamera = true) override;
void renderIncremental() override;
@ -113,13 +82,6 @@ public:
// 相机程序化变化(取景/预设/缩放)后回调,供底图按新视锥重算覆盖(否则首帧部分瓦片要手动微动才出)。
std::function<void()> onCameraChanged;
// ── B 方案#2沿线位置巡航雷达超长测线──────────────────────────────────────
// t∈[0,1] 沿数据【最长轴】定位;取景到该位置一段【窗口】(windowFrac=窗口占长轴比例)
// 保持当前朝向(ResetCamera 只重定位+缩放、不转向)→ 像滚动读长 radargram。短轴满幅、长轴只取一段。
void focusAlongLongAxis(double t, double windowFrac);
// 数据包围盒长短轴比(max/min 跨度)。用于判是否细长(雷达)→ 决定沿线滑块显隐。无数据返回 0。
double longAxisElongation() const;
private:
// 首个带经纬数据(剖面/足迹)到达时把共享 frame 重锚到其 lat/lon 包围盒中心:使数据落在世界原点近旁
// (否则样本默认原点可能离真实数据数百公里→图元在视锥外、移动视角也找不到)。已锚或无经纬则跳过。
@ -129,20 +91,6 @@ private:
void removeProps(std::vector<vtkSmartPointer<vtkProp>>& props); // 从 renderer 移除并清空
// 仅数据图元(剖面/体素/地形/测线)的包围盒,不含底图 → 坐标轴/取景不被~公里级底图撑大。
bool computeDataBounds(double out[6]) const;
// 角落可点击方向标 gnomonT3首次(交互器就绪)时装配【专用叠加渲染器】(图层1、固定右下角、
// 非交互无边框) + 三轴线 + 6 向可拾取球 + 正向标签 + 左键/相机观察者。
// 幂等:装配后置 gnomonReady_重复调直接返回。render/renderIncremental/构造均可安全调用。
void ensureGnomon();
// 左键按下高优先级(先于交互样式)回调:点在 gnomon 角落视口且命中方向球 → orbitToCurrentPivot + abort
// (消费事件,阻止相机旋转/场景拾取);否则不 abort放行正常交互(旋转/平移/缩放/切片/拾取)。
void handleGnomonClick();
// 鼠标移动(非 abort、不阻塞场景交互)回调:仅当光标落在 gnomon 角落视口内才拾取,命中方向球 →
// 高亮(提亮本色 + 放大 ~1.18×),复原其余;离开角落或未命中 → 复原全部。picking 只在角落内进行(廉价)。
void handleGnomonHover();
// 把主相机朝向(投影方向 + view-up)镜像到 gnomon 叠加渲染器相机(定距、焦点在 gizmo 原点)
// 使 gizmo 随场景旋转同步转。主相机 ModifiedEvent 观察者与初始装配各调一次。
// 同时按视口像素长宽比自适应取景半高(球始终不裁切) + 把正向标签推到球前(朝相机)避免被球面遮挡。
void syncGnomonCamera();
public:
// 当前所有数据图元(剖面等)合并范围的水平半径(米);无数据返回 0。供底图动态定最大范围。
@ -167,10 +115,6 @@ private:
// 当前坐标轴 proprender 可能多次调用 rebuildAxesrebuild 末尾 + 异步回灌),
// 持引用以便重建前移除旧 prop避免叠加评审 HIGH
vtkSmartPointer<vtkCubeAxesActor> currentAxes_;
// 贴合轴态T2true=坐标轴按 fittedBounds_选中子树盒非全场景数据包围盒选中时冻结该盒
// 取消/清场复位为 false走全场景 computeDataBounds
bool useFittedAxes_ = false;
double fittedBounds_[6] = {0, 0, 0, 0, 0, 0};
// 当前体素 image + 色阶P3 切片附着源);无体素时为空。「当前」=最后添加/活动的体(保存切片/
// 创建异常的默认归属)。多体并发下各体 image 另存 volumes_。
@ -183,7 +127,6 @@ private:
vtkSmartPointer<vtkImageData> image;
geopro::core::ColorScale cs;
double vmin = 0.0, vmax = 0.0;
vtkSmartPointer<vtkVolume> volume; // 体 actor运行时调不透明度改其 property 的不透明度传递函数)
};
std::map<std::string, VolumeRec> volumes_;
@ -203,33 +146,6 @@ private:
std::vector<vtkSmartPointer<vtkProp>> miscProps_;
std::string volumeOwnerDs_; // 当前 currentVolumeImage_ 归属的 ds其被移除时置空切片源
std::map<std::string, vtkSmartPointer<vtkActor>> anomalyProps_; // 异常 id → 3D actor
// 哪些 dsProps_ 条目是 2D 足迹(addMapLine):供足迹 actor 归属识别(Task E2/F2 用)。
std::set<std::string> mapLineDs_;
// ── 可点击方向标 gnomonT3──────────────────────────────────────────────────
// 专用叠加渲染器图层1、固定右下角视口、InteractiveOff、透明背景、无边框 —— 不是 widget
// 故无外框、不可拖动/缩放;相机由 syncGnomonCamera 镜像主相机朝向 → gizmo 随场景转。
// gnomonPicker_ 在此渲染器上做硬件拾取(仅方向球可拾取)。
vtkSmartPointer<vtkRenderer> gnomonRenderer_;
vtkSmartPointer<vtkPropPicker> gnomonPicker_;
vtkSmartPointer<vtkCallbackCommand> gnomonClickCmd_; // 左键观察者命令(可条件 SetAbortFlag 消费)
unsigned long gnomonClickTag_ = 0; // 左键观察者句柄(析构时摘除)
vtkSmartPointer<vtkCallbackCommand> gnomonCamCmd_; // 主相机 ModifiedEvent 观察者命令
unsigned long gnomonCamTag_ = 0; // 相机观察者句柄(析构时摘除)
vtkCamera* gnomonObservedCam_ = nullptr; // 被观察的主相机(非拥有;析构摘观察者用)
bool gnomonReady_ = false; // 已装配(幂等 ensureGnomon
// 6 个方向球 actor → ViewDir 映射(拾取命中球 → 该方向。actor 由叠加渲染器持有保活。
std::map<vtkProp*, geopro::controller::ViewDir> gnomonDirs_;
// ── hover 高亮spec §6─────────────────────────────────────────────────────
vtkSmartPointer<vtkCallbackCommand> gnomonHoverCmd_; // 鼠标移动观察者命令(不 abort非阻塞
unsigned long gnomonHoverTag_ = 0; // 移动观察者句柄(析构摘除)
vtkProp* gnomonHovered_ = nullptr; // 当前高亮的方向球裸指针renderer 保活)
std::map<vtkProp*, std::array<double, 3>> gnomonBaseColor_; // 各球本色hover 复原用)
// 正向标签(白字) + 其球心:每次 syncGnomonCamera 把标签推到球前(朝相机)→ 不被球面遮挡。
// raw ptr 非拥有,由叠加渲染器持有保活(与 gnomonDirs_ 同生命周期约定)。
std::vector<std::pair<vtkBillboardTextActor3D*, std::array<double, 3>>> gnomonLabels_;
};
} // namespace geopro::app

View File

@ -1,15 +1,9 @@
#include "VtkViewToolbar.hpp"
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QMenu>
#include <QPoint>
#include <QSize>
#include <QSlider>
#include <QToolButton>
#include <QVBoxLayout>
#include <QWidgetAction>
#include "Glyphs.hpp"
#include "Theme.hpp"
@ -54,28 +48,9 @@ VtkViewToolbar::VtkViewToolbar(QWidget* parent) : QWidget(parent) {
col->addWidget(line);
};
// ── 段1设置坐标轴/ 底图 ──
// ── 段1设置坐标轴──
connect(iconBtn(Glyph::Gear, QStringLiteral("坐标轴设置")), &QToolButton::clicked, this,
&VtkViewToolbar::axesSettingsRequested);
// 共享 3D 底图控件:天地图/无 + 透明度滑块spec §7.5/§9.1,从数据集栏移至渲染区工具条,全局唯一)。
{
auto* mapBtn = iconBtn(Glyph::Map, QStringLiteral("底图"));
mapBtn->setPopupMode(QToolButton::InstantPopup);
auto* menu = new QMenu(mapBtn);
menu->addAction(QStringLiteral("天地图"), this, [this] { emit basemapKindChanged(0); });
menu->addAction(QStringLiteral(""), this, [this] { emit basemapKindChanged(1); });
menu->addSeparator();
auto* wa = new QWidgetAction(menu);
auto* sld = new QSlider(Qt::Horizontal, menu);
sld->setRange(0, 100);
sld->setValue(50);
sld->setToolTip(QStringLiteral("底图透明度"));
connect(sld, &QSlider::valueChanged, this,
[this](int v) { emit basemapOpacityChanged(v / 100.0); });
wa->setDefaultWidget(sld);
menu->addAction(wa);
mapBtn->setMenu(menu);
}
sep();
// ── 段2快捷视图前/后/上/下/左/右)──
struct V {
@ -86,11 +61,11 @@ VtkViewToolbar::VtkViewToolbar(QWidget* parent) : QWidget(parent) {
{"", ViewDir::Bottom}, {"", ViewDir::Left}, {"", ViewDir::Right}};
for (const V& v : views) {
const ViewDir d = v.d;
auto* b = textBtn(QString::fromUtf8(v.t));
connect(b, &QToolButton::clicked, this, [this, d] { emit viewRequested(d); });
connect(textBtn(QString::fromUtf8(v.t)), &QToolButton::clicked, this,
[this, d] { emit viewRequested(d); });
}
sep();
// ── 段3缩放 / 复位 ──(三维体不透明度已移交色阶「不透明度」,旧「透」滑块退役移除)
// ── 段3缩放 / 复位 ──
connect(iconBtn(Glyph::Plus, QStringLiteral("放大")), &QToolButton::clicked, this,
&VtkViewToolbar::zoomInRequested);
connect(iconBtn(Glyph::Minus, QStringLiteral("缩小")), &QToolButton::clicked, this,

View File

@ -2,14 +2,10 @@
#include <QWidget>
#include "I3dSceneView.hpp" // geopro::controller::ViewDir
class QToolButton;
class QSlider;
class QLabel;
namespace geopro::app {
// VTK 画布竖排工具条spec §9全局视图控制——设置(坐标轴)/底图/前后上下左右/放大缩小复位。
// 仅发信号,不认 VTK由 main 接到场景控制器与共享 3D 底图
// VTK 画布竖排工具条spec §9全局视图控制——设置(坐标轴)/前后上下左右/放大缩小复位。
// 仅发信号,不认 VTK由 main 接到场景控制器。
class VtkViewToolbar : public QWidget {
Q_OBJECT
public:
@ -17,8 +13,6 @@ public:
signals:
void axesSettingsRequested(); // 设置 → 弹 AxesSettingsDialog
void basemapKindChanged(int kind); // 底图类型0 天地图 / 1 无
void basemapOpacityChanged(double o); // 底图透明度0..1
void viewRequested(geopro::controller::ViewDir dir); // 前/后/上/下/左/右
void zoomInRequested();
void zoomOutRequested();

View File

@ -100,8 +100,7 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
"#brandSubtitle { color: rgba(255,255,255,0.82); font-size: %3px; }"
"#fieldLabel { color: {{text/secondary}}; font-size: %4px; font-weight: %5; }"
// 输入框已 Ela 化(ElaLineEdit 自绘 Fluent + 自动明暗),不再写 QLineEdit QSS。
// 验证码容器固定白底:后端验证码图是浅底,白底贴合图边(两种主题皆然,故用白字面值)。
"#captchaImg { border: 1px solid {{border/strong}}; border-radius: 8px; background: #FFFFFF; }")
"#captchaImg { border: 1px solid {{border/strong}}; border-radius: 8px; background: {{bg/hover}}; }")
.arg(scaledPx(type::kDisplay))
.arg(type::kWeightBold)
.arg(scaledPx(type::kCaption))

File diff suppressed because it is too large Load Diff

View File

@ -51,8 +51,7 @@ void DatasetDetailPage::build(const QString& dsId, const QString& ddCode, const
// dsIdGetter 用本页 dsId_此处已赋值随项目/数据集稳定。
auto view = makeDetailView(spec.kind, this, colorTplRepo_, projectIdGetter_, cmdRepo_,
[this] { return dsId_; },
[this] { return tmObjectId_; },
viewState_); // 抛出由调用栈兜底GuardedApplication
[this] { return tmObjectId_; }); // 抛出由调用栈兜底GuardedApplication
IDetailView* raw = view.release(); // QWidget 由 this/QwtPlot 父子树接管生命周期
views_[i] = raw;
// lazy 页签:建覆盖该视图的加载遮罩(父为视图 widget随其尺寸覆盖图区

View File

@ -12,10 +12,6 @@ class IColorTemplateRepository;
class IDatasetCommandRepository;
}
namespace geopro::controller {
class DatasetViewState; // 跨视图色阶真源(统一同步)
}
namespace geopro::app {
class IDetailView;
@ -39,9 +35,6 @@ public:
// 所属 TM 对象 id=白化 structParentId注入须在 build 前设置 → tmObjectIdGetter 透传给视图)。
void setTmObjectId(const QString& tmObjectId) { tmObjectId_ = tmObjectId; }
// 跨视图色阶真源注入(须在 build 前设置 → 透传给网格视图,实现 2D↔3D 色阶同步)。
void setViewState(geopro::controller::DatasetViewState* state) { viewState_ = state; }
// 按页签集构建页签首次打开调一次。dsId/ddCode/dsName 用于 tabNeeded。
void build(const QString& dsId, const QString& ddCode, const QString& dsName,
const std::vector<geopro::controller::TabSpec>& tabs);
@ -82,8 +75,6 @@ private:
// 反演命令仓储注入(透传给 makeDetailView → measurement 散点视图反演按钮)。
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
geopro::controller::DatasetViewState* viewState_ = nullptr; // 跨视图色阶真源(透传给网格视图)
};
} // namespace geopro::app

View File

@ -15,10 +15,6 @@ void DatasetDetailPanel::setCommandRepo(geopro::data::IDatasetCommandRepository*
cmdRepo_ = repo;
}
void DatasetDetailPanel::setViewState(geopro::controller::DatasetViewState* state) {
viewState_ = state;
}
DatasetDetailPanel::DatasetDetailPanel(QWidget* parent) : QTabWidget(parent) {
setTabsClosable(true);
connect(this, &QTabWidget::tabCloseRequested, this, [this](int i) { widget(i)->deleteLater(); });
@ -44,7 +40,6 @@ void DatasetDetailPanel::onDatasetOpened(const QString& dsId, const QString& ddC
// 注入须在 build 前build 内造视图时即透传给工厂)。
p->setColorTemplateRepo(colorTplRepo_, projectIdGetter_);
p->setCommandRepo(cmdRepo_);
p->setViewState(viewState_); // 跨视图色阶真源build 前设置 → 透传给网格视图)
p->setTmObjectId(tmObjectId); // 白化 structParentIdbuild 前设置 → 透传给视图)
p->build(dsId, ddCode, dsName, tabs); // ddCode 透传 → 页内 tabNeeded 携带
const QString title = dsName.isEmpty() ? dsId : dsName; // 页签标题用数据名(空则回退 id

View File

@ -9,9 +9,6 @@ namespace geopro::data {
class IColorTemplateRepository;
class IDatasetCommandRepository;
}
namespace geopro::controller {
class DatasetViewState; // 跨视图色阶真源(统一同步)
}
namespace geopro::app {
class DatasetDetailPage;
@ -28,9 +25,6 @@ public:
// 反演命令仓储透传给每个新建的详情页measurement 反演运算/生成视电阻率用)。
void setCommandRepo(geopro::data::IDatasetCommandRepository* repo);
// 跨视图色阶真源:透传给每个新建的详情页 → 网格视图2D↔3D 色阶同步)。
void setViewState(geopro::controller::DatasetViewState* state);
// 数据集打开find-or-create 页 → build(tabs) → 加/抬该面板页签。
// tmObjectId所属 TM 对象 id白化 structParentIdbuild 前交给页 → 视图。
void onDatasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName,
@ -57,7 +51,5 @@ private:
// 反演命令仓储注入(新页 build 前 setCommandRepo 透传)。
geopro::data::IDatasetCommandRepository* cmdRepo_ = nullptr;
geopro::controller::DatasetViewState* viewState_ = nullptr; // 跨视图色阶真源(透传给详情页)
};
} // namespace geopro::app

View File

@ -90,26 +90,14 @@ public:
const bool isContainer = idx.data(kDsDdCodeRole).toString() == QStringLiteral("container");
if (checkable) {
QRect checkRect(r.left() + 12, r.top() + (r.height() - box) / 2, box, box);
if (idx.data(kDsBusyRole).toBool()) {
// 渲染中:复选框位置画旋转 spinner等待动画角度由 kDsSpinAngleRole 驱动)。
const int angle = idx.data(kDsSpinAngleRole).toInt();
p->save();
p->setRenderHint(QPainter::Antialiasing, true);
QPen pen(geopro::app::tokenColor("accent/primary"), 2.0);
pen.setCapStyle(Qt::RoundCap);
p->setPen(pen);
p->drawArc(QRectF(checkRect).adjusted(2, 2, -2, -2), -angle * 16, 270 * 16);
p->restore();
} else {
const auto cs = static_cast<Qt::CheckState>(idx.data(Qt::CheckStateRole).toInt());
QStyleOptionViewItem o(opt);
o.rect = checkRect;
o.state &= ~QStyle::State_HasFocus;
o.state |= (cs == Qt::Checked ? QStyle::State_On : QStyle::State_Off);
const QWidget* w = opt.widget;
QStyle* st = w ? w->style() : QApplication::style();
st->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &o, p, w);
}
const auto cs = static_cast<Qt::CheckState>(idx.data(Qt::CheckStateRole).toInt());
QStyleOptionViewItem o(opt);
o.rect = checkRect;
o.state &= ~QStyle::State_HasFocus;
o.state |= (cs == Qt::Checked ? QStyle::State_On : QStyle::State_Off);
const QWidget* w = opt.widget;
QStyle* st = w ? w->style() : QApplication::style();
st->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &o, p, w);
textLeftPad = 12 + box + 8; // 复选框右侧留白后再放文本
} else if (isContainer) {
// 容器文本左缘对齐子级复选框的左缘(r.left()+12)——使「容器→带框子级」的视觉缩进 = 一个树级
@ -155,7 +143,6 @@ public:
const QModelIndex& idx) override {
if (!(idx.flags() & Qt::ItemIsUserCheckable))
return QStyledItemDelegate::editorEvent(ev, model, opt, idx);
const bool busy = idx.data(kDsBusyRole).toBool(); // 渲染中 → 吞掉勾选交互
const QRect r = opt.rect.adjusted(4, 2, -4, -2);
const int box = 16;
// 命中区放宽到复选框左侧整段(含一点文本起始),便于点击。
@ -167,14 +154,12 @@ public:
if (ev->type() == QEvent::MouseButtonRelease) {
auto* me = static_cast<QMouseEvent*>(ev);
if (me->button() == Qt::LeftButton && hit.contains(me->pos())) {
if (busy) return true; // 渲染中:不切换,仅消费
toggle();
return true;
}
} else if (ev->type() == QEvent::KeyPress) {
auto* ke = static_cast<QKeyEvent*>(ev);
if (ke->key() == Qt::Key_Space || ke->key() == Qt::Key_Select) {
if (busy) return true;
toggle();
return true;
}

View File

@ -23,8 +23,6 @@ constexpr int kDsNameRole = 0x0105; // Qt::UserRole + 5dsName详情页
constexpr int kDsTypeNameRole = 0x0106; // Qt::UserRole + 6类型名快速筛选用
constexpr int kDsCreateTimeRole = 0x0107; // Qt::UserRole + 7创建时间按日期筛选用
constexpr int kDsTmObjectIdRole = 0x0108; // Qt::UserRole + 8所属 TM 对象 id=白化 structParentId
constexpr int kDsBusyRole = 0x0109; // Qt::UserRole + 9true=该行渲染中,复选框位置画等待 spinner
constexpr int kDsSpinAngleRole = 0x010A; // Qt::UserRole + 10spinner 角度定时器驱动delegate 据此旋转)
// 数据页签:树形(按 DsRow.parentId 嵌套,源数据为根、派生数据挂其下,对齐原版 el-table 树)。
// 每项列0文本 = dsName +「创建时间 · 类型名」data(0,角色) 存 dsId/ddCode/dsName。

View File

@ -30,10 +30,7 @@ LoadingOverlay::LoadingOverlay(QWidget* parent) : QWidget(parent), label_(new QL
hide();
}
void LoadingOverlay::showOver() { showOver(QStringLiteral("加载中…")); }
void LoadingOverlay::showOver(const QString& message) {
label_->setText(message);
void LoadingOverlay::showOver() {
if (parentWidget()) setGeometry(parentWidget()->rect());
raise();
show();

View File

@ -3,14 +3,12 @@
class QLabel;
namespace geopro::app {
// 半透明「等待」遮罩(公共组件)。贴在任意目标视图上层(含 VTK QVTKOpenGLStereoWidget
// showOver()/hide() 切换,几何随父 resize 跟随。可传自定义文案在不同场景复用。
// 半透明「加载中…」遮罩。贴在目标视图上层showOver()/hide() 切换,几何随父 resize 跟随。
class LoadingOverlay : public QWidget {
Q_OBJECT
public:
explicit LoadingOverlay(QWidget* parent);
void showOver(); // 铺满父尺寸、置顶、显示(默认「加载中…」)
void showOver(const QString& message); // 同上,自定义提示文案(如「正在保存三维体…」)
void showOver(); // 铺满父尺寸、置顶、显示
protected:
bool eventFilter(QObject* obj, QEvent* ev) override; // 跟随父 resize
private:

View File

@ -11,11 +11,6 @@ inline double clamp01(double v) {
inline unsigned char lerpByte(unsigned char a, unsigned char b, double t) {
return static_cast<unsigned char>(a + (b - a) * t + 0.5);
}
// 两级透明度:每色 alpha × 整体透明度(渲染时相乘,不烘焙)。
inline core::Rgba applyGlobalAlpha(core::Rgba c, double g) {
c.a = static_cast<unsigned char>(c.a * g + 0.5);
return c;
}
} // namespace
ColorMapService::ColorMapService(const core::ColorScale& scale)
@ -52,18 +47,17 @@ double ColorMapService::normalized(double v) const {
}
core::Rgba ColorMapService::colorAtContinuous(double v) const {
const double g = scale_.globalOpacity(); // 两级第二级:整体透明度
if (normStops_.empty()) return applyGlobalAlpha(core::Rgba{0, 0, 0, 255}, g);
if (normStops_.size() == 1) return applyGlobalAlpha(normStops_.front().color, g);
if (normStops_.empty()) return core::Rgba{0, 0, 0, 255};
if (normStops_.size() == 1) return normStops_.front().color;
double t = normalized(v);
// 非有限值NaN/Inf可能来自降级后端的脏数据或退化数据范围回退首断点色
// 避免下方 upper_bound 用 NaN 比较返回 end() 后解引用越界(崩溃)。
if (!std::isfinite(t)) return applyGlobalAlpha(normStops_.front().color, g);
if (!std::isfinite(t)) return normStops_.front().color;
// 找到 t 落在哪两个 normStop 之间
if (t <= normStops_.front().pos) return applyGlobalAlpha(normStops_.front().color, g);
if (t >= normStops_.back().pos) return applyGlobalAlpha(normStops_.back().color, g);
if (t <= normStops_.front().pos) return normStops_.front().color;
if (t >= normStops_.back().pos) return normStops_.back().color;
// 二分查找第一个 pos > t
auto it = std::upper_bound(normStops_.begin(), normStops_.end(), t,
@ -74,16 +68,16 @@ core::Rgba ColorMapService::colorAtContinuous(double v) const {
double segLen = hi.pos - lo.pos;
double frac = (segLen > 0.0) ? (t - lo.pos) / segLen : 0.0;
return applyGlobalAlpha(core::Rgba{
return core::Rgba{
lerpByte(lo.color.r, hi.color.r, frac),
lerpByte(lo.color.g, hi.color.g, frac),
lerpByte(lo.color.b, hi.color.b, frac),
lerpByte(lo.color.a, hi.color.a, frac)
}, g);
};
}
core::Rgba ColorMapService::colorAtDiscrete(double v) const {
return applyGlobalAlpha(scale_.colorAt(v), scale_.globalOpacity());
return scale_.colorAt(v);
}
} // namespace geopro::app

View File

@ -1,57 +0,0 @@
#include "panels/chart/ColorScaleProperties.hpp"
#include <QJsonArray>
namespace geopro::app {
QString rgbaToColorBarCss(const geopro::core::Rgba& c) {
if (c.a >= 255)
return QStringLiteral("#%1%2%3")
.arg(c.r, 2, 16, QLatin1Char('0'))
.arg(c.g, 2, 16, QLatin1Char('0'))
.arg(c.b, 2, 16, QLatin1Char('0'))
.toUpper();
return QStringLiteral("rgba(%1, %2, %3, %4)")
.arg(c.r)
.arg(c.g)
.arg(c.b)
.arg(QString::number(c.a / 255.0, 'g', 3));
}
QJsonObject buildColorScaleProperties(const geopro::core::ColorScale& scale,
const ContourLineConfig& lineCfg, const QString& lvlSchemeType,
int logLinesCount, int equalAreaLayerCount,
bool includeLvlScheme) {
QJsonArray colorBar;
for (const auto& [value, color] : scale.stops())
colorBar.append(QJsonArray{QString::number(value, 'f', 2), rgbaToColorBarCss(color)});
QJsonObject lineConfig{
{QStringLiteral("showLines"), lineCfg.lineShow},
{QStringLiteral("color"), rgbaToColorBarCss(lineCfg.lineColor)},
{QStringLiteral("lineType"),
lineCfg.dashed ? QStringLiteral("dashed") : QStringLiteral("solid")}};
QJsonObject labelConfig{{QStringLiteral("showLabels"), lineCfg.labelShow},
{QStringLiteral("color"), rgbaToColorBarCss(lineCfg.labelColor)}};
QJsonObject props{{QStringLiteral("colorBar"), colorBar},
{QStringLiteral("opacity"), scale.globalOpacity()}, // 两级第二级:整体透明度
{QStringLiteral("lineConfig"), lineConfig},
{QStringLiteral("labelConfig"), labelConfig}};
if (includeLvlScheme) { // 等值面(网格/反演)路径:层级方案透传字段(复刻原版,整条覆盖写须带)
props[QStringLiteral("lvlSchemeType")] = lvlSchemeType;
props[QStringLiteral("logLinesCount")] = logLinesCount;
props[QStringLiteral("equalAreaLayerCount")] = equalAreaLayerCount;
}
return props;
}
QJsonObject withColorBarAndOpacity(const QJsonObject& base, const geopro::core::ColorScale& scale) {
QJsonObject props = base; // 保留 lineConfig/labelConfig/层级方案 等加载到的原值
QJsonArray colorBar;
for (const auto& [value, color] : scale.stops())
colorBar.append(QJsonArray{QString::number(value, 'f', 2), rgbaToColorBarCss(color)});
props[QStringLiteral("colorBar")] = colorBar; // 仅覆盖本次编辑的颜色
props[QStringLiteral("opacity")] = scale.globalOpacity(); // 与整体不透明度
return props;
}
} // namespace geopro::app

View File

@ -1,29 +0,0 @@
#pragma once
#include <QJsonObject>
#include <QString>
#include "ContourLineDialog.hpp" // ContourLineConfig
#include "model/ColorScale.hpp"
namespace geopro::app {
// core::Rgba → colorBar 颜色串(不透明 #RRGGBB半透明 rgba(r,g,b,a∈0..1)),与后端 colorBar 互通。
QString rgbaToColorBarCss(const geopro::core::Rgba& c);
// 组装色阶持久化 propertiescolorBar[每色含 alpha] + opacity[整体透明度] + lineConfig + labelConfig
// + lvlSchemeType/logLinesCount/equalAreaLayerCount[层级方案,复刻原版透传字段])。
// 散点/网格共用同一格式(同一条后端记录 businessCode="")。整条 properties 覆盖写,故层级字段必须带,
// 否则会清空 web 设过的值。opacity 为两级透明度的第二级。
// includeLvlScheme=falsemeasurement 散点(type3) 路径,不写等值面专属的层级方案字段(对齐原版
// scatters「仅发 colorBar/lineConfig/labelConfig」+ 桌面两级 opacity避免向 R0 记录注入无关字段。
QJsonObject buildColorScaleProperties(const geopro::core::ColorScale& scale,
const ContourLineConfig& lineCfg,
const QString& lvlSchemeType = QStringLiteral("normal"),
int logLinesCount = 8, int equalAreaLayerCount = 10,
bool includeLvlScheme = true);
// load-then-save 回写(对齐原版 originPage在加载到的原始 properties 上【只覆盖 colorBar+opacity】
// 其余字段lineConfig/labelConfig/层级方案)原样保留,避免散点保存清掉网格(共用同一条记录)的值。
QJsonObject withColorBarAndOpacity(const QJsonObject& base, const geopro::core::ColorScale& scale);
} // namespace geopro::app

View File

@ -195,7 +195,7 @@ void ContourPlotItem::buildFillImage(const core::Grid& g, ColorMapService* svc)
double v = (v00 * (1 - ti) + v10 * ti) * (1 - tj) +
(v01 * (1 - ti) + v11 * ti) * tj;
auto c = svc->colorAtDiscrete(v); // 离散色带 → 平滑填充带边界
scan[px] = qRgba(c.r, c.g, c.b, c.a); // 听色阶 alphaalpha=0 真透明(无 alpha 色阶默认 255 不受影响)
scan[px] = qRgba(c.r, c.g, c.b, c.a ? c.a : 255);
}
}
fillImage_ = std::move(img);

View File

@ -17,8 +17,7 @@ std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget*
std::function<QString()> projectIdGetter,
geopro::data::IDatasetCommandRepository* cmdRepo,
std::function<QString()> dsIdGetter,
std::function<QString()> tmObjectIdGetter,
geopro::controller::DatasetViewState* viewState) {
std::function<QString()> tmObjectIdGetter) {
switch (kind) {
case controller::ViewKind::Scatter: {
auto* raw = new RawDataChartView(parent);
@ -26,8 +25,6 @@ std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget*
raw->setCommandRepo(cmdRepo, dsIdGetter, projectIdGetter);
// 注入色阶模板仓储(散点「色阶配置」编辑器另存为/打开/覆盖用projectId 复用上面的 getter
raw->setColorTemplateRepo(colorTplRepo);
// 注入跨视图色阶真源(反演原数据 type1 与网格/3D 共用色阶 → 实时联动measurement 不路由)。
raw->setViewState(viewState);
return std::unique_ptr<IDetailView>(raw);
}
case controller::ViewKind::FilledContour: {
@ -38,8 +35,6 @@ std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget*
grid->setCommandRepo(cmdRepo, std::move(dsIdGetter), std::move(projectIdGetter));
// 注入 tmObjectId 取值回调(白化对话框模板列表用,= 数据集 structParentId
grid->setTmObjectIdGetter(std::move(tmObjectIdGetter));
// 注入跨视图色阶真源编辑→真源→3D 帘面/体等跟随;本视图也跟随他视图改色)。
grid->setViewState(viewState);
return std::unique_ptr<IDetailView>(grid);
}
case controller::ViewKind::Table: {

View File

@ -13,10 +13,6 @@ class IColorTemplateRepository;
class IDatasetCommandRepository;
}
namespace geopro::controller {
class DatasetViewState; // 跨视图色阶真源(统一同步)
}
namespace geopro::app {
class IDetailView;
@ -33,7 +29,6 @@ std::unique_ptr<IDetailView> makeDetailView(
std::function<QString()> projectIdGetter = {},
geopro::data::IDatasetCommandRepository* cmdRepo = nullptr,
std::function<QString()> dsIdGetter = {},
std::function<QString()> tmObjectIdGetter = {},
geopro::controller::DatasetViewState* viewState = nullptr);
std::function<QString()> tmObjectIdGetter = {});
} // namespace geopro::app

View File

@ -36,8 +36,6 @@
#include "panels/chart/AutoAnnotationDialog.hpp"
#include "panels/chart/ColorBarWidget.hpp"
#include "panels/chart/ColorMapService.hpp"
#include "panels/chart/ColorScaleProperties.hpp"
#include "DatasetViewState.hpp"
#include "panels/chart/ContourDrawTool.hpp"
#include "panels/chart/ContourHoverTip.hpp"
#include "panels/chart/ContourPlotItem.hpp"
@ -326,7 +324,7 @@ void GridDataChartView::openColorScaleEditor() {
tplRepo_, projectId, lvlTemplateId_, this);
if (dlg.exec() != QDialog::Accepted) return;
const auto cs = dlg.colorScale();
gridScale_ = dlg.colorScale();
lineCfg_ = dlg.lineConfig();
showLabels_ = lineCfg_.labelShow; // 标注显隐同步 + 回写工具条复选框(避免 UI 与状态脱钩)
if (chkShowLabels_) {
@ -334,63 +332,10 @@ void GridDataChartView::openColorScaleEditor() {
chkShowLabels_->setChecked(showLabels_);
}
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
// 统一同步:写入色阶真源 → 经 colorScaleChanged 触发本视图(及 3D 帘面/体等)重渲染。
// 无状态层(理论不至)才本地兜底重绘。
if (state_ && !dsId.isEmpty()) {
state_->setColorScale(dsId, cs);
} else {
gridScale_ = cs;
applyColorScaleRender();
}
persistColorScale(dsId, cs, dlg.lvlSchemeType(), dlg.logLinesCount(),
dlg.equalAreaLayerCount()); // 持久化到后端businessCode="",与散点同一条记录)
}
void GridDataChartView::applyColorScaleRender() {
delete colorSvc_;
colorSvc_ = new ColorMapService(gridScale_);
rebuildContour();
if (colorBar_) colorBar_->setColorScale(gridScale_);
}
void GridDataChartView::setViewState(geopro::controller::DatasetViewState* state) {
state_ = state;
if (!state_) return;
connect(state_, &geopro::controller::DatasetViewState::colorScaleChanged, this,
&GridDataChartView::onColorScaleChanged);
}
void GridDataChartView::onColorScaleChanged(const QString& dsId) {
if (!state_ || !hasGrid_) return;
if (!dsIdGetter_ || dsIdGetter_() != dsId) return; // 只跟随本视图所示数据集
const auto* cs = state_->colorScale(dsId);
if (!cs) return;
gridScale_ = *cs;
applyColorScaleRender();
}
void GridDataChartView::persistColorScale(const QString& dsId, const geopro::core::ColorScale& cs,
const QString& lvlSchemeType, int logLinesCount,
int equalAreaLayerCount) {
if (!cmdRepo_ || dsId.isEmpty()) return; // 无仓储/无 dsId → 仅本地生效(不阻塞)
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
// 网格(getDetail type2) 与 反演散点(type1) 共用 businessCode=""(后端按 (dsObjectId,businessCode)
// 存唯一记录save 无 type 字段。properties 含每色 alpha + 整体透明度 + 层级方案透传字段。
QJsonObject body{
{QStringLiteral("dsObjectId"), dsId},
{QStringLiteral("templateId"), lvlTemplateId_},
{QStringLiteral("businessCode"), QString()},
{QStringLiteral("projectId"), projectId},
{QStringLiteral("properties"),
buildColorScaleProperties(cs, lineCfg_, lvlSchemeType, logLinesCount, equalAreaLayerCount)},
};
QPointer<GridDataChartView> self(this);
cmdRepo_->saveColorGradation(body, [self](bool ok, QString msg) {
if (!self || ok) return;
QMessageBox::warning(self, QStringLiteral("色阶配置"),
msg.isEmpty() ? QStringLiteral("色阶保存失败") : msg);
});
colorBar_->setColorScale(gridScale_);
}
void GridDataChartView::setCommandRepo(geopro::data::IDatasetCommandRepository* repo,

View File

@ -4,7 +4,6 @@
#include <vector>
#include <QJsonObject> // submitDrawnException 默认参数 const QJsonObject& = {} 需完整类型
#include <QPointer>
#include <QString>
#include <QWidget>
@ -29,10 +28,6 @@ class IColorTemplateRepository;
class IDatasetCommandRepository;
}
namespace geopro::controller {
class DatasetViewState; // 跨视图色阶真源(统一同步机制)
}
namespace geopro::app {
class AnomalyTablePanel;
@ -76,17 +71,9 @@ public:
tmObjectIdGetter_ = std::move(tmObjectIdGetter);
}
// 注入跨视图色阶真源(统一同步):编辑写入它、并连 colorScaleChanged 跟随他视图(如 3D改色。
void setViewState(geopro::controller::DatasetViewState* state);
private:
void rebuildContour(); // 按当前显隐开关重建并重绘 ContourPlotItem
void openColorScaleEditor(); // 「色阶配置」→ 共享色阶编辑器(色阶 + 层级⚙ + 线形⚙)
void applyColorScaleRender(); // 用当前 gridScale_ 重建色彩服务/等值面/色阶条
void onColorScaleChanged(const QString& dsId); // 色阶真源变更(本视图或他视图编辑)→ 跟随重渲染
void persistColorScale(const QString& dsId, const geopro::core::ColorScale& cs,
const QString& lvlSchemeType, int logLinesCount,
int equalAreaLayerCount); // 存后端(含层级方案透传字段)
void openGridWizard(); // I1「网格」→ 网格化向导
void openWhitening(); // I3「白化」→ 白化弹窗
void openFilter(); // I4「滤波处理」→ 滤波弹窗
@ -144,8 +131,6 @@ private:
// tmObjectId 取值回调(= 数据集 structParentId。白化对话框模板列表用空 → 模板列表为空。
std::function<QString()> tmObjectIdGetter_;
QPointer<geopro::controller::DatasetViewState> state_; // 跨视图色阶真源注入QPointer 自动判空防悬挂)
};
} // namespace geopro::app

View File

@ -2,8 +2,6 @@
#include "ColorScaleConfigDialog.hpp"
#include "panels/chart/ChartTheme.hpp"
#include "panels/chart/ColorBarWidget.hpp"
#include "panels/chart/ColorScaleProperties.hpp"
#include "DatasetViewState.hpp"
#include "panels/chart/GridWizardDialog.hpp"
#include "panels/chart/InversionFormDialog.hpp"
#include "panels/chart/SaveAsDialog.hpp"
@ -304,6 +302,41 @@ void styleToolIconButton(QToolButton* btn, const QIcon& icon) {
btn->setCursor(Qt::PointingHandCursor);
}
// core::Rgba → colorBar 颜色串(与 ColorScaleConfigDialog::rgbaToCss 同格式:不透明 #RRGGBB
// 半透明 rgba(r,g,b,a∈0..1)),与后端 colorBar 互通。
QString rgbaToColorBarCss(const geopro::core::Rgba& c) {
if (c.a >= 255)
return QStringLiteral("#%1%2%3")
.arg(c.r, 2, 16, QLatin1Char('0'))
.arg(c.g, 2, 16, QLatin1Char('0'))
.arg(c.b, 2, 16, QLatin1Char('0'))
.toUpper();
return QStringLiteral("rgba(%1, %2, %3, %4)")
.arg(c.r)
.arg(c.g)
.arg(c.b)
.arg(QString::number(c.a / 255.0, 'g', 3));
}
// 组装色阶 propertiescolorBar + lineConfig + labelConfig与原版散点路径
// newLvlColorLevel 一致battery/scatters 仅发这三块,不含 lvlSchemeType 等等值面专属字段)。
QJsonObject buildColorScaleProperties(const geopro::core::ColorScale& scale,
const ContourLineConfig& lineCfg) {
QJsonArray colorBar;
for (const auto& [value, color] : scale.stops())
colorBar.append(QJsonArray{QString::number(value, 'f', 2), rgbaToColorBarCss(color)});
QJsonObject lineConfig{
{QStringLiteral("showLines"), lineCfg.lineShow},
{QStringLiteral("color"), rgbaToColorBarCss(lineCfg.lineColor)},
{QStringLiteral("lineType"),
lineCfg.dashed ? QStringLiteral("dashed") : QStringLiteral("solid")}};
QJsonObject labelConfig{{QStringLiteral("showLabels"), lineCfg.labelShow},
{QStringLiteral("color"), rgbaToColorBarCss(lineCfg.labelColor)}};
return QJsonObject{{QStringLiteral("colorBar"), colorBar},
{QStringLiteral("lineConfig"), lineConfig},
{QStringLiteral("labelConfig"), labelConfig}};
}
} // namespace
void RawDataChartView::showNotImplemented(QWidget* anchor) {
@ -367,21 +400,16 @@ void RawDataChartView::openInversionColorScale(QWidget* anchor) {
this);
if (dlg.exec() != QDialog::Accepted) return;
const auto cs = dlg.colorScale();
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
// 统一同步:反演原数据(type1,"")与网格/3D 共用 → 写真源,经 colorScaleChanged 各视图(含自身)重绘。
if (state_ && !dsId.isEmpty()) {
state_->setColorScale(dsId, cs);
} else {
data_.scale = cs;
delete colorSvc_;
colorSvc_ = new ColorMapService(data_.scale);
redrawScatter();
colorBar_->setColorScale(data_.scale);
}
// 本地重建上色重绘。
data_.scale = dlg.colorScale();
delete colorSvc_;
colorSvc_ = new ColorMapService(data_.scale);
redrawScatter();
colorBar_->setColorScale(data_.scale);
showToast(this, QStringLiteral("色阶应用成功")); // M8 成功提示(对照原版 Message.success
// 持久化businessCode 空,对照原版 originPage newLvlColorLevel businessCode:'')。
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
if (!cmdRepo_ || dsId.isEmpty()) return;
QJsonObject body{
@ -389,13 +417,7 @@ void RawDataChartView::openInversionColorScale(QWidget* anchor) {
{QStringLiteral("templateId"), data_.templateId}, // 读取到的色阶模板 id对照原版可空
{QStringLiteral("businessCode"), QString()},
{QStringLiteral("projectId"), projectId},
// load-then-save在加载到的原始 properties 上只覆盖 colorBar+opacity保留网格设过的
// lineConfig/层级(共用同一条 businessCode="" 记录);无原始记录(首次)才整条新建。
{QStringLiteral("properties"),
data_.properties.isEmpty()
? buildColorScaleProperties(cs, dlg.lineConfig(), dlg.lvlSchemeType(),
dlg.logLinesCount(), dlg.equalAreaLayerCount())
: withColorBarAndOpacity(data_.properties, cs)},
{QStringLiteral("properties"), buildColorScaleProperties(data_.scale, dlg.lineConfig())},
};
QPointer<RawDataChartView> self(this);
cmdRepo_->saveColorGradation(body, [self](bool ok, QString msg) {
@ -405,26 +427,6 @@ void RawDataChartView::openInversionColorScale(QWidget* anchor) {
});
}
void RawDataChartView::setViewState(geopro::controller::DatasetViewState* state) {
state_ = state;
if (!state_) return;
connect(state_, &geopro::controller::DatasetViewState::colorScaleChanged, this,
&RawDataChartView::onColorScaleChanged);
}
void RawDataChartView::onColorScaleChanged(const QString& dsId) {
if (!state_) return;
if (!dsIdGetter_ || dsIdGetter_() != dsId) return; // 只跟随本视图所示数据集
const auto* cs = state_->colorScale(dsId);
if (!cs) return;
data_.scale = *cs;
delete colorSvc_;
colorSvc_ = new ColorMapService(data_.scale);
redrawScatter();
if (data_.verticalLegend) colorBarV_->setColorScale(data_.scale);
else colorBar_->setColorScale(data_.scale);
}
void RawDataChartView::openInversionSaveAs(QWidget* anchor) {
// O3另存为复用 SaveAsDialog::Inversion → saveInversionAsData
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
@ -597,10 +599,7 @@ void RawDataChartView::openScatterColorScale(QWidget* anchor) {
{QStringLiteral("templateId"), data_.templateId}, // 读取到的色阶模板 id对照原版可空
{QStringLiteral("businessCode"), currentVFieldCode()},
{QStringLiteral("projectId"), projectId},
// measurement(type3,"R0") 独立记录不写等值面层级方案字段includeLvlScheme=false
{QStringLiteral("properties"),
buildColorScaleProperties(data_.scale, dlg.lineConfig(), QStringLiteral("normal"), 8, 10,
/*includeLvlScheme=*/false)},
{QStringLiteral("properties"), buildColorScaleProperties(data_.scale, dlg.lineConfig())},
};
QPointer<RawDataChartView> self(this);
cmdRepo_->saveColorGradation(body, [self](bool ok, QString msg) {

View File

@ -1,7 +1,6 @@
#pragma once
#include <functional>
#include <QPointer>
#include <QString>
#include <QWidget>
#include "model/detail/DetailPayloads.hpp"
@ -19,10 +18,6 @@ class IDatasetCommandRepository;
class IColorTemplateRepository;
}
namespace geopro::controller {
class DatasetViewState; // 跨视图色阶真源(统一同步)
}
namespace geopro::app {
class ColorBarWidget;
@ -57,10 +52,6 @@ public:
// setCommandRepo 注入的 projectIdGetter_。可传空 → 编辑器后端按钮禁用。
void setColorTemplateRepo(geopro::data::IColorTemplateRepository* repo);
// 注入跨视图色阶真源:反演原数据散点(type1, businessCode="")与网格/3D 共用色阶 → 实时联动。
// measurement(type3,"R0") 单视图、不路由真源openScatterColorScale 不经此)。
void setViewState(geopro::controller::DatasetViewState* state);
protected:
// 信息模式M13下捕获画布点击找最近散点显示属性。其余事件不消费。
bool eventFilter(QObject* obj, QEvent* ev) override;
@ -79,7 +70,6 @@ private:
// 反演原数据默认工具条交互O1/O2/O3
void openGridWizard(QWidget* anchor); // O1 网格化向导(复用 GridWizardDialog
void openInversionColorScale(QWidget* anchor); // O2 原数据散点色阶type1businessCode=''
void onColorScaleChanged(const QString& dsId); // 色阶真源变更(本视图或网格/3D)→ type1 散点跟随重绘
void openInversionSaveAs(QWidget* anchor); // O3 另存为(复用 SaveAsDialog::Inversion
// measurement 交互:
@ -140,8 +130,6 @@ private:
std::function<QString()> projectIdGetter_;
// 色阶模板仓储(注入;空则编辑器「另存为/打开」禁用)。
geopro::data::IColorTemplateRepository* colorTplRepo_ = nullptr;
QPointer<geopro::controller::DatasetViewState> state_; // 跨视图色阶真源(注入;仅 type1 路由QPointer 防悬挂)
};
} // namespace geopro::app

View File

@ -1,17 +1,10 @@
#include "panels/columns/CategoryAnalysisTab.hpp"
#include <QAbstractItemView>
#include <QLabel>
#include <QPointer>
#include <QScrollArea>
#include <QScrollBar>
#include <QTimer>
#include <QTreeWidget>
#include <QVBoxLayout>
#include "Theme.hpp"
#include "panels/columns/CategorySection.hpp"
#include "repo/CategoryDescriptor.hpp" // categoryCatalog含 trajectory 段)
namespace geopro::app {
@ -22,44 +15,32 @@ CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* d
outer->setSpacing(0);
auto* scroll = new QScrollArea(this);
scroll_ = scroll;
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // 内容随面板宽自适应,不出横向滚动条
outer->addWidget(scroll, 1);
auto* content = new QWidget(scroll);
content_ = content;
auto* col = new QVBoxLayout(content);
col_ = col;
col->setContentsMargins(0, space::kSm, 0, space::kSm); // 顶部留白:首段段头不贴顶
col->setContentsMargins(0, 0, 0, 0);
col->setSpacing(space::kSm);
for (const auto& desc : geopro::data::categoryCatalog()) {
auto* sec = new CategorySection(desc, dict, content);
sections_[desc.id] = sec;
ordered_.push_back(sec);
connect(sec, &CategorySection::collapsedChanged, this,
&CategoryAnalysisTab::relayoutSections); // 折叠/展开 → 重排 stretch向上收
const std::string segId = desc.id;
for (const CategorySpec& spec : categoryConfigs()) {
auto* sec = new CategorySection(spec, dict, content);
sections_[spec.id] = sec;
const std::string segId = spec.id;
connect(sec, &CategorySection::checkedDatasetsChanged, this,
[this, segId](const QStringList& ids) {
checkedBySeg_[segId] = ids;
recomputeCheckedUnion();
updatePlaceholderAndVisibility(); // 勾选/段内容变化后段「有无行」可能变 → 刷新显隐
});
connect(sec, &CategorySection::generateVolumeRequested, this,
&CategoryAnalysisTab::generateVolumeRequested);
// 注:「导入雷达测线」入口已迁至 TopBar「设备」菜单Task D1CategorySection 段头按钮与
// CategoryAnalysisTab::radarImportRequested 信号均已移除。
connect(sec, &CategorySection::detailRequested, this, &CategoryAnalysisTab::detailRequested);
connect(sec, &CategorySection::datasetActivated, this, &CategoryAnalysisTab::datasetActivated);
connect(sec, &CategorySection::deleteDatasetRequested, this,
&CategoryAnalysisTab::deleteDatasetRequested);
connect(sec, &CategorySection::sliceRequested, this, &CategoryAnalysisTab::sliceRequested);
connect(sec, &CategorySection::colorScaleRequested, this, &CategoryAnalysisTab::colorScaleRequested);
connect(sec, &CategorySection::radarGainModeRequested, this,
&CategoryAnalysisTab::radarGainModeRequested);
connect(sec, &CategorySection::sliceSaveRequested, this, &CategoryAnalysisTab::sliceSaveRequested);
connect(sec, &CategorySection::sliceSaveAsRequested, this, &CategoryAnalysisTab::sliceSaveAsRequested);
connect(sec, &CategorySection::sliceExportImageRequested, this,
@ -68,80 +49,23 @@ CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* d
&CategoryAnalysisTab::sliceExportDatRequested);
connect(sec, &CategorySection::anomalyVisibilityChanged, this,
&CategoryAnalysisTab::anomalyVisibilityChanged);
// 全列互斥选中:某段选中非空数据行 → 清其余各段选中,保证整列至多一个 ds 选中,
// 避免「电阻率段与三维体段各选一行」的二义贴合轴该听谁。clearSelection 内部 QSignalBlocker
// 阻断,被清段不回发 datasetSelected(空)无环路inSelectionSync_ 为兜底防重入。
connect(sec, &CategorySection::datasetSelected, this,
[this, sec](const QString& dsId, const QString& ddCode) {
if (!dsId.isEmpty() && !inSelectionSync_) {
inSelectionSync_ = true;
for (auto* other : ordered_)
if (other != sec) other->clearSelection();
inSelectionSync_ = false;
}
emit datasetSelected(dsId, ddCode);
});
connect(sec, &CategorySection::planeZChanged, this, &CategoryAnalysisTab::planeZChanged);
connect(sec, &CategorySection::basemapKindChanged, this,
&CategoryAnalysisTab::basemapKindChanged);
connect(sec, &CategorySection::basemapOpacityChanged, this,
&CategoryAnalysisTab::basemapOpacityChanged);
connect(sec, &CategorySection::datasetSelected, this, &CategoryAnalysisTab::datasetSelected);
// #7各段等分 stretch → 内容都少时四段平分高度填满面板(初始与 VTK 区等高、不出滚动条)
// 某段内容增多时其最小高度(=内容总高)撑大,超出视口则由外层 QScrollArea 统一出纵向滚动条。
col->addWidget(sec, 1);
}
// 尾部弹簧(末项):默认 0全部段折叠时由 relayoutSections 置 1吸收余量把段头顶到顶部。
col->addStretch(0);
scroll->setWidget(content);
// 全空占位:所有段都无可渲染数据行时显示,与 scroll 同级、互斥显隐(由 updatePlaceholderAndVisibility 切换)。
auto* placeholder = new QLabel(QStringLiteral("请在左侧对象树勾选测线 / 数据集"), this);
placeholder->setAlignment(Qt::AlignCenter);
placeholder->setWordWrap(true);
applyTokenizedStyleSheet(placeholder,
QStringLiteral("QLabel{color:{{text/tertiary}};padding:24px;}"));
outer->addWidget(placeholder, 0);
placeholder->hide();
placeholder_ = placeholder;
updatePlaceholderAndVisibility(); // 初始无数据 → 直接进入占位态
}
void CategoryAnalysisTab::relayoutSections() {
if (!col_) return;
int expanded = 0;
for (auto* sec : ordered_)
if (sec->isExpanded()) ++expanded;
// 展开段 stretch=1吸收余量、铺满折叠段 stretch=0只占段头高下方不再留空
for (auto* sec : ordered_) col_->setStretchFactor(sec, sec->isExpanded() ? 1 : 0);
// 尾部弹簧:仅当全部折叠时=1把所有段头顶到顶部有任一展开段时=0由展开段吸收余量
col_->setStretch(col_->count() - 1, expanded == 0 ? 1 : 0);
}
void CategoryAnalysisTab::setBuckets(const CategoryBuckets& b) {
// splitByCategory 现按 categoryCatalog() 分桶(含 trajectory 桶,自然分发到轨迹段)。
// 轨迹段已随 catalog 在构造时建出Task C2section() 命中存在的段;下方 guard 仍保平衡兜底。
const auto& cat = geopro::data::categoryCatalog();
for (std::size_t i = 0; i < cat.size() && i < b.segments.size(); ++i) {
const auto& cfg = categoryConfigs();
for (std::size_t i = 0; i < cfg.size() && i < b.segments.size(); ++i) {
// voxel(三维体) 段数据来自 mock voxelTree(体/切片/异常),由调用方单独 section("voxel")->setDatasets
// 注入splitByCategory 对它永远是空桶,若在此用空桶覆盖会先清掉其勾选(随后重填但勾选已丢) →
// 表现为「创建切片/异常后体/切片选择被清空」(用户 issue 1)。故跳过 voxel勿覆盖。
if (cat[i].id == "voxel") continue;
if (auto* sec = section(cat[i].id)) sec->setDatasets(b.segments[i]);
if (cfg[i].id == "voxel") continue;
if (auto* sec = section(cfg[i].id)) sec->setDatasets(b.segments[i]);
}
updatePlaceholderAndVisibility(); // 分发后据各段有无数据刷新显隐 + 占位
}
void CategoryAnalysisTab::updatePlaceholderAndVisibility() {
bool anyVisible = false;
for (auto* sec : ordered_) {
const bool has = sec->hasRenderableRows();
sec->setVisible(has);
if (has) anyVisible = true;
}
if (scroll_) scroll_->setVisible(anyVisible);
if (placeholder_) placeholder_->setVisible(!anyVisible);
relayoutSections();
}
void CategoryAnalysisTab::setStructure(const std::vector<geopro::data::StructNode>& nodes) {
@ -157,58 +81,6 @@ CategorySection* CategoryAnalysisTab::section(const std::string& id) const {
return it != sections_.end() ? it->second : nullptr;
}
QStringList CategoryAnalysisTab::subtreeDsIds(const QString& dsId) const {
for (auto* sec : ordered_) {
const QStringList ids = sec->subtreeDsIds(dsId);
if (!ids.isEmpty()) return ids; // ds 归属唯一段 → 首个命中段即答案
}
return {};
}
// ds 归属唯一段未知 → 广播到各段,仅含该 ds 的段会命中生效(其余 no-op
void CategoryAnalysisTab::setItemChecked(const QString& dsId, bool on) {
for (auto* sec : ordered_) sec->setChecked(dsId, on);
}
void CategoryAnalysisTab::setItemBusy(const QString& dsId, bool busy) {
for (auto* sec : ordered_) sec->setBusy(dsId, busy);
}
void CategoryAnalysisTab::clearAllBusy() {
for (auto* sec : ordered_) sec->clearAllBusy();
}
void CategoryAnalysisTab::scrollItemToTop(const QString& dsId) {
// 先就地展开所在段(同步),再进入多拍重试定位(等布局/滚动条范围结算)。
for (auto* sec : ordered_)
if (sec->itemFor(dsId)) { sec->ensureExpanded(); break; }
scrollItemToTopRetry(dsId, /*attemptsLeft=*/5);
}
void CategoryAnalysisTab::scrollItemToTopRetry(const QString& dsId, int attemptsLeft) {
if (!scroll_ || !content_) return;
CategorySection* sec = nullptr;
QTreeWidgetItem* item = nullptr;
for (auto* s : ordered_)
if ((item = s->itemFor(dsId)) != nullptr) { sec = s; break; }
if (sec && item) {
sec->ensureExpanded();
for (QTreeWidgetItem* p = item->parent(); p; p = p->parent())
p->setExpanded(true); // 展开树内父节点,使目标行有有效几何
QTreeWidget* tree = sec->listWidget();
tree->scrollToItem(item, QAbstractItemView::PositionAtTop); // 内层树(若有内滚动)
// 行顶映射到滚动内容坐标 → 设外层滚动条把该行顶到面板最上方。
const int y = tree->viewport()->mapTo(content_, tree->visualItemRect(item).topLeft()).y();
scroll_->verticalScrollBar()->setValue(y);
}
// 多拍重试:每拍布局更趋稳定(滚动条 range 长够、行几何更新),末拍稳定到位 → 根治"有时滚不到位"。
if (attemptsLeft > 0) {
QPointer<CategoryAnalysisTab> self(this);
const QString id = dsId;
QTimer::singleShot(16, this, [self, id, attemptsLeft]() {
if (self) self->scrollItemToTopRetry(id, attemptsLeft - 1);
});
}
}
void CategoryAnalysisTab::recomputeCheckedUnion() {
QStringList all; // ds 归属唯一段,跨段不重复,直接拼接
for (const auto& [id, ids] : checkedBySeg_) all += ids;

View File

@ -4,9 +4,6 @@
#include <map>
#include <string>
#include <vector>
class QVBoxLayout;
class QScrollArea;
#include "DatasetCategory.hpp" // CategoryBuckets
#include "interact/SlicePlaneMath.hpp" // geopro::render::interact::SliceAxis
#include "repo/RepoTypes.hpp"
@ -26,58 +23,31 @@ class CategoryAnalysisTab : public QWidget {
public:
explicit CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* dict, QWidget* parent = nullptr);
void setBuckets(const CategoryBuckets& b); // 分发到各大类段(与 categoryCatalog() 同序)
void setBuckets(const CategoryBuckets& b); // 分发到 5 段(与 categoryConfigs 同序)
void setStructure(const std::vector<geopro::data::StructNode>& nodes); // 转发各段
void refreshArrayFilters(); // 装置枚举异步加载后,重填各段装置筛选下拉
CategorySection* section(const std::string& id) const; // 按 CategoryDescriptor.id 取段
// 该 ds 所在段的层级子树 dsId 集(贴合坐标轴子树盒):遍历各段,返首个命中段的 subtreeDsIds。空=无段含该 ds。
QStringList subtreeDsIds(const QString& dsId) const;
// ── 三维体生成后:自动勾选触发渲染 + 标题前等待动画(spinner)反馈(转发到含该 ds 的段)──
void setItemChecked(const QString& dsId, bool on); // 自动勾选(触发渲染)
void setItemBusy(const QString& dsId, bool busy); // 复选框↔等待 spinner 切换
void clearAllBusy(); // 撤回所有 spinner失败兜底
void scrollItemToTop(const QString& dsId); // 把某行尽量滚到面板顶部(新建三维体后定位)
void refreshVisibility() { updatePlaceholderAndVisibility(); } // 外部注入(如 voxel setDatasets)后刷新段显隐/占位
CategorySection* section(const std::string& id) const; // 按 CategorySpec.id 取段
signals:
void checkedDatasetsChanged(const QStringList& dsIds); // 5 段勾选并集
void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds);
void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 右键「详情」=属性弹窗
void datasetActivated(const QString& dsId, const QString& ddCode, const QString& name); // 双击=适配+图表联动(T4)
void detailRequested(const QString& dsId, const QString& ddCode, const QString& name);
void deleteDatasetRequested(const QString& dsId, const QString& ddCode); // 右键删除切片/异常
// ── 三维体段操作转发(迁自旧 Column3DAnalysis全接──
void sliceRequested(geopro::render::interact::SliceAxis axis, const QString& volumeDsId);
void colorScaleRequested(const QString& dsId);
void radarGainModeRequested(const QString& dsId, int mode); // 雷达体显示增益模式(0关/1AGC/2保幅)
void sliceSaveRequested(const QString& dsId);
void sliceSaveAsRequested(const QString& dsId);
void sliceExportImageRequested(const QString& dsId);
void sliceExportDatRequested(const QString& dsId);
void anomalyVisibilityChanged(const QString& dsId, bool vis);
void datasetSelected(const QString& dsId, const QString& ddCode); // 树选中→VTK 高亮联动
void planeZChanged(const QString& typeId, double z); // 2D 段 z 值滑块:整体升降该类型平面
void basemapKindChanged(const QString& typeId, int kind); // 2D 段底图弹窗:矢量平面(0)/无(1)
void basemapOpacityChanged(const QString& typeId, double o); // 2D 段底图弹窗:透明度[0,1]
private:
void recomputeCheckedUnion();
// scrollItemToTop 的多拍重试实现:展开段/新增行后布局与滚动条范围需多次结算,单拍常滚不到位。
// 每拍重算行位置并设滚动条,剩余拍数耗尽前持续校正 → 末拍几何稳定后行稳定到顶。
void scrollItemToTopRetry(const QString& dsId, int attemptsLeft);
// 据各段折叠态重排 stretch折叠段=0(仅段头高)、展开段=1(吸收余量);全折叠时尾部弹簧吸收→段头顶到顶部。
// 仅在面板不产生滚动条(内容short于视口)时有可见效果——正是用户反馈的"折叠后停在原位中间"场景。
void relayoutSections();
// 据各段是否有可渲染数据行 显隐段;全空时显示占位提示、隐藏滚动区。数据变化(分发/勾选/注入)后调用。
void updatePlaceholderAndVisibility();
std::map<std::string, CategorySection*> sections_;
std::vector<CategorySection*> ordered_; // 按 categoryCatalog() 顺序relayout 遍历用)
QScrollArea* scroll_ = nullptr; // 外层滚动区scrollItemToTop 定位用)
QWidget* content_ = nullptr; // 滚动内容容器(坐标映射用)
QVBoxLayout* col_ = nullptr; // 段堆叠布局(末项=尾部弹簧)
QWidget* placeholder_ = nullptr; // 全空时显示的占位提示(与 scroll_ 同级,互斥显隐)
std::map<std::string, QStringList> checkedBySeg_; // 各段当前勾选(合并成并集)
bool inSelectionSync_ = false; // 跨段互斥清选进行中标记防重入信号环clearSelection 已阻断信号,此为兜底)
};
} // namespace geopro::app

View File

@ -8,23 +8,17 @@
#include <QHBoxLayout>
#include <QLabel>
#include <QMenu>
#include <QMouseEvent>
#include <QPoint>
#include <QPushButton>
#include <QSet>
#include <QSignalBlocker>
#include <QSizePolicy>
#include <QSlider>
#include <QTimer>
#include <QToolButton>
#include <QTreeWidget>
#include <QWidgetAction>
#include <QTreeWidgetItemIterator>
#include <QVBoxLayout>
#include "Theme.hpp"
#include "panels/DatasetListPanel.hpp"
#include "panels/columns/SectionIconBar.hpp"
#include "repo/DatasetFieldDictionary.hpp"
namespace geopro::app {
@ -32,81 +26,36 @@ namespace geopro::app {
using geopro::data::DsRow;
using geopro::data::DsTypeFields;
namespace {
// 段头图标条默认上限spec §6超过则末位收进「…」下拉。钉死为常量而非随操作数浮动
// 否则上限恒=操作数、计数溢出折叠分支永不触发(当前各段操作 ≤3对用户不可见但保证分支正确
constexpr int kDefaultMaxIcons = 3;
} // namespace
CategorySection::CategorySection(const geopro::data::CategoryDescriptor& desc,
geopro::data::DatasetFieldDictionary* dict, QWidget* parent)
: QWidget(parent), desc_(desc), dict_(dict) {
CategorySection::CategorySection(const CategorySpec& spec, geopro::data::DatasetFieldDictionary* dict,
QWidget* parent)
: QWidget(parent), spec_(spec), dict_(dict) {
auto* root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
// 数据类型段头可折叠规范§4.3/§6chevron + 标题(title 字号·半粗) 右侧响应式图标条(SectionIconBar)。
// 段头加浅底 + 底分隔线作视觉分段;去原生小三角(难看)→chevron 文本前缀,随主题/hover 变色。
// 数据类型标题行spec §7折叠箭头+标题(左) | 「+新增三维体」(右,仅反演类,在标题行而非筛选行)。
auto* headerRow = new QWidget(this);
headerRow->setObjectName(QStringLiteral("secHeader"));
applyTokenizedStyleSheet(headerRow,
QStringLiteral("QWidget#secHeader{background:{{bg/panel-subtle}};"
"border-bottom:1px solid {{divider}};}"));
auto* hl = new QHBoxLayout(headerRow);
hl->setContentsMargins(space::kMd, space::kSm, space::kSm, space::kSm);
hl->setContentsMargins(space::kSm, 0, space::kSm, 0);
hl->setSpacing(space::kSm);
header_ = new QToolButton(headerRow);
header_->setText(QString::fromStdString(spec_.title));
header_->setCheckable(true);
header_->setChecked(true);
header_->setArrowType(Qt::NoArrow);
header_->setToolButtonStyle(Qt::ToolButtonTextOnly);
header_->setCursor(Qt::PointingHandCursor);
applyTokenizedStyleSheet(
header_, QStringLiteral("QToolButton{border:none;background:transparent;padding:0;"
"font-size:%1px;font-weight:%2;color:{{text/primary}};}"
"QToolButton:hover{color:{{accent/primary}};}")
.arg(scaledPx(type::kTitle))
.arg(type::kWeightSemibold));
auto syncHeader = [this] {
header_->setText((header_->isChecked() ? QStringLiteral("") : QStringLiteral(""))
+ QString::fromStdString(desc_.title));
};
syncHeader();
// 标题先让位:水平 Preferred + 最小宽 0列变窄时标题先收必要时裁字图标条守住自身
// sizeHint(全图标宽)直到真正没空间才折叠右侧图标进「…」。否则标题不肯缩→图标条被迫先折(过早)。
header_->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
header_->setMinimumWidth(0);
header_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
header_->setArrowType(Qt::DownArrow);
header_->setAutoRaise(true);
hl->addWidget(header_);
hl->addStretch(1);
// 段头图标条:遍历 desc_.operations经一处 OpKind→IconAction 映射装配spec §6
// glyph 键须命中 SectionIconBar::glyphFromKey 已识别集z 值无专用键,复用 collapse(竖向双箭头)。
iconBar_ = new SectionIconBar(headerRow);
std::vector<IconAction> acts;
for (geopro::data::OpKind op : desc_.operations) {
switch (op) {
case geopro::data::OpKind::GenerateVolume:
// dsTypeCode 不再由段配置带(描述符无此字段)→ 发空串,接收方按 sourceIds 解析类型。
acts.push_back({QStringLiteral("plus"), QStringLiteral("新增三维体"),
[this] { emit generateVolumeRequested(QString(), checkedDsIds()); }, {}});
break;
case geopro::data::OpKind::Filter:
acts.push_back({QStringLiteral("filter"), QStringLiteral("筛选"),
[this] { if (filterRow_) filterRow_->setVisible(!filterRow_->isVisible()); },
{}});
break;
case geopro::data::OpKind::PlaneZ:
acts.push_back({QStringLiteral("collapse"), QStringLiteral("z 值"), {},
[this](QToolButton* host) { showPlaneZPopup(host); }});
break;
case geopro::data::OpKind::Basemap:
acts.push_back({QStringLiteral("map"), QStringLiteral("底图"), {},
[this](QToolButton* host) { showBasemapPopup(host); }});
break;
}
if (spec_.canGenerateVolume) {
auto* gen = new QToolButton(headerRow);
gen->setText(QStringLiteral("+ 新增三维体"));
gen->setAutoRaise(true);
connect(gen, &QToolButton::clicked, this, [this] {
emit generateVolumeRequested(QString::fromStdString(spec_.dsTypeCode), checkedDsIds());
});
hl->addWidget(gen);
}
iconBar_->setMaxIcons(kDefaultMaxIcons); // spec §6 默认上限 3够宽全显超数/不够宽收进「…」)
iconBar_->setActions(acts);
hl->addWidget(iconBar_);
root->addWidget(headerRow);
body_ = new QWidget(this);
@ -114,27 +63,19 @@ CategorySection::CategorySection(const geopro::data::CategoryDescriptor& desc,
body->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kMd);
body->setSpacing(space::kSm);
// 筛选行(默认折叠,由段头 Filter 图标 toggle按 desc_.filters 装配 —— DateRange→采集时间范围(在前)
// ArrayType→装置类型下拉(在后)。未列出的维度不建控件passesFilters/rebuildList 视该控件缺席=不筛该维。
filterRow_ = new QWidget(body_);
auto* filterLay = new QHBoxLayout(filterRow_);
filterLay->setContentsMargins(0, 0, 0, 0);
filterLay->setSpacing(space::kSm);
for (geopro::data::FilterKind fk : desc_.filters) {
if (fk == geopro::data::FilterKind::DateRange) {
dateRange_ = new DateRangeEdit(filterRow_);
connect(dateRange_, &DateRangeEdit::rangeChanged, this, [this] { rebuildList(); });
filterLay->addWidget(dateRange_, 1);
} else if (fk == geopro::data::FilterKind::ArrayType) {
arrayCombo_ = new QComboBox(filterRow_);
arrayCombo_->addItem(QStringLiteral("全部装置"), QString());
connect(arrayCombo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this](int) { rebuildList(); });
filterLay->addWidget(arrayCombo_);
}
// 筛选行:采集时间范围(在前)+ 装置类型(在后,仅 ERT 类)。
auto* filterRow = new QHBoxLayout();
dateRange_ = new DateRangeEdit(body_);
connect(dateRange_, &DateRangeEdit::rangeChanged, this, [this] { rebuildList(); });
filterRow->addWidget(dateRange_, 1);
if (spec_.hasArrayTypeFilter) {
arrayCombo_ = new QComboBox(body_);
arrayCombo_->addItem(QStringLiteral("全部装置"), QString());
connect(arrayCombo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this](int) { rebuildList(); });
filterRow->addWidget(arrayCombo_);
}
filterRow_->hide(); // 默认折叠,点段头筛选图标展开
body->addWidget(filterRow_);
body->addLayout(filterRow);
// 段体:可勾选数据树(勾选=渲染)。复用数据列表卡片委托与 populateDatasetList。
list_ = new QTreeWidget(body_);
@ -145,122 +86,41 @@ CategorySection::CategorySection(const geopro::data::CategoryDescriptor& desc,
list_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
list_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
applyDatasetCardDelegate(list_);
list_->viewport()->installEventFilter(this); // 捕获按下瞬间选中态(默认改选前),供单击已选行 toggle 取消
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem* it, int) {
// 用户点勾选框会在按下→释放间先发 itemChanged标记本次按下改动了勾选框itemClicked 据此不取消选中
// (点勾选框只切渲染,不应连带取消该行选中/丢贴合轴)。程序化改勾选均走 SignalBlocker不到此处。
if (it && it == pressedItem_) checkToggledThisPress_ = true;
// 异常行复选框 = 该异常显隐(异常不进渲染勾选集,单独走 anomalyVisibilityChanged → setAnomalyVisible
if (it && it->data(0, kDsDdCodeRole).toString() == QStringLiteral("dd_anomaly"))
emit anomalyVisibilityChanged(it->data(0, kDsIdRole).toString(),
it->checkState(0) == Qt::Checked);
emitChecked();
});
// 单击已选中的数据行 → 取消选中toggle-offitemClicked 于释放时触发,此时选中态已落定;
// 仅当「按下瞬间该行已选中」且「本次未点勾选框」才取消。取消走 list_->clearSelection()(非成员
// clearSelection后者阻断信号→ 直发 itemSelectionChanged(空) → datasetSelected("") → 恢复全景轴、丢贴合轴。
// 双击仍可激活:首次释放虽可能 toggle 掉选中,随后 DblClick 会重新选中该行并发 datasetActivatedfit+详情)。
connect(list_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* it, int) {
if (!it || it != pressedItem_ || !pressedSelected_ || checkToggledThisPress_) return;
if (it->data(0, kDsDdCodeRole).toString() == QStringLiteral("container")) return; // 容器行不参与选中
pressedSelected_ = false; // 防重入
list_->clearSelection(); // 发空 itemSelectionChanged → datasetSelected("") → 上层恢复全景轴
});
connect(list_, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* it, int) {
const QString id = it->data(0, kDsIdRole).toString();
if (id.isEmpty()) return;
// 双击=适配相机到该 ds 子树盒 + 联动中下方图表详情页(无图表页类型静默,见 main.cpp T4 接线)。
// 属性弹窗改由右键「详情」触发detailRequested双击不再弹属性框决策6三维体只适配、静默
// 双击首击的 itemClicked 可能已把「本已选中」的该行 toggle 取消(见下方 toggle 逻辑);此处补回选中,
// 使双击终态 = 选中+贴合轴+详情一致(!isSelected 仅在被 toggle 掉时成立,正常双击未选行首击已选中→跳过)。
if (!it->isSelected()) list_->setCurrentItem(it);
emit datasetActivated(id, it->data(0, kDsDdCodeRole).toString(), it->data(0, kDsNameRole).toString());
emit detailRequested(id, it->data(0, kDsDdCodeRole).toString(), it->data(0, kDsNameRole).toString());
});
// 树选中任意数据行 → datasetSelected各类型段通用驱动该 ds 子树贴合坐标轴spec §3.2
// 并由上层 CategoryAnalysisTab 做全列互斥。voxel 段切片/异常的 VTK 高亮由上层据 ddCode 处理,
// 故此信号对所有段发射即可,任意类型选中都能显子树贴合轴(非仅三维体)。
// 空选中(点树空白/清选)→ 发空 datasetSelected上层据此恢复全景轴、清高亮贴合轴取消
connect(list_, &QTreeWidget::itemSelectionChanged, this, [this] {
const auto items = list_->selectedItems();
if (items.isEmpty()) { emit datasetSelected(QString(), QString()); return; }
QTreeWidgetItem* it = items.first();
const QString id = it->data(0, kDsIdRole).toString();
const QString dd = it->data(0, kDsDdCodeRole).toString();
if (!id.isEmpty() && dd != QStringLiteral("container")) emit datasetSelected(id, dd);
});
if (desc_.id == "voxel") { // 仅三维体段提供右键操作菜单(体/切片/异常)
if (spec_.id == "voxel") { // 仅三维体段提供右键操作菜单(体/切片/异常)
list_->setContextMenuPolicy(Qt::CustomContextMenu);
connect(list_, &QTreeWidget::customContextMenuRequested, this, &CategorySection::showContextMenu);
// 树选中切片/异常 → VTK 高亮联动(正向 list→VTK
connect(list_, &QTreeWidget::itemSelectionChanged, this, [this] {
const auto items = list_->selectedItems();
if (items.isEmpty()) return;
QTreeWidgetItem* it = items.first();
const QString id = it->data(0, kDsIdRole).toString();
const QString dd = it->data(0, kDsDdCodeRole).toString();
if (!id.isEmpty() && dd != QStringLiteral("container")) emit datasetSelected(id, dd);
});
}
body->addWidget(list_, 1);
root->addWidget(body_, 1);
connect(header_, &QToolButton::toggled, this, [this, syncHeader](bool on) {
connect(header_, &QToolButton::toggled, this, [this](bool on) {
body_->setVisible(on);
syncHeader(); // ▾(展开)/▸(折叠) 切换
emit collapsedChanged(); // 外层据此把折叠段 stretch 归 0、展开段吸收余量 → 折叠向上收
header_->setArrowType(on ? Qt::DownArrow : Qt::RightArrow);
});
}
bool CategorySection::eventFilter(QObject* obj, QEvent* ev) {
// 段体树 viewport 左键按下:在 QTreeWidget 默认处理改选之前,记录被按行及其「按下瞬间是否已选中」,
// 并清本次「勾选框改动」标记。itemClicked释放据此判定已选中且未点勾选框 → toggle 取消选中。
if (list_ && obj == list_->viewport() && ev->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(ev);
if (me->button() == Qt::LeftButton) {
pressedItem_ = list_->itemAt(me->position().toPoint());
pressedSelected_ = pressedItem_ && pressedItem_->isSelected();
checkToggledThisPress_ = false;
}
}
return QWidget::eventFilter(obj, ev);
}
bool CategorySection::isExpanded() const { return header_ && header_->isChecked(); }
void CategorySection::ensureExpanded() {
if (header_ && !header_->isChecked()) header_->setChecked(true); // toggled→展开段体
}
QTreeWidgetItem* CategorySection::itemFor(const QString& dsId) const {
if (!list_ || dsId.isEmpty()) return nullptr;
for (QTreeWidgetItemIterator it(list_); *it; ++it)
if ((*it)->data(0, kDsIdRole).toString() == dsId) return *it;
return nullptr;
}
QStringList CategorySection::subtreeDsIds(const QString& dsId) const {
QTreeWidgetItem* item = itemFor(dsId);
if (!item) return {};
// 归一到子树根:向上找到最高的非容器祖先(三维体行;其父为结构容器 TM。选中体/切片/异常都收敛到该体。
QTreeWidgetItem* root = item;
for (QTreeWidgetItem* p = root->parent(); p; p = p->parent()) {
if (p->data(0, kDsDdCodeRole).toString() == QStringLiteral("container")) break;
root = p;
}
// 自根向下遍历整棵子树,收集非空 dsId跳过容器骨架节点
QStringList ids;
std::vector<QTreeWidgetItem*> stack{root};
while (!stack.empty()) {
QTreeWidgetItem* it = stack.back();
stack.pop_back();
const QString id = it->data(0, kDsIdRole).toString();
if (!id.isEmpty()) ids << id;
for (int i = 0; i < it->childCount(); ++i) stack.push_back(it->child(i));
}
return ids;
}
bool CategorySection::hasRenderableRows() const {
if (!list_) return false;
// 数据行 = 非 container 的可勾选行;只有容器节点(分组)不算「有数据」。
for (QTreeWidgetItemIterator it(list_); *it; ++it)
if ((*it)->data(0, kDsDdCodeRole).toString() != QStringLiteral("container"))
return true;
return false;
}
void CategorySection::setStructure(const std::vector<geopro::data::StructNode>& nodes) {
structure_ = nodes; // 容器分层(项目根/GS/TM→ds在 Task 12 接入真实结构后据此构建。
}
@ -281,13 +141,6 @@ void CategorySection::selectItem(const QString& dsId) {
list_->setCurrentItem(nullptr); // 空 dsId / 未找到 → 清选中
}
void CategorySection::clearSelection() {
if (!list_) return;
const QSignalBlocker block(list_); // 跨段互斥清选:被清段不回发 datasetSelected(空),避免环路/误恢复全景轴
list_->setCurrentItem(nullptr);
list_->clearSelection();
}
void CategorySection::setChecked(const QString& dsId, bool on) {
for (QTreeWidgetItemIterator it(list_); *it; ++it)
if ((*it)->data(0, kDsIdRole).toString() == dsId &&
@ -297,53 +150,8 @@ void CategorySection::setChecked(const QString& dsId, bool on) {
}
}
void CategorySection::setBusy(const QString& dsId, bool on) {
QTreeWidgetItem* target = nullptr;
for (QTreeWidgetItemIterator it(list_); *it; ++it)
if ((*it)->data(0, kDsIdRole).toString() == dsId) { target = *it; break; }
if (!target) return;
{
// 改 busy/角度角色用 SignalBlocker不触发 itemChanged→emitChecked→重渲染viewport 仍重绘。
const QSignalBlocker block(list_);
target->setData(0, kDsBusyRole, on);
if (on) target->setData(0, Qt::CheckStateRole, Qt::Checked); // 渲染中保持勾选(仍属渲染集)
}
bool anyBusy = false;
for (QTreeWidgetItemIterator it(list_); *it; ++it)
if ((*it)->data(0, kDsBusyRole).toBool()) { anyBusy = true; break; }
if (anyBusy) {
if (!spinTimer_) {
spinTimer_ = new QTimer(this);
spinTimer_->setInterval(80);
connect(spinTimer_, &QTimer::timeout, this, [this]() {
spinAngle_ = (spinAngle_ + 30) % 360;
const QSignalBlocker block(list_);
for (QTreeWidgetItemIterator it(list_); *it; ++it)
if ((*it)->data(0, kDsBusyRole).toBool())
(*it)->setData(0, kDsSpinAngleRole, spinAngle_);
});
}
if (!spinTimer_->isActive()) spinTimer_->start();
} else if (spinTimer_) {
spinTimer_->stop();
}
if (list_->viewport()) list_->viewport()->update();
}
void CategorySection::clearAllBusy() {
const QSignalBlocker block(list_);
bool any = false;
for (QTreeWidgetItemIterator it(list_); *it; ++it)
if ((*it)->data(0, kDsBusyRole).toBool()) {
(*it)->setData(0, kDsBusyRole, false);
any = true;
}
if (spinTimer_) spinTimer_->stop();
if (any && list_->viewport()) list_->viewport()->update();
}
void CategorySection::refreshArrayCombo() {
if (!arrayCombo_) return; // 该段无装置筛选控件desc_.filters 未含 ArrayType→ 不刷
if (!spec_.hasArrayTypeFilter || !arrayCombo_) return;
const QString prev = arrayCombo_->currentData().toString();
const QSignalBlocker block(arrayCombo_);
arrayCombo_->clear();
@ -373,8 +181,8 @@ void CategorySection::refreshArrayCombo() {
}
bool CategorySection::passesFilters(const DsRow& row) const {
// 类型筛选("全部"=空不筛;无 arrayCombo_=该段不筛装置维度):按 ds 自身类型值typeName回退 dsTypeCode命中选中项。
if (arrayCombo_) {
// 类型筛选("全部"=空不筛):按 ds 自身类型值typeName回退 dsTypeCode命中选中项。
if (spec_.hasArrayTypeFilter && arrayCombo_) {
const QString sel = arrayCombo_->currentData().toString();
if (!sel.isEmpty()) {
const QString t = !row.typeName.empty() ? QString::fromStdString(row.typeName)
@ -386,8 +194,7 @@ bool CategorySection::passesFilters(const DsRow& row) const {
const QDate from = dateRange_ ? dateRange_->from() : QDate();
const QDate to = dateRange_ ? dateRange_->to() : QDate();
if (from.isValid() || to.isValid()) {
// 采集时间字段定义按 ds 自身类型取(描述符无段级 dsTypeCode逐行查更准缺定义则回退 createTime
const DsTypeFields* f = dict_ ? dict_->fields(row.dsTypeCode) : nullptr;
const DsTypeFields* f = dict_ ? dict_->fields(spec_.dsTypeCode) : nullptr;
std::string ts = f ? collectTimeOf(row, *f) : std::string();
if (ts.empty()) ts = row.createTime;
const QDate d = QDate::fromString(QString::fromStdString(ts).left(10), QStringLiteral("yyyy-MM-dd"));
@ -500,27 +307,20 @@ void CategorySection::showContextMenu(const QPoint& pos) {
QMenu menu(this);
menu.addAction(QStringLiteral("详情"), this,
[this, id, ddCode, name] { emit detailRequested(id, ddCode, name); });
if (ddCode == QStringLiteral("dd_voxel") || ddCode == QStringLiteral("dd_radar_3d")) { // 三维体
if (ddCode == QStringLiteral("dd_voxel")) { // 三维体
QMenu* sl = menu.addMenu(QStringLiteral("生成切片")); // id=被右键的三维体 dsId切片建到该体上
sl->addAction(QStringLiteral("上下"), this, [this, id] { emit sliceRequested(SliceAxis::UpDown, id); });
sl->addAction(QStringLiteral("前后"), this, [this, id] { emit sliceRequested(SliceAxis::FrontBack, id); });
sl->addAction(QStringLiteral("左右"), this, [this, id] { emit sliceRequested(SliceAxis::LeftRight, id); });
sl->addAction(QStringLiteral("任意"), this, [this, id] { emit sliceRequested(SliceAxis::Oblique, id); });
menu.addAction(QStringLiteral("色阶"), this, [this, id] { emit colorScaleRequested(id); });
if (ddCode == QStringLiteral("dd_radar_3d")) { // 雷达体:显示增益模式切换(纯显示,重建体)
QMenu* gm = menu.addMenu(QStringLiteral("增益模式"));
gm->addAction(QStringLiteral("AGC显深部·找目标"), this,
[this, id] { emit radarGainModeRequested(id, 1); });
gm->addAction(QStringLiteral("保幅 tpow判振幅·复核"), this,
[this, id] { emit radarGainModeRequested(id, 2); });
gm->addAction(QStringLiteral("关(原始振幅)"), this,
[this, id] { emit radarGainModeRequested(id, 0); });
}
} else if (ddCode == QStringLiteral("dd_slice")) { // 切片(列表中均为已保存=定稿锁定,无保存/另存)
menu.addAction(QStringLiteral("色阶…"), this, [this, id] { emit colorScaleRequested(id); });
} else if (ddCode == QStringLiteral("dd_slice")) { // 切片
menu.addAction(QStringLiteral("保存位姿"), this, [this, id] { emit sliceSaveRequested(id); });
menu.addAction(QStringLiteral("另存为…"), this, [this, id] { emit sliceSaveAsRequested(id); });
QMenu* ex = menu.addMenu(QStringLiteral("导出"));
ex->addAction(QStringLiteral("图片"), this, [this, id] { emit sliceExportImageRequested(id); });
ex->addAction(QStringLiteral("dat"), this, [this, id] { emit sliceExportDatRequested(id); });
menu.addAction(QStringLiteral("色阶"), this, [this, id] { emit colorScaleRequested(id); });
menu.addAction(QStringLiteral("色阶…"), this, [this, id] { emit colorScaleRequested(id); });
menu.addSeparator();
menu.addAction(QStringLiteral("删除"), this, [this, id, ddCode] { emit deleteDatasetRequested(id, ddCode); });
} else if (ddCode == QStringLiteral("dd_anomaly")) { // 异常
@ -532,86 +332,4 @@ void CategorySection::showContextMenu(const QPoint& pos) {
menu.exec(list_->viewport()->mapToGlobal(pos));
}
void CategorySection::showPlaneZPopup(QToolButton* host) {
// z 值滑块 popup仿工具条底图滑块拖动整体升降本类型平面含其上全部足迹
// 范围 ±500 米:覆盖常见场景高程量级;滑块值即平面绝对高程(米),首开回显 lastPlaneZ_(无则 0)。
constexpr int kPlaneZRangeM = 500;
QMenu menu(this);
auto* wa = new QWidgetAction(&menu);
auto* box = new QWidget(&menu);
auto* lay = new QVBoxLayout(box);
lay->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kSm);
lay->setSpacing(space::kXs);
auto* lab = new QLabel(box);
auto* sld = new QSlider(Qt::Horizontal, box);
sld->setRange(-kPlaneZRangeM, kPlaneZRangeM);
sld->setValue(static_cast<int>(lastPlaneZ_));
sld->setMinimumWidth(160);
sld->setToolTip(QStringLiteral("平面高程 z"));
auto syncLabel = [lab](int v) { lab->setText(QStringLiteral("平面 z%1 米").arg(v)); };
syncLabel(sld->value());
// 直接平移平面/足迹/底图(改 actor position即时同步→ 无需防抖valueChanged 每步直发 planeZChanged
// 拖动即实时跟随、无移除+异步重载。lastPlaneZ_ 记住终值供重开 popup 回显。
connect(sld, &QSlider::valueChanged, this, [this, syncLabel](int v) {
lastPlaneZ_ = v;
syncLabel(v);
emit planeZChanged(QString::fromStdString(desc_.id), static_cast<double>(v));
});
lay->addWidget(lab);
lay->addWidget(sld);
wa->setDefaultWidget(box);
menu.addAction(wa);
menu.exec(host->mapToGlobal(QPoint(0, host->height())));
}
void CategorySection::showBasemapPopup(QToolButton* host) {
// 底图 popup仿工具条底图控件本类型平面底图 矢量平面(默认)/无 + 透明度滑块(0100默认 50)。
// 类型切换为离散单次事件直发;透明度拖动逐步触发→单发 QTimer(150ms)防抖,停手后一次发射,免抖动重铺瓦片。
QMenu menu(this);
auto* wa = new QWidgetAction(&menu);
auto* box = new QWidget(&menu);
auto* lay = new QVBoxLayout(box);
lay->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kSm);
lay->setSpacing(space::kXs);
auto* kindCombo = new QComboBox(box);
kindCombo->addItem(QStringLiteral("矢量平面")); // index 0
kindCombo->addItem(QStringLiteral("")); // index 1
kindCombo->setCurrentIndex(lastBasemapKind_);
connect(kindCombo, &QComboBox::currentIndexChanged, this, [this](int idx) {
lastBasemapKind_ = idx;
emit basemapKindChanged(QString::fromStdString(desc_.id), idx);
});
auto* lab = new QLabel(box);
auto* sld = new QSlider(Qt::Horizontal, box);
sld->setRange(0, 100);
sld->setValue(lastBasemapOpacity_);
sld->setMinimumWidth(160);
sld->setToolTip(QStringLiteral("底图透明度"));
auto syncLabel = [lab](int v) { lab->setText(QStringLiteral("透明度:%1%").arg(v)); };
syncLabel(sld->value());
if (!basemapOpacityTimer_) { // 定时器 parent=this存活于 modal popup 之外,停手后安全发射终值
basemapOpacityTimer_ = new QTimer(this);
basemapOpacityTimer_->setSingleShot(true);
basemapOpacityTimer_->setInterval(150);
connect(basemapOpacityTimer_, &QTimer::timeout, this, [this]() {
emit basemapOpacityChanged(QString::fromStdString(desc_.id), pendingBasemapOpacity_);
});
}
connect(sld, &QSlider::valueChanged, this, [this, syncLabel](int v) {
lastBasemapOpacity_ = v;
syncLabel(v);
pendingBasemapOpacity_ = v / 100.0;
basemapOpacityTimer_->start(); // 重启防抖窗口:覆盖拖动、键盘、点轨——停手后一次性发射
});
lay->addWidget(kindCombo);
lay->addWidget(lab);
lay->addWidget(sld);
wa->setDefaultWidget(box);
menu.addAction(wa);
menu.exec(host->mapToGlobal(QPoint(0, host->height())));
}
} // namespace geopro::app

View File

@ -3,16 +3,14 @@
#include <QWidget>
#include <vector>
#include "interact/SlicePlaneMath.hpp" // geopro::render::interact::SliceAxis
#include "repo/CategoryDescriptor.hpp" // geopro::data::CategoryDescriptor / OpKind / FilterKind
#include "repo/CategoryConfig.hpp"
#include "repo/RepoTypes.hpp"
class QTreeWidget;
class QTreeWidgetItem;
class QComboBox;
class QDateEdit;
class QLabel;
class QToolButton;
class QTimer;
class QWidget;
namespace geopro::data {
@ -22,58 +20,32 @@ class DatasetFieldDictionary;
namespace geopro::app {
class DateRangeEdit;
class SectionIconBar;
// 单个数据类型大类段spec §7段头标题/折叠 + 段头图标条)+ 段体(折叠筛选行 + 可勾选数据树)。
// 段头图标条由 descriptor.operations(OpKind) 驱动;筛选行由 descriptor.filters(FilterKind) 驱动、默认折叠。
// 勾选数据行 = 渲染(帘面/体素/切片/二维面GenerateVolume 图标据当前勾选源发 generateVolumeRequested。
// 单个数据类型大类段spec §7段头标题/折叠 + 装置类型/日期筛选 + 「+新增三维体」)+ 段体(可勾选数据树)。
// 勾选数据行 = 渲染(帘面/体素/切片);段头生成按钮据当前勾选源发 generateVolumeRequested。
class CategorySection : public QWidget {
Q_OBJECT
public:
CategorySection(const geopro::data::CategoryDescriptor& desc,
geopro::data::DatasetFieldDictionary* dict, QWidget* parent = nullptr);
CategorySection(const CategorySpec& spec, geopro::data::DatasetFieldDictionary* dict,
QWidget* parent = nullptr);
// 对象树同源的扁平 GS/TM 节点段体容器分层用Task 12 接入真实结构,当前仅存储)。
void setStructure(const std::vector<geopro::data::StructNode>& nodes);
void setDatasets(const std::vector<geopro::data::DsRow>& rows);
void setChecked(const QString& dsId, bool on); // 按 dsId 勾选/取消(新建切片自动勾选等场景)
// 渲染中:该行复选框替换为等待 spinnerbusy=true/复原false。busy 期间保持勾选、动画由定时器驱动。
void setBusy(const QString& dsId, bool busy);
void clearAllBusy(); // 撤回本段所有 spinner失败兜底
void selectItem(const QString& dsId); // 程序化选中某行(VTK→list 反向联动);空/未找到=清选中
void clearSelection(); // 清本段树选中(信号阻断,不回发 datasetSelected);供跨段全列互斥用
QStringList checkedIds() const { return checkedDsIds(); } // 当前勾选 ds异常显隐同步用
void refreshArrayFilter() { refreshArrayCombo(); } // 装置枚举异步加载后重填下拉
bool isExpanded() const; // 段头展开态(供外层按折叠状态重分配 stretch实现"折叠向上收"
void ensureExpanded(); // 展开本段(折叠则展开):滚动定位前确保目标行可见
QTreeWidget* listWidget() const { return list_; } // 段体树(外层滚动定位用)
QTreeWidgetItem* itemFor(const QString& dsId) const; // 按 dsId 取行(无则 nullptr
// 该 ds 所在层级子树的全部 dsId贴合坐标轴子树盒spec §2/§3.2 决策 3先归一到子树根
// (向上找最高非容器祖先=三维体行),再自根向下收集整棵子树(体+切片+异常)的非空 dsId。
// 故选中体/切片/异常都归到同一个「该体子树」盒。dsId 不在本段则返回空。
QStringList subtreeDsIds(const QString& dsId) const;
bool hasRenderableRows() const; // 段体是否含可渲染数据行(非 container 容器节点),供单列动态显隐
protected:
// 段体树 viewport 事件过滤:在默认处理改选之前记录「按下瞬间该行是否已选中」,供单击已选行的 toggle 取消判定。
bool eventFilter(QObject* obj, QEvent* ev) override;
const CategorySpec& spec() const { return spec_; }
signals:
void checkedDatasetsChanged(const QStringList& dsIds); // 数据行勾选=渲染
void collapsedChanged(); // 折叠/展开切换 → 外层 CategoryAnalysisTab 重排各段 stretch
void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); // 段头「+新增三维体」(接收方按 sourceIds 解析类型)
void planeZChanged(const QString& typeId, double z); // PlaneZ 滑块:整体升降该 2D 类型平面z=绝对高程,米)
void basemapKindChanged(const QString& typeId, int kind); // 底图弹窗:矢量平面(0)/无(1)
void basemapOpacityChanged(const QString& typeId, double o); // 底图弹窗透明度滑块[0,1](防抖发射)
void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 右键「详情」=属性弹窗
// 双击某行决策6/T4适配相机到该 ds 子树空间范围 + 联动中下方详情面板(无详情页类型静默)。
// 与 detailRequested右键属性弹窗分开使双击「只适配 + 图表联动」,三维体等无图表页时静默。
void datasetActivated(const QString& dsId, const QString& ddCode, const QString& name);
void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); // 段头「+新增三维体」
void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); // 双击/右键=详情
void deleteDatasetRequested(const QString& dsId, const QString& ddCode); // 右键删除(切片/异常)
// ── 三维体段右键操作(迁自旧 Column3DAnalysis全接──
void sliceRequested(geopro::render::interact::SliceAxis axis, const QString& volumeDsId); // 体→生成切片(轴+目标体)
void colorScaleRequested(const QString& dsId); // 体/切片→色阶
void radarGainModeRequested(const QString& dsId, int mode); // 雷达体→显示增益模式(0关/1AGC/2保幅tpow)
void sliceSaveRequested(const QString& dsId); // 切片→保存位姿
void sliceSaveAsRequested(const QString& dsId); // 切片→另存
void sliceExportImageRequested(const QString& dsId); // 切片→导出图片
@ -83,37 +55,22 @@ signals:
private:
void showContextMenu(const QPoint& pos); // 段体树右键菜单(详情 + 删除)
void showPlaneZPopup(QToolButton* host); // PlaneZ 图标:弹 z 值滑块 popup → planeZChanged
void showBasemapPopup(QToolButton* host); // Basemap 图标:弹 矢量平面/无 + 透明度滑块 popup
void rebuildList(); // 据 rows_经装置/日期筛选)重建段体树并复原勾选
void refreshArrayCombo(); // 据当前 rows_ 重填装置类型下拉项(经字典 value→中文
void emitChecked(); // 收集勾选 → checkedDatasetsChanged
QStringList checkedDsIds() const;
bool passesFilters(const geopro::data::DsRow& row) const; // 装置类型 + 采集时间范围
geopro::data::CategoryDescriptor desc_;
CategorySpec spec_;
geopro::data::DatasetFieldDictionary* dict_ = nullptr;
std::vector<geopro::data::DsRow> rows_;
std::vector<geopro::data::StructNode> structure_;
QToolButton* header_ = nullptr; // 折叠头(标题 + 箭头)
SectionIconBar* iconBar_ = nullptr; // 段头响应式图标条(由 desc_.operations 装配)
QWidget* body_ = nullptr; // 段体容器(折叠时隐藏)
QWidget* filterRow_ = nullptr; // 筛选行容器(默认折叠,由 Filter 图标 toggle
QComboBox* arrayCombo_ = nullptr; // 装置类型筛选(仅 desc_.filters 含 ArrayType
QComboBox* arrayCombo_ = nullptr; // 装置类型筛选(仅 hasArrayTypeFilter
DateRangeEdit* dateRange_ = nullptr; // 采集时间范围筛选(起止双日历,可清空)
QTreeWidget* list_ = nullptr;
QTimer* spinTimer_ = nullptr; // 驱动 busy 行 spinner 旋转(有 busy 行时运行)
int spinAngle_ = 0; // 当前 spinner 角度(度)
double lastPlaneZ_ = 0.0; // 上次 z 值滑块设定的平面高程(重开 popup 时回显,无则 0直接平移故无防抖
int lastBasemapKind_ = 0; // 上次底图选择0=矢量平面/1=无),重开 popup 时回显
int lastBasemapOpacity_ = 50; // 上次底图透明度0100重开 popup 回显;默认 50
QTimer* basemapOpacityTimer_ = nullptr; // 底图透明度滑块发射防抖(透明度改动会触发瓦片重铺,仍需防抖)
double pendingBasemapOpacity_ = 0.5; // 防抖待发的底图透明度[0,1](定时器到点发射 basemapOpacityChanged
// 单击已选行取消选中toggle-offeventFilter 于按下瞬间(默认改选前)记录被按行及其选中态itemClicked 据此取消。
QTreeWidgetItem* pressedItem_ = nullptr; // 本次左键按下的行itemAt(press),与 itemClicked 的行比对)
bool pressedSelected_ = false; // 按下瞬间该行是否已选中true 且非勾选框点击时,单击取消选中)
bool checkToggledThisPress_ = false; // 本次按下是否切换了勾选框(点勾选框只改渲染,不取消选中)
};
} // namespace geopro::app

View File

@ -0,0 +1,101 @@
#include "panels/columns/Column2DDataset.hpp"
#include <QComboBox>
#include "EmptyAwareComboBox.hpp"
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QSignalBlocker>
#include <QTreeWidget>
#include <QTreeWidgetItemIterator>
#include <QVBoxLayout>
#include "Theme.hpp"
#include "panels/DatasetListPanel.hpp"
namespace geopro::app {
Column2DDataset::Column2DDataset(QWidget* parent) : QWidget(parent) {
auto* root = new QVBoxLayout(this);
root->setContentsMargins(space::kMd, space::kMd, space::kMd, space::kMd);
root->setSpacing(space::kMd);
// 地图
{
auto* form = new QFormLayout();
auto* basemap = new EmptyAwareComboBox();
basemap->addItem(QStringLiteral("天地图"));
basemap->addItem(QStringLiteral("Google Map"));
basemap->addItem(QStringLiteral("隐藏"));
basemap->setCurrentIndex(0); // 默认天地图:数据重锚后由 onFrameReanchored 在数据位置加载
connect(basemap, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this](int index) { emit basemapChanged(index); });
form->addRow(QStringLiteral("底图源"), basemap);
root->addWidget(new QLabel(QStringLiteral("地图")));
root->addLayout(form);
}
// 2D视图
{
auto* form = new QFormLayout();
auto* view2d = new EmptyAwareComboBox();
view2d->addItem(QStringLiteral("关闭"));
view2d->addItem(QStringLiteral("Z=0"));
view2d->addItem(QStringLiteral("顶部"));
view2d->addItem(QStringLiteral("底部"));
view2d->addItem(QStringLiteral("自定义"));
view2d->setCurrentIndex(1);
auto* zSpin = new QDoubleSpinBox();
zSpin->setRange(-1000000, 1000000);
zSpin->setSuffix(QStringLiteral(" m"));
zSpin->setValue(0);
connect(view2d, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this, form, zSpin](int idx) {
form->setRowVisible(zSpin, idx == 4); // 整行隐藏(含"Z 值"标签),非自定义时不留孤标签
emit view2DModeChanged(idx);
});
connect(zSpin, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
[this](double z) { emit customZChanged(z); });
form->addRow(QStringLiteral("位置"), view2d);
form->addRow(QStringLiteral("Z 值"), zSpin);
form->setRowVisible(zSpin, false); // 默认非自定义→隐藏整行
root->addWidget(new QLabel(QStringLiteral("2D视图")));
root->addLayout(form);
}
// 数据集列表(可勾选)
list_ = new QTreeWidget();
list_->setHeaderHidden(true);
list_->setRootIsDecorated(true);
applyDatasetCardDelegate(list_);
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) {
QStringList ids;
for (QTreeWidgetItemIterator it(list_); *it; ++it) {
if ((*it)->checkState(0) == Qt::Checked)
ids << (*it)->data(0, kDsIdRole).toString();
}
emit checkedDatasetsChanged(ids);
});
root->addWidget(list_, 1);
}
void Column2DDataset::setDatasets(const std::vector<geopro::data::DsRow>& rows) {
{
QSignalBlocker blocker(list_);
populateDatasetList(list_, rows, /*append=*/false);
for (QTreeWidgetItemIterator it(list_); *it; ++it) {
(*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable);
(*it)->setCheckState(0, Qt::Unchecked);
}
} // blocker released here
// 填充后统一发一次(新载入必为空选):清掉上一次的渲染勾选
QStringList ids;
for (QTreeWidgetItemIterator it(list_); *it; ++it)
if ((*it)->checkState(0) == Qt::Checked)
ids << (*it)->data(0, kDsIdRole).toString();
emit checkedDatasetsChanged(ids);
}
} // namespace geopro::app

View File

@ -0,0 +1,28 @@
#pragma once
#include <QWidget>
#include <QStringList>
#include <vector>
#include "repo/RepoTypes.hpp"
class QTreeWidget;
namespace geopro::app {
// 二维数据集栏:地图 + 2D视图(含自定义 Z) + 2D 数据集列表。
class Column2DDataset : public QWidget {
Q_OBJECT
public:
explicit Column2DDataset(QWidget* parent = nullptr);
void setDatasets(const std::vector<geopro::data::DsRow>& rows);
signals:
void basemapChanged(int index); // 0 天地图 / 1 Google / 2 隐藏
void view2DModeChanged(int index); // 0 关闭 /1 Z=0 /2 顶部 /3 底部 /4 自定义
void customZChanged(double z); // 世界绝对高程(米),向上为正
void checkedDatasetsChanged(const QStringList& dsIds);
private:
QTreeWidget* list_ = nullptr;
};
} // namespace geopro::app

View File

@ -1,19 +1,30 @@
#include "panels/columns/ColumnDrawer.hpp"
#include "panels/columns/Column2DDataset.hpp"
#include "panels/columns/CategoryAnalysisTab.hpp"
#include <algorithm>
#include "Glyphs.hpp"
#include "Theme.hpp"
#include <QHBoxLayout>
#include <QPushButton>
#include <QResizeEvent>
#include <QTabBar>
#include <QTabWidget>
namespace geopro::app {
ColumnDrawer::ColumnDrawer(QWidget* parent, geopro::data::DatasetFieldDictionary* dict)
: QWidget(parent)
{
// 单列承载:去 QTabWidgetbody_ 直接为 CategoryAnalysisTab含 trajectory 段,二维并入同列)。
col2D_ = new Column2DDataset(this);
analysisTab_ = new CategoryAnalysisTab(dict, this);
body_ = analysisTab_;
// Tab 容器body_两 tab三维分析[分段] / 二维分析)。
auto* tabs = new QTabWidget(this);
body_ = tabs;
tabs->addTab(analysisTab_, QStringLiteral("三维分析"));
tabs->addTab(col2D_, QStringLiteral("二维分析"));
tabs->tabBar()->setUsesScrollButtons(false); // 永不出左右滚动箭头(两 tab 必能平铺)
// 折叠按钮:固定宽 18px垂直拉伸。
// 用 SVG 图标(makeGlyph)而非 ◀/▶ 文字——三角符(U+25C0/25B6)不在 YaHei作按钮文字会触发
@ -43,6 +54,22 @@ ColumnDrawer::ColumnDrawer(QWidget* parent, geopro::data::DatasetFieldDictionary
setMaximumWidth(560);
}
void ColumnDrawer::resizeEvent(QResizeEvent* e)
{
QWidget::resizeEvent(e);
// 两 tab 平分抽屉宽度填满(带样式表的 tab 不响应 setExpanding须按 barWidth/n 显式给宽)。
// 消除旧 3 栏布局遗留的右侧空白——重构成 2 栏后不再三分、留空第三位。
if (auto* tabs = qobject_cast<QTabWidget*>(body_)) {
const int n = tabs->count();
if (n > 0 && tabs->width() > 0) {
// 每 tab 内容宽 = 总宽/n - 每 tab 非内容开销(全局 QSS padding 8+16+16=… 约 32 + margin 4)。
// 稍欠一点宽避免溢出(溢出会触发滚动箭头)setUsesScrollButtons(false) 再兜底。
const int w = std::max(40, tabs->width() / n - 42);
tabs->tabBar()->setStyleSheet(QStringLiteral("QTabBar::tab{width:%1px;}").arg(w));
}
}
}
void ColumnDrawer::toggleCollapsed()
{
collapsed_ = !collapsed_;
@ -52,7 +79,6 @@ void ColumnDrawer::toggleCollapsed()
// 折叠后只保留按钮宽度;展开恢复可调范围
setMinimumWidth(collapsed_ ? 0 : 180);
setMaximumWidth(collapsed_ ? 18 : 560);
emit collapsedChanged(collapsed_); // 通知上层调 QSplitter 尺寸,回收/恢复栏宽(防残留空白)
}
void ColumnDrawer::expand()

View File

@ -2,6 +2,7 @@
#include <QWidget>
class QPushButton;
class QResizeEvent;
namespace geopro::data {
class DatasetFieldDictionary;
@ -9,29 +10,30 @@ class DatasetFieldDictionary;
namespace geopro::app {
class Column2DDataset;
class CategoryAnalysisTab;
// VTK视图左侧内嵌抽屉单列承载 CategoryAnalysisTab(按数据类型分段,含 trajectory 段) + 折叠开关。
// B2 去 tab原「三维分析 / 二维分析」双 tab 合一,二维(足迹/轨迹)经描述符分段并入同一列。
// VTK视图左侧内嵌抽屉两 tab(三维分析[按数据类型分段]/二维分析) + 折叠开关。
class ColumnDrawer : public QWidget {
Q_OBJECT
public:
explicit ColumnDrawer(QWidget* parent = nullptr,
geopro::data::DatasetFieldDictionary* dict = nullptr);
Column2DDataset* col2D() const { return col2D_; }
CategoryAnalysisTab* analysisTab() const { return analysisTab_; }
signals:
// 折叠/展开切换:上层据此调整 QSplitter 尺寸,回收/恢复抽屉栏宽(否则收起残留空白区)。
void collapsedChanged(bool collapsed);
public slots:
void toggleCollapsed();
void expand(); // 强制展开(进入全屏时确保单列可见)
void expand(); // 强制展开(进入全屏时确保三栏可见)
protected:
void resizeEvent(QResizeEvent* e) override; // 两 tab 按抽屉宽平分(消除右侧空白"第三栏位"
private:
Column2DDataset* col2D_ = nullptr;
CategoryAnalysisTab* analysisTab_ = nullptr;
QWidget* body_ = nullptr; // = analysisTab_折叠时隐藏
QWidget* body_ = nullptr; // QTabWidget折叠时隐藏
QPushButton* toggleBtn_ = nullptr;
bool collapsed_ = false;
};

View File

@ -1,159 +0,0 @@
#include "panels/columns/SectionIconBar.hpp"
#include <QAction>
#include <QHBoxLayout>
#include <QMenu>
#include <QResizeEvent>
#include <QSize>
#include <QSizePolicy>
#include <QToolButton>
#include <algorithm>
#include "Glyphs.hpp"
#include "Theme.hpp"
namespace geopro::app {
int visibleIconCount(int totalIcons, int availablePx, int iconPx, int overflowPx, int maxIcons) {
if (totalIcons <= 0 || iconPx <= 0) return 0;
const int ideal = std::min(totalIcons, std::max(0, maxIcons));
const bool overflowFromCap = totalIcons > ideal; // 超 max → 必有溢出位
// 先看理想数能否放下(若已因 cap 溢出,理想数也要含溢出位)
auto fits = [&](int n, bool withOverflow) {
return n * iconPx + (withOverflow ? overflowPx : 0) <= availablePx;
};
int n = ideal;
bool overflow = overflowFromCap;
while (n > 0 && !fits(n, overflow || (totalIcons > n))) {
--n;
overflow = true; // 一旦减少必有「…」
}
if (n < 0) n = 0;
return n;
}
namespace {
constexpr int kGlyphPx = 18; // glyph 绘制像素;按钮本体宽用 iconPx_与溢出计算口径一致
// glyphKey 字符串 → Glyph 枚举C2 按段头操作填键;未知键回退中性图标)。
Glyph glyphFromKey(const QString& key) {
const QString k = key.toLower();
if (k == QStringLiteral("plus")) return Glyph::Plus;
if (k == QStringLiteral("filter")) return Glyph::Filter;
if (k == QStringLiteral("upload")) return Glyph::Upload;
if (k == QStringLiteral("download")) return Glyph::Download;
if (k == QStringLiteral("collapse")) return Glyph::Collapse;
if (k == QStringLiteral("fullscreen")) return Glyph::Fullscreen;
if (k == QStringLiteral("map")) return Glyph::Map;
if (k == QStringLiteral("detail")) return Glyph::Detail;
if (k == QStringLiteral("property")) return Glyph::Property;
if (k == QStringLiteral("gear")) return Glyph::Gear;
return Glyph::Property;
}
} // namespace
SectionIconBar::SectionIconBar(QWidget* parent) : QWidget(parent) {
auto* lay = new QHBoxLayout(this);
lay->setContentsMargins(0, 0, 0, 0);
lay->setSpacing(0);
// 水平 Preferred优先取 sizeHint(全图标宽),列变窄受压时可缩向 minimumSizeHint(仅「…」)
// 垂直 Fixed高度恒为图标按钮高。不可用 Fixed-Fixed(永不折叠)或 Ignored(丢 sizeHint)。
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
}
QSize SectionIconBar::sizeHint() const {
// 默认上限内的图标全显所需宽度min(操作数, maxIcons_) 个按钮,各占 iconPx_。
const int count = std::min(static_cast<int>(actions_.size()), std::max(0, maxIcons_));
return QSize(count * iconPx_, iconPx_);
}
QSize SectionIconBar::minimumSizeHint() const {
// 最窄也要放下「…」溢出按钮,使本条可缩到仅剩「…」。
return QSize(overflowPx_, iconPx_);
}
void SectionIconBar::setActions(const std::vector<IconAction>& a) {
// 清旧按钮与溢出按钮
for (auto* b : btns_) {
if (b) b->deleteLater();
}
btns_.clear();
if (overflowBtn_) {
overflowBtn_->deleteLater();
overflowBtn_ = nullptr;
}
actions_ = a;
auto* lay = qobject_cast<QHBoxLayout*>(layout());
const QColor ic = tokenColor("text/secondary");
for (const IconAction& act : actions_) {
auto* b = new QToolButton(this);
b->setIcon(makeGlyph(glyphFromKey(act.glyphKey), ic, kGlyphPx));
b->setIconSize(QSize(kGlyphPx, kGlyphPx));
b->setAutoRaise(true);
b->setToolTip(act.tooltip);
b->setFixedSize(iconPx_, iconPx_);
if (act.popupBuilder) {
const auto builder = act.popupBuilder;
QToolButton* host = b;
connect(b, &QToolButton::clicked, this, [builder, host] { builder(host); });
} else if (act.onClick) {
const auto cb = act.onClick;
connect(b, &QToolButton::clicked, this, [cb] { cb(); });
}
if (lay) lay->addWidget(b);
btns_.push_back(b);
}
// 末尾「…」溢出按钮:即点即弹菜单
overflowBtn_ = new QToolButton(this);
overflowBtn_->setText(QStringLiteral(""));
overflowBtn_->setAutoRaise(true);
overflowBtn_->setToolTip(QStringLiteral("更多"));
overflowBtn_->setFixedSize(overflowPx_, iconPx_);
overflowBtn_->setPopupMode(QToolButton::InstantPopup);
overflowBtn_->setMenu(new QMenu(overflowBtn_));
if (lay) lay->addWidget(overflowBtn_);
updateGeometry(); // 操作数变化 → 通知父布局重新按新 sizeHint 分配宽度
relayout();
}
void SectionIconBar::resizeEvent(QResizeEvent* e) {
QWidget::resizeEvent(e);
relayout();
}
void SectionIconBar::relayout() {
const int total = static_cast<int>(actions_.size());
const int vis = visibleIconCount(total, width(), iconPx_, overflowPx_, maxIcons_);
QMenu* menu = overflowBtn_ ? overflowBtn_->menu() : nullptr;
if (menu) menu->clear();
for (int i = 0; i < total; ++i) {
QToolButton* b = btns_[static_cast<size_t>(i)];
if (!b) continue;
if (i < vis) {
b->setVisible(true);
continue;
}
b->setVisible(false);
if (!menu) continue;
const IconAction& act = actions_[static_cast<size_t>(i)];
QAction* ma = menu->addAction(act.tooltip);
if (act.popupBuilder) {
const auto builder = act.popupBuilder;
QToolButton* host = overflowBtn_;
connect(ma, &QAction::triggered, this, [builder, host] { builder(host); });
} else if (act.onClick) {
const auto cb = act.onClick;
connect(ma, &QAction::triggered, this, [cb] { cb(); });
}
}
if (overflowBtn_) overflowBtn_->setVisible(vis < total);
}
} // namespace geopro::app

View File

@ -1,47 +0,0 @@
#pragma once
#include <QWidget>
#include <QSize>
#include <QString>
#include <functional>
#include <vector>
class QToolButton;
class QResizeEvent;
namespace geopro::app {
// 纯逻辑:给定约束返回可见图标数(其余收进「…」)。见 spec §6。
int visibleIconCount(int totalIcons, int availablePx, int iconPx, int overflowPx, int maxIcons);
// 段头响应式图标工具条≤maxIcons 且宽度够则全显;否则右侧依次收进末尾「…」下拉。
struct IconAction {
QString glyphKey; // 图标键(映射到 Glyph
QString tooltip;
std::function<void()> onClick; // 直接动作;为空则用 popupBuilder
std::function<void(QToolButton*)> popupBuilder; // 弹 popupz值/底图/筛选用)
};
class SectionIconBar : public QWidget {
Q_OBJECT
public:
explicit SectionIconBar(QWidget* parent = nullptr);
void setActions(const std::vector<IconAction>& actions); // 重建按钮
void setMaxIcons(int n) { maxIcons_ = n; updateGeometry(); relayout(); }
// sizeHint声明放下「默认上限内全部图标」所需宽度使段头 HBox 分给本条真实宽度
// (否则 relayout 在 width()=0 时折叠全部图标→内层布局尺寸塌缩成只剩「…」→恒折叠,见 spec §6
QSize sizeHint() const override;
// minimumSizeHint至少容下「…」溢出按钮列足够窄时本条可缩到仅剩「…」。
QSize minimumSizeHint() const override;
protected:
void resizeEvent(QResizeEvent* e) override;
private:
void relayout(); // 按当前宽度算可见数,多余进「…」菜单
std::vector<IconAction> actions_;
std::vector<QToolButton*> btns_;
QToolButton* overflowBtn_ = nullptr;
int maxIcons_ = 3;
int iconPx_ = 30;
int overflowPx_ = 30;
};
} // namespace geopro::app

View File

@ -1,63 +0,0 @@
#include "panels/web/ProjectWebView.hpp"
#include <QUrl>
#include <QVBoxLayout>
#include <QWebEnginePage>
#include <QWebEngineScript>
#include <QWebEngineScriptCollection>
#include <QWebEngineView>
namespace geopro::app {
namespace {
// 把字符串转成安全的 JS 字面量(带引号、转义),用于拼进注入脚本。
QString jsStringLiteral(const QString& s) {
// QJsonValue::toJson 不直接给单值字符串手工转义足够token 仅含 base64/空格)。
QString out;
out.reserve(s.size() + 2);
out += QLatin1Char('"');
for (const QChar c : s) {
switch (c.unicode()) {
case '\\': out += QStringLiteral("\\\\"); break;
case '"': out += QStringLiteral("\\\""); break;
case '\n': out += QStringLiteral("\\n"); break;
case '\r': out += QStringLiteral("\\r"); break;
case '\t': out += QStringLiteral("\\t"); break;
default: out += c; break;
}
}
out += QLatin1Char('"');
return out;
}
} // namespace
ProjectWebView::ProjectWebView(const QString& token, QWidget* parent) : QWidget(parent) {
auto* lay = new QVBoxLayout(this);
lay->setContentsMargins(0, 0, 0, 0);
lay->setSpacing(0);
view_ = new QWebEngineView(this);
lay->addWidget(view_, 1);
// token 注入DocumentCreation 阶段把登录 token 写入 localStorage["token"]
// 早于嵌入页 SPA 启动脚本,保证其读取鉴权时已就绪。每次 load 都会重新执行。
if (!token.isEmpty()) {
QWebEngineScript script;
script.setName(QStringLiteral("inject-geopro-token"));
script.setInjectionPoint(QWebEngineScript::DocumentCreation);
script.setWorldId(QWebEngineScript::MainWorld);
script.setRunsOnSubFrames(true);
script.setSourceCode(
QStringLiteral("try{localStorage.setItem('token', %1);}catch(e){}")
.arg(jsStringLiteral(token)));
view_->page()->scripts().insert(script);
}
}
void ProjectWebView::load(const QString& url) {
view_->load(QUrl(url));
}
} // namespace geopro::app

View File

@ -1,25 +0,0 @@
#pragma once
#include <QString>
#include <QWidget>
class QWebEngineView;
namespace geopro::app {
// 项目管理 webview 宿主:内嵌 QWebEngineView承载需「直接嵌入」的 web 管理页
// (在线监测 / 工具组件 / 批量导出 / 告警管理)。
// 构造期注入 DocumentCreation 脚本,把登录 token 写入页面 localStorage["token"]
// 早于页面自身脚本执行,确保 web 端读取鉴权时已就绪。
class ProjectWebView : public QWidget {
Q_OBJECT
public:
explicit ProjectWebView(const QString& token, QWidget* parent = nullptr);
// 加载嵌入页(完整 URL含 #/embed?space=..&projectId=..&target=..)。
void load(const QString& url);
private:
QWebEngineView* view_ = nullptr;
};
} // namespace geopro::app

View File

@ -2,8 +2,6 @@ find_package(Qt6 COMPONENTS Core REQUIRED)
add_library(geopro_controller STATIC
WorkbenchNavController.cpp
DatasetDetailController.cpp
DatasetViewState.cpp
DatasetRenderStrategy.cpp
VtkSceneController.cpp)
target_include_directories(geopro_controller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(geopro_controller PUBLIC geopro_data Qt6::Core)

View File

@ -31,10 +31,6 @@ public slots:
void loadTabPaged(const QString& dsId, const QString& ddCode, int tabIndex, int pageNo,
int pageSize);
void focusDataset(const QString& dsId);
public:
// 该 ddCode 是否有已注册的详情页策略(=有中下方图表详情页)。供联动入口 gate
// 无策略的类型(如 dd_voxel/dd_radar_3d 三维体)不走 openDataset避免 loadFailed 的状态栏提示,静默。
bool supports(const QString& ddCode) const { return registry_.supports(ddCode.toStdString()); }
signals:
void datasetOpened(const QString& dsId, const QString& ddCode, const QString& dsName,
const QString& tmObjectId, const std::vector<controller::TabSpec>& tabs);

View File

@ -1,98 +0,0 @@
#include "controller/DatasetRenderStrategy.hpp"
#include <string>
#include <vector>
#include "controller/VtkSceneController.hpp" // 完整类型 + 含 I3dSceneViewview_.removeDataset
namespace geopro::controller {
namespace {
constexpr double kDefaultBasemapOpacity = 0.5; // 平面底图默认半透明(地下足迹可透过看)
}
// 各策略委托回控制器既有渲染路径(友元访问其私有 addDatasetAsync/add2DDatasetAsync/view_
// 增量 gen 沿用控制器当前 rebuildGeneration_不自增与并发增量互不作废
void VolumeRenderStrategy::add(const std::string& /*typeId*/, const std::string& dsId) {
ctrl_.addDatasetAsync(dsId, ctrl_.rebuildGeneration_);
}
void VolumeRenderStrategy::remove(const std::string& dsId) { ctrl_.view_.removeDataset(dsId); }
void CurtainRenderStrategy::add(const std::string& /*typeId*/, const std::string& dsId) {
ctrl_.addDatasetAsync(dsId, ctrl_.rebuildGeneration_);
}
void CurtainRenderStrategy::remove(const std::string& dsId) { ctrl_.view_.removeDataset(dsId); }
void Plane2DRenderStrategy::add(const std::string& typeId, const std::string& dsId) {
// 该类型平面 z首勾以 dsZ(场景地表基准 zRefElev)定平面,后续勾选返回既有平面 z(投影)。
const double dsZ = ctrl_.view_.zRefElev();
const double z = planeReg_.onChecked(typeId, dsId, dsZ);
dsToType_[dsId] = typeId; // remove 仅得 dsId记下归属类型以回收平面成员
ctrl_.add2DDatasetAsync(dsId, ctrl_.rebuildGeneration_, z);
}
void Plane2DRenderStrategy::remove(const std::string& dsId) {
ctrl_.view_.removeDataset(dsId);
auto it = dsToType_.find(dsId);
if (it == dsToType_.end()) return;
planeReg_.onUnchecked(it->second, dsId); // 该类型成员集空时平面自动消失
dsToType_.erase(it);
}
// 该类型首勾(控制器活跃计数 0→1先于 add 触发):建该类型平面矢量底图。
// 注意 onTypeActivated 在 add 之前触发 → 此刻 planeReg_ 尚无该平面createBasemap 以 zRefElev() 兜底
// (即首勾 add 即将写入的平面 z二者一致)。kind/opacity 复位为默认(矢量平面/0.5)。
void Plane2DRenderStrategy::onTypeActivated(const std::string& typeId) {
bmKind_[typeId] = 0;
bmOpacity_[typeId] = kDefaultBasemapOpacity;
createBasemap(typeId);
}
// 该类型全消(控制器活跃计数 1→0):销毁该类型底图(析构移除瓦片→底图随平面一并消失),遗忘其 kind/opacity。
void Plane2DRenderStrategy::onTypeDeactivated(const std::string& typeId) {
bms_.erase(typeId);
bmKind_.erase(typeId);
bmOpacity_.erase(typeId);
}
void Plane2DRenderStrategy::setPlaneZ(const std::string& typeId, double z) {
planeReg_.setPlaneZ(typeId, z); // 平面 z 真源更新(类型不存在则无操作)
// 直接平移该类型全部已勾选足迹:只改足迹 actor 的 position无移除+异步重载)→ 拖滑块即时跟随、无闪烁。
std::vector<std::string> dsIds;
for (const auto& [dsId, t] : dsToType_)
if (t == typeId) dsIds.push_back(dsId);
if (!dsIds.empty()) ctrl_.view_.setMapLinesZ(dsIds, z);
// 底图同步平移:直接改瓦片 position无销毁+重建、无重下载)→ 底图与足迹一同实时跟随滑块。
auto it = bms_.find(typeId);
if (it != bms_.end()) it->second->setGroundZ(z);
ctrl_.view_.renderIncremental();
}
void Plane2DRenderStrategy::setBasemapKind(const std::string& typeId, int kind) {
bmKind_[typeId] = kind;
auto it = bms_.find(typeId);
if (it == bms_.end()) return;
if (kind == 0) it->second->show(0); // 矢量平面
else it->second->hide(); // 无(保留实例,仅隐瓦片)
}
void Plane2DRenderStrategy::setBasemapOpacity(const std::string& typeId, double o) {
bmOpacity_[typeId] = o;
auto it = bms_.find(typeId);
if (it != bms_.end()) it->second->setOpacity(o);
}
void Plane2DRenderStrategy::createBasemap(const std::string& typeId) {
if (!basemapFactory_) return; // 工厂未注入(如纯逻辑单测,无 VTK) → 不建底图
// 平面 z已建平面取 planeReg_(setPlaneZ 重建走此)onTypeActivated 时平面尚未建 → 以 zRefElev() 兜底
// (= add 即将写入的首勾平面 z二者一致)。
const double gz =
planeReg_.hasPlane(typeId) ? planeReg_.planeZ(typeId) : ctrl_.view_.zRefElev();
auto bm = basemapFactory_(gz);
if (!bm) return;
const int kind = bmKind_.count(typeId) ? bmKind_[typeId] : 0;
const double op = bmOpacity_.count(typeId) ? bmOpacity_[typeId] : kDefaultBasemapOpacity;
bm->setOpacity(op); // 先定透明度,再 show 套用到初次铺瓦
if (kind == 0) bm->show(0); // 默认矢量平面kind=1(无)则仅留实例
bms_[typeId] = std::move(bm); // 替换旧实例 → 旧底图适配器析构移除其瓦片
}
} // namespace geopro::controller

View File

@ -1,100 +0,0 @@
#pragma once
#include <functional>
#include <map>
#include <memory>
#include <string>
#include "controller/PlaneZRegistry.hpp"
namespace geopro::controller {
class VtkSceneController; // 策略委托回控制器既有渲染路径add→addDatasetAsync/add2DDatasetAsyncremove→view.removeDataset
// 平面底图抽象:控制器层不依赖 VTK/appgeopro_controller 仅链 geopro_data+Qt::Core
// app 层(main.cpp)经工厂注入具体 TileBasemap 适配器,仿既有 I3dSceneView/VtkSceneView 边界,
// 避免 geopro_controller 反向依赖 app 层与 VTK。
class IPlaneBasemap {
public:
virtual ~IPlaneBasemap() = default;
virtual void show(int kind) = 0; // 0=矢量平面(Street)/其它=无(hide)
virtual void hide() = 0;
virtual void setOpacity(double o) = 0; // 半透明度[0,1]
virtual void setGroundZ(double z) = 0; // 直接平移平面高程 z(拖 z 值滑块):改瓦片 position无重铺
};
// 工厂:按平面 z 造一份平面底图(底图所需 scene/渲染窗/frame/数据半径规则由 app 闭包捕获)。
// 未注入(空)则不建底图——便于无 VTK 的纯逻辑单测。
using PlaneBasemapFactory = std::function<std::unique_ptr<IPlaneBasemap>(double groundZ)>;
class IDatasetRenderStrategy {
public:
virtual ~IDatasetRenderStrategy() = default;
virtual void add(const std::string& typeId, const std::string& dsId) = 0;
virtual void remove(const std::string& dsId) = 0;
virtual void onTypeActivated(const std::string& /*typeId*/) {}
virtual void onTypeDeactivated(const std::string& /*typeId*/) {}
};
class RenderStrategyRegistry {
public:
void registerStrategy(std::string id, std::unique_ptr<IDatasetRenderStrategy> s) {
strategies_[std::move(id)] = std::move(s);
}
IDatasetRenderStrategy* get(const std::string& id) const {
auto it = strategies_.find(id);
return it == strategies_.end() ? nullptr : it->second.get();
}
private:
std::map<std::string, std::unique_ptr<IDatasetRenderStrategy>> strategies_;
};
// 3 策略:各持 VtkSceneController& 引用,把 add/remove 委托回控制器既有渲染路径。
// Volume/Curtain::add 均转调 addDatasetAsync其内部按 isVolumeDataset 自分体/帘面分支);
// Plane2D::add 暂转调 add2DDatasetAsyncPhase E/F 改平面 z + 底图。remove 统一 view.removeDataset。
class VolumeRenderStrategy : public IDatasetRenderStrategy {
public:
explicit VolumeRenderStrategy(VtkSceneController& ctrl) : ctrl_(ctrl) {}
void add(const std::string& typeId, const std::string& dsId) override;
void remove(const std::string& dsId) override;
private:
VtkSceneController& ctrl_;
};
class CurtainRenderStrategy : public IDatasetRenderStrategy {
public:
explicit CurtainRenderStrategy(VtkSceneController& ctrl) : ctrl_(ctrl) {}
void add(const std::string& typeId, const std::string& dsId) override;
void remove(const std::string& dsId) override;
private:
VtkSceneController& ctrl_;
};
// 每个 2D 类型(段)的勾选足迹落到该类型「同一平面」上:首勾的 ds 定平面 z后续投影到该 z
// 全消则平面消失(z 遗忘)。平面 z 生命周期由 PlaneZRegistry 维护;足迹摆放经 add2DDatasetAsync(z)。
class Plane2DRenderStrategy : public IDatasetRenderStrategy {
public:
explicit Plane2DRenderStrategy(VtkSceneController& ctrl) : ctrl_(ctrl) {}
void add(const std::string& typeId, const std::string& dsId) override;
void remove(const std::string& dsId) override;
void onTypeActivated(const std::string& typeId) override; // 首勾:建该类型平面矢量底图
void onTypeDeactivated(const std::string& typeId) override; // 全消:销毁该类型底图(瓦片随之消失)
// 滑块整体升降该类型平面 z更新 planeReg_ 后,对该类型已勾选足迹移除并按新 z 重摆 + 底图重建于新 z。
void setPlaneZ(const std::string& typeId, double z);
// 底图工厂注入main.cpp 构造后一次性下发;未注入则底图建造静默跳过,便于纯逻辑单测)。
void setBasemapFactory(PlaneBasemapFactory f) { basemapFactory_ = std::move(f); }
void setBasemapKind(const std::string& typeId, int kind); // 0=矢量平面(show)/1=无(hide)
void setBasemapOpacity(const std::string& typeId, double o); // 该类型底图半透明度[0,1]
private:
void createBasemap(const std::string& typeId); // 按当前 z/kind/opacity 建(或重建)该类型底图
VtkSceneController& ctrl_;
PlaneZRegistry planeReg_; // 按类型的平面 z 生命周期
std::map<std::string, std::string> dsToType_; // dsId→typeId(remove 只得 dsId需自存反查)
// 每 2D 类型一份平面矢量底图(贴该类型平面 z);随平面建/销/升降。键=typeId。
std::map<std::string, std::unique_ptr<IPlaneBasemap>> bms_;
std::map<std::string, int> bmKind_; // 该类型底图选择(0=矢量平面/1=无),重建时复用
std::map<std::string, double> bmOpacity_; // 该类型底图透明度,重建时复用
PlaneBasemapFactory basemapFactory_; // app 注入的底图工厂(空=不建底图)
};
} // namespace geopro::controller

View File

@ -1,2 +0,0 @@
#include "DatasetViewState.hpp"
// 实现全部内联于头;此 .cpp 仅为让 AUTOMOC 为带 Q_OBJECT 的头生成并链接 moc。

View File

@ -1,53 +0,0 @@
#pragma once
#include <QHash>
#include <QObject>
#include <QString>
#include "model/ColorScale.hpp"
namespace geopro::controller {
// 跨视图共享的「单一真源」会话状态,按 dsId 维护。统一所有视图间同步,取代两两接线:
// - 改色阶:任何编辑入口只调 setColorScale(dsId, cs),不再各改各的拷贝;
// - 观察:各视图连一次 colorScaleChanged(dsId),槽里【只重渲染】、【绝不回写】→ 无信号回环;
// - 加载:视图取色阶时先问 hubcolorScale 非空则用之),否则把后端值 seed 进来当真源。
// 新增同步项(可见性/选中/值域…)= 加一个字段 + 一个 xxxChanged(dsId) 信号,沿用同一套机制。
//
// 作用域:数据集的「默认/共享色阶」(后端 businessCode=""),被 反演散点/网格/帘面/体 共用(同一条后端
// 记录。measurement(businessCode="R0") 为单视图、无跨视图伙伴,暂不入此层(清晰边界,非欠债)。
class DatasetViewState : public QObject {
Q_OBJECT
public:
explicit DatasetViewState(QObject* parent = nullptr) : QObject(parent) {}
bool hasColorScale(const QString& dsId) const { return scales_.contains(dsId); }
// 无记录返回 nullptr调用方据此兜底为自带值
const geopro::core::ColorScale* colorScale(const QString& dsId) const {
auto it = scales_.constFind(dsId);
return it == scales_.constEnd() ? nullptr : &it.value();
}
// 用户编辑应用:写入真源并广播。观察者据此重渲染。
void setColorScale(const QString& dsId, const geopro::core::ColorScale& cs) {
scales_.insert(dsId, cs);
emit colorScaleChanged(dsId);
}
// 首次从后端加载得到色阶时播种:已有则不覆盖、不广播(避免加载即触发重建/存盘)。
void seedColorScale(const QString& dsId, const geopro::core::ColorScale& cs) {
if (!scales_.contains(dsId)) scales_.insert(dsId, cs);
}
// 清除某 ds 的色阶真源(不广播):体被以新参数重建(如雷达切增益模式→值域大变)时调,
// 让下次 seedColorScale 用重建后的新色阶,而非沿用旧窗口。
void clearColorScale(const QString& dsId) { scales_.remove(dsId); }
signals:
void colorScaleChanged(const QString& dsId);
private:
QHash<QString, geopro::core::ColorScale> scales_;
};
} // namespace geopro::controller

View File

@ -1,6 +1,5 @@
#pragma once
#include <string>
#include <vector>
#include "model/ColorScale.hpp"
#include "model/Field.hpp"
@ -32,8 +31,6 @@ public:
virtual void clear() = 0;
virtual void setVerticalExaggeration(double ve) = 0;
// 三维体体绘制最大不透明度0~1运行时调节已渲染体 + 后续新体(默认 0.30)。默认空实现,测试 mock 无需覆盖。
virtual void setVolumeOpacity(double maxOpacity) { (void)maxOpacity; }
// 地表高程基准测线地表高程2D 足迹「顶部/底部」摆放锚定真实地表。
virtual double zRefElev() const = 0;
@ -45,16 +42,9 @@ public:
// 3D体绘制IDW 体素 + colorScale按 dsId 跟踪。
virtual void addVolume(const std::string& dsId, const geopro::data::VolumeGrid& vol,
const geopro::core::ColorScale& cs) = 0;
// 原地更新已渲染体颜色/不透明度(仅换传函、不重建 image色阶改动用避免换 image 连带关闭未保存切片。
// 返回 true=已原地更新false=该体未渲染/不支持 → 调用方回退 remove+add。默认 false。
virtual bool updateVolumeColorInPlace(const std::string& /*dsId*/,
const geopro::core::ColorScale& /*cs*/) { return false; }
// 2D 足迹:把测线/轨迹经纬折线平铺进 3D 地图worldZ=摆放高程);按 dsId 跟踪以支持增量移除。
virtual void addMapLine(const std::string& dsId, const geopro::data::MapLine& line,
double worldZ) = 0;
// 直接平移一组 2D 足迹到新平面 z拖 z 值滑块用):改足迹 actor 的 SetPosition无移除+异步重载。
// 仅对属于足迹的 dsId 生效;即时渲染。默认空实现,测试 mock 无需覆盖。
virtual void setMapLinesZ(const std::vector<std::string>& /*dsIds*/, double /*z*/) {}
// 3DDEM 地形 + 影像纹理。
virtual void addTerrain(const geopro::data::TerrainPaths& paths) = 0;
// 增量移除某数据集的全部图元(取消勾选时调,不影响其余 ds 与底图)。

View File

@ -1,45 +0,0 @@
#pragma once
#include <map>
#include <set>
#include <string>
namespace geopro::controller {
// 纯逻辑:按 2D 类型管理「平面 z + 成员集」。首勾定 z(之后固定); 全消则平面消失。见 spec §8.2。
// 无 Qt/VTK 依赖,便于纯逻辑单测。被 Plane2DRenderStrategy 持有以摆放足迹。
class PlaneZRegistry {
public:
// 某类型某 ds 勾选:首勾(成员集空)记录平面 z=dsZ返回该类型当前平面 z(后续勾选投影到此)。
double onChecked(const std::string& typeId, const std::string& dsId, double dsZ) {
auto& p = planes_[typeId];
if (p.members.empty()) p.z = dsZ; // 首勾定 z
p.members.insert(dsId);
return p.z;
}
// 取消勾选:移出成员;该类型成员集空时清除条目(平面消失z 遗忘)。
void onUnchecked(const std::string& typeId, const std::string& dsId) {
auto it = planes_.find(typeId);
if (it == planes_.end()) return;
it->second.members.erase(dsId);
if (it->second.members.empty()) planes_.erase(it); // 全消 → 平面消失
}
bool hasPlane(const std::string& typeId) const { return planes_.count(typeId) > 0; }
double planeZ(const std::string& typeId) const {
auto it = planes_.find(typeId);
return it == planes_.end() ? 0.0 : it->second.z;
}
// 滑块整体调:移动该类型既有平面 z(类型不存在则无操作)。
void setPlaneZ(const std::string& typeId, double z) {
auto it = planes_.find(typeId);
if (it != planes_.end()) it->second.z = z;
}
private:
struct Plane {
double z = 0.0;
std::set<std::string> members;
};
std::map<std::string, Plane> planes_;
};
} // namespace geopro::controller

View File

@ -4,136 +4,127 @@
#include <set>
#include <utility>
#include <QDebug>
#include <QPointer>
#include "DatasetViewState.hpp"
#include "I3dSceneView.hpp"
#include "controller/DatasetRenderStrategy.hpp"
#include "repo/CategoryDescriptor.hpp"
#include "repo/IDatasetRepository.hpp"
namespace geopro::controller {
namespace {
// 二维足迹「顶部/底部」摆放相对参考高程(Z=0)的偏移(米):控制器无地形/参考高程源
// (地形异步、帘面经纬未必到场),故退化为 Z=0 上/下固定偏移,使足迹不与帘面顶/底面重叠遮挡。
constexpr double kTopOffsetZ = 50.0; // 顶部:参考面上方
constexpr double kBottomOffsetZ = -50.0; // 底部:参考面下方
} // namespace
VtkSceneController::VtkSceneController(data::IDatasetRepository& dsRepo,
data::I3dSceneRepository& sceneRepo, I3dSceneView& view,
QObject* parent)
: QObject(parent), dsRepo_(dsRepo), sceneRepo_(sceneRepo), view_(view) {
// 注册 3 渲染策略(键与 CategoryDescriptor.renderStrategyId 对应)。各持本控制器引用,
// add/remove 委托回既有渲染路径addDatasetAsync/add2DDatasetAsync/view_.removeDataset
registry_.registerStrategy("volume", std::make_unique<VolumeRenderStrategy>(*this));
registry_.registerStrategy("curtain", std::make_unique<CurtainRenderStrategy>(*this));
auto plane2d = std::make_unique<Plane2DRenderStrategy>(*this);
plane2d_ = plane2d.get(); // 留裸指针供 setPlaneZ 直呼registry_ 持所有权)
registry_.registerStrategy("plane2d", std::move(plane2d));
}
: QObject(parent), dsRepo_(dsRepo), sceneRepo_(sceneRepo), view_(view) {}
void VtkSceneController::setPlaneZ(const QString& typeId, double z) {
if (plane2d_) plane2d_->setPlaneZ(typeId.toStdString(), z);
}
void VtkSceneController::setCheckedDatasets(const QStringList& dsIds) {
std::vector<std::string> newDs;
newDs.reserve(static_cast<std::size_t>(dsIds.size()));
for (const QString& id : dsIds) newDs.push_back(id.toStdString());
void VtkSceneController::setPlaneBasemapFactory(PlaneBasemapFactory factory) {
if (plane2d_) plane2d_->setBasemapFactory(std::move(factory));
}
void VtkSceneController::setBasemapKind(const QString& typeId, int kind) {
if (plane2d_) plane2d_->setBasemapKind(typeId.toStdString(), kind);
}
void VtkSceneController::setBasemapOpacity(const QString& typeId, double opacity) {
if (plane2d_) plane2d_->setBasemapOpacity(typeId.toStdString(), opacity);
}
IDatasetRenderStrategy* VtkSceneController::strategyForType(const std::string& typeId) const {
for (const auto& d : geopro::data::categoryCatalog())
if (d.id == typeId) return registry_.get(d.renderStrategyId);
return nullptr;
}
void VtkSceneController::setViewState(DatasetViewState* state) {
state_ = state;
if (state_)
connect(state_, &DatasetViewState::colorScaleChanged, this,
&VtkSceneController::recolorDataset);
}
void VtkSceneController::recolorDataset(const QString& qid) {
if (!state_) return;
const geopro::core::ColorScale* cs = state_->colorScale(qid);
if (!cs) return;
const std::string dsId = qid.toStdString();
volumeScaleCache_[dsId] = *cs; // 体色阶随真源更新(未渲染时下次勾选命中)
if (!isChecked(dsId)) return; // 未渲染 → 仅更缓存
// 就地重建:体 → 用新色阶重 addVolumeaddVolume 内部触发体下切片随新色阶重建);
// 帘面 → 用缓存源网格重 addCurtain。一个 dsId 只会是其一。
bool changed = false;
if (auto vit = volumeCache_.find(dsId); vit != volumeCache_.end()) {
// 优先原地改色(仅换传函、不重建 image→ 该体下未保存切片不被关闭、跟随改色。
// 原地失败(理论不至)才回退 remove+add会关未保存切片
if (!view_.updateVolumeColorInPlace(dsId, *cs)) {
view_.removeDataset(dsId);
view_.addVolume(dsId, vit->second, *cs);
}
changed = true;
} else if (auto sit = sectionGridCache_.find(dsId); sit != sectionGridCache_.end()) {
view_.removeDataset(dsId);
view_.addCurtain(dsId, sit->second, *cs);
changed = true;
// 2D 俯视测线:保持全量重建(测线非按 ds 跟踪移除)。
if (mode_ == ViewMode::Map2D) {
checkedDs_ = std::move(newDs);
rebuildInternal();
return;
}
if (changed) view_.renderIncremental();
}
void VtkSceneController::setCheckedDatasets(
const std::vector<std::pair<std::string, std::string>>& idType) {
std::map<std::string, std::string> next; // dsId→typeId
for (const auto& p : idType) next[p.first] = p.second;
// 3D增量 diff —— 只处理新增/移除,不全量重建(底图、其余 ds、相机均不动
const std::set<std::string> oldSet(checkedDs_.begin(), checkedDs_.end());
const std::set<std::string> newSet(newDs.begin(), newDs.end());
const std::map<std::string, std::string> prev = checked_; // diff 快照(区分新增/移除)
for (const auto& id : checkedDs_)
if (!newSet.count(id)) view_.removeDataset(id); // 移除:旧有新无 → 仅删该 ds 图元
// 移除:旧有新无 → 派该 ds 类型策略 remove活跃计数归零则 onTypeDeactivated。
for (const auto& [id, typeId] : prev)
if (!next.count(id)) {
if (auto* s = strategyForType(typeId)) s->remove(id);
if (--typeActive_[typeId] == 0)
if (auto* s = strategyForType(typeId)) s->onTypeDeactivated(typeId);
}
// 取景意图按「场景是否已有数据到场过」判定(连续快速勾选时 checked_ 已非空但首批未到场,
// 不可据 checked_ 空否清取景意图,否则相机不对准数据 → 看似不渲染)。全消时复位基线。
checkedDs_ = std::move(newDs);
// 取景意图按「场景是否已有数据到场过」判定,而非 checkedDs_ 是否空——否则连续快速勾选第二个
// ds 时 checkedDs_ 已非空但首批尚未到场,会被误清取景意图,相机不对准数据 → 看似不渲染。
fitOnArrival_ = !hadArrivedData_;
if (next.empty()) hadArrivedData_ = false;
if (checkedDs_.empty()) hadArrivedData_ = false; // 全取消 → 下批到场重新取景
// 先提交勾选集,再派 add异步取数回调以 isChecked() 守「仍勾选?」,同步仓储(测试)会即时回灌,
// 若 add 时 checked_ 未更新则 isChecked 假、回调丢弃 → 不渲染。故必须先 commit 再 add。
checked_ = next;
// 新增:新有旧无 → 活跃计数 0→1 时 onTypeActivated再派策略 add委托回既有渲染路径
for (const auto& [id, typeId] : checked_)
if (!prev.count(id)) {
if (typeActive_[typeId]++ == 0)
if (auto* s = strategyForType(typeId)) s->onTypeActivated(typeId);
if (auto* s = strategyForType(typeId)) s->add(typeId, id);
}
const unsigned long long gen = rebuildGeneration_; // 不自增:并发增量互不作废
for (const auto& id : checkedDs_)
if (!oldSet.count(id)) addDatasetAsync(id, gen); // 新增:新有旧无 → 异步取数增量入场
view_.renderIncremental(); // 立即反映移除 / 触发坐标轴重算
}
void VtkSceneController::add2DDatasetAsync(const std::string& dsId, unsigned long long gen,
double z) {
void VtkSceneController::setChecked2DDatasets(const QStringList& dsIds) {
std::vector<std::string> newDs;
newDs.reserve(static_cast<std::size_t>(dsIds.size()));
for (const QString& id : dsIds) newDs.push_back(id.toStdString());
// 二维足迹始终画进 View3D 场景,且按 dsId 跟踪 → 一律增量 diff不全量重建不打断 3D 帘面/体)。
const std::set<std::string> oldSet(checked2dDs_.begin(), checked2dDs_.end());
const std::set<std::string> newSet(newDs.begin(), newDs.end());
// 此前空场景(无 3D 数据且无 2D 足迹) → 首批足迹到场自动取景;否则增量追加保持相机不跳。
const bool wasEmpty = checkedDs_.empty() && checked2dDs_.empty();
for (const auto& id : checked2dDs_)
if (!newSet.count(id)) view_.removeDataset(id); // 取消勾选 → 移除该足迹图元
checked2dDs_ = std::move(newDs);
fitOnArrival_ = wasEmpty; // 首批足迹(空场景)取景;否则保持当前相机不跳
// 足迹画进 View3D 场景mode=0 关闭 → 仅记录勾选不渲染(见 set2DPlacement 切回时补画)。
if (placement2dMode_ != 0 && mode_ == ViewMode::View3D) {
const unsigned long long gen = rebuildGeneration_; // 不自增:与 3D 增量互不作废
for (const auto& id : checked2dDs_)
if (!oldSet.count(id)) add2DDatasetAsync(id, gen); // 新增 → 异步取足迹增量入场
}
view_.renderIncremental(); // 立即反映移除
}
void VtkSceneController::set2DPlacement(int mode, double customZ) {
const bool changed = (mode != placement2dMode_) || (mode == 4 && customZ != customZ2d_);
placement2dMode_ = mode;
customZ2d_ = customZ;
if (!changed || checked2dDs_.empty()) return;
// 摆放变化 → 对已勾选足迹重摆:先全部移除,再按新 Z 重加mode=0 关闭则只移除不重加)。
for (const auto& id : checked2dDs_) view_.removeDataset(id);
if (placement2dMode_ != 0 && mode_ == ViewMode::View3D) {
const unsigned long long gen = rebuildGeneration_;
fitOnArrival_ = false; // 重摆:保持相机
for (const auto& id : checked2dDs_) add2DDatasetAsync(id, gen);
}
view_.renderIncremental();
}
double VtkSceneController::placementZ() const {
const double surf = view_.zRefElev(); // 真实地表高程基准(测线地表高程)
switch (placement2dMode_) {
case 1: return 0.0; // Z=0世界原点
case 2: return surf + kTopOffsetZ; // 顶部:贴真实地表上方
case 3: return surf + kBottomOffsetZ; // 底部:真实地表下方
case 4: return customZ2d_; // 自定义
default: return 0.0; // 关闭(0) 不应走到此(调用方拦截)
}
}
void VtkSceneController::add2DDatasetAsync(const std::string& dsId, unsigned long long gen) {
if (loadingDs_.count(dsId)) return; // 已在加载(重复勾选竞态)→ 不重复请求
loadingDs_.insert(dsId);
QPointer<VtkSceneController> self(this);
sceneRepo_.loadMapLine(
dsId,
[self, gen, dsId, z](data::MapLine line) {
[self, gen, dsId](data::MapLine line) {
if (!self) return;
self->loadingDs_.erase(dsId);
// gen 作废 / 已取消勾选 → 丢弃迟到回调。
if (gen != self->rebuildGeneration_ || !self->is2DChecked(dsId)) {
// gen 作废 / 已取消勾选 / 摆放已关闭 → 丢弃迟到回调。
if (gen != self->rebuildGeneration_ || !self->is2DChecked(dsId) ||
self->placement2dMode_ == 0) {
return;
}
// 足迹摆到所属 2D 类型的平面 z首勾定、后续投影由 Plane2DRenderStrategy 决定)。
self->view_.addMapLine(dsId, line, z);
// 落地时按当前摆放 Z非请求时快照→ 加载期间摆放变化也取最新高程
self->view_.addMapLine(dsId, line, self->placementZ());
self->onDatasetArrived();
},
[self, gen, dsId](const std::string& m) {
@ -154,17 +145,11 @@ void VtkSceneController::addDatasetAsync(const std::string& dsId, unsigned long
auto cachedGrid = volumeCache_.find(dsId);
auto cachedScale = volumeScaleCache_.find(dsId);
if (cachedGrid != volumeCache_.end() && cachedScale != volumeScaleCache_.end()) {
const QString qid = QString::fromStdString(dsId); // 优先用色阶真源(含已编辑值)
const geopro::core::ColorScale& useCs =
(state_ && state_->colorScale(qid)) ? *state_->colorScale(qid) : cachedScale->second;
view_.addVolume(dsId, cachedGrid->second, useCs); // 缓存命中(色阶随体缓存)
view_.addVolume(dsId, cachedGrid->second, cachedScale->second); // 缓存命中(色阶随体缓存)
onDatasetArrived();
emit volumeRendered(QString::fromStdString(dsId)); // 缓存命中即时完成 → 撤 spinner
emit datasetRendered(QString::fromStdString(dsId));
return;
}
loadingDs_.insert(dsId);
emit datasetLoading(QString::fromStdString(dsId)); // 异步建体开始 → 列表项转 spinner
sceneRepo_.loadVolume(
dsId,
[self, gen, dsId](data::VolumeGrid g, core::ColorScale cs) {
@ -172,19 +157,9 @@ void VtkSceneController::addDatasetAsync(const std::string& dsId, unsigned long
self->loadingDs_.erase(dsId);
if (gen != self->rebuildGeneration_ || !self->isChecked(dsId)) return;
self->volumeScaleCache_[dsId] = cs; // 色阶随体一起缓存mock 体在 dsRepo_ 无条目)
const QString qid = QString::fromStdString(dsId);
if (self->state_) self->state_->seedColorScale(qid, cs); // 播种真源
auto it = self->volumeCache_.emplace(dsId, std::move(g)).first;
qInfo().noquote() << "[volrender] addVolume dsId=" << qid
<< "nx=" << it->second.vol.nx() << "ny=" << it->second.vol.ny()
<< "nz=" << it->second.vol.nz();
const geopro::core::ColorScale& useCs =
(self->state_ && self->state_->colorScale(qid)) ? *self->state_->colorScale(qid)
: self->volumeScaleCache_[dsId];
self->view_.addVolume(dsId, it->second, useCs);
self->view_.addVolume(dsId, it->second, self->volumeScaleCache_[dsId]);
self->onDatasetArrived();
emit self->volumeRendered(QString::fromStdString(dsId)); // 落地完成 → 撤 spinner
emit self->datasetRendered(QString::fromStdString(dsId));
},
[self, gen, dsId](const std::string& m) {
if (!self) return;
@ -197,22 +172,14 @@ void VtkSceneController::addDatasetAsync(const std::string& dsId, unsigned long
// 剖面 → 帘面(着色用 loadSection 返回的 s.scale与体的源色阶同源
loadingDs_.insert(dsId);
emit datasetLoading(QString::fromStdString(dsId)); // 剖面首次加载较慢 → 列表项转 spinner
sceneRepo_.loadSection(
dsId,
[self, gen, dsId](data::SectionData s) {
if (!self) return;
self->loadingDs_.erase(dsId);
if (gen != self->rebuildGeneration_ || !self->isChecked(dsId)) return; // 作废/已取消
self->sectionGridCache_.insert_or_assign(dsId, s.grid); // 留存源网格供帘面重着色Grid 无默认构造)
const QString qid = QString::fromStdString(dsId);
if (self->state_) self->state_->seedColorScale(qid, s.scale); // 播种真源
const geopro::core::ColorScale& useCs =
(self->state_ && self->state_->colorScale(qid)) ? *self->state_->colorScale(qid)
: s.scale;
self->view_.addCurtain(dsId, s.grid, useCs);
self->view_.addCurtain(dsId, s.grid, s.scale);
self->onDatasetArrived();
emit self->datasetRendered(QString::fromStdString(dsId)); // 帘面落地 → 复原复选框
},
[self, gen, dsId](const std::string& m) {
if (!self) return;
@ -231,11 +198,11 @@ void VtkSceneController::onDatasetArrived() {
}
bool VtkSceneController::isChecked(const std::string& dsId) const {
return checked_.count(dsId) > 0; // 统一勾选集(异步回调「仍勾选?」守护)
return std::find(checkedDs_.begin(), checkedDs_.end(), dsId) != checkedDs_.end();
}
bool VtkSceneController::is2DChecked(const std::string& dsId) const {
return checked_.count(dsId) > 0; // 同上2D 足迹与 3D 同处统一勾选集
return std::find(checked2dDs_.begin(), checked2dDs_.end(), dsId) != checked2dDs_.end();
}
void VtkSceneController::setViewMode(ViewMode mode) {
@ -262,42 +229,21 @@ void VtkSceneController::setVerticalExaggeration(double ve) {
preserveCameraOnRebuild_ = false;
}
void VtkSceneController::setVolumeOpacity(double maxOpacity) {
// 运行时更新已渲染体的不透明度传递函数(不重建体,实时跟手)+ 记为后续新体默认(见 VtkSceneView
view_.setVolumeOpacity(maxOpacity);
}
void VtkSceneController::rebuild() { rebuildInternal(); }
void VtkSceneController::setVolumeColorScale(const std::string& dsId,
const geopro::core::ColorScale& cs) {
// 统一走色阶真源:写入即广播 colorScaleChanged → recolorDataset 就地重着色(体/帘面+切片),
// 同时 2D 详情等其它视图一并跟随。无 state_理论不至才退化为直连重建。
if (state_) {
state_->setColorScale(QString::fromStdString(dsId), cs);
return;
}
volumeScaleCache_[dsId] = cs;
if (!isChecked(dsId)) return;
volumeScaleCache_[dsId] = cs; // 会话级 mock 持久(再勾选命中缓存,见 addDatasetAsync
if (!isChecked(dsId)) return; // 未渲染 → 仅更缓存,下次勾选生效
auto git = volumeCache_.find(dsId);
if (git == volumeCache_.end()) return;
if (git == volumeCache_.end()) return; // 体网格尚未到场 → 同上
// 移除旧体素 → 以新色阶重建addVolume 内部置 currentColorScale_ 并触发 onVolumeChanged
// InteractionManager 据此以新色阶重建该体下已勾选切片。
view_.removeDataset(dsId);
view_.addVolume(dsId, git->second, cs);
view_.renderIncremental();
}
void VtkSceneController::rebuildRadarVolume(const std::string& dsId) {
// 仓储已切增益模式并失效其 cachedGrid(setRadarGainMode)。此处:
// 1) 清控制器缓存(否则命中旧体)2) 清旧色阶真源(增益后值域大变,须用重建后新窗口)
// 3) 移除旧 actor4) 若勾选中 → 异步用新增益重建体并重渲。
volumeCache_.erase(dsId);
volumeScaleCache_.erase(dsId);
if (state_) state_->clearColorScale(QString::fromStdString(dsId));
view_.removeDataset(dsId);
if (isChecked(dsId)) addDatasetAsync(dsId, rebuildGeneration_);
view_.renderIncremental();
}
void VtkSceneController::setAxesMode(AxesMode mode) {
axesMode_ = mode;
rebuildInternal(); // 坐标轴随场景重建clear 会移除旧坐标轴 prop
@ -328,6 +274,12 @@ void VtkSceneController::zoomIn() { view_.zoom(1.2); }
void VtkSceneController::zoomOut() { view_.zoom(1.0 / 1.2); }
void VtkSceneController::fit() { view_.fitView(); }
const geopro::core::Grid& VtkSceneController::grid(const std::string& dsId) {
auto it = gridCache_.find(dsId);
if (it == gridCache_.end()) it = gridCache_.emplace(dsId, dsRepo_.loadGrid(dsId)).first;
return it->second;
}
const geopro::core::ColorScale& VtkSceneController::colorScale(const std::string& dsId) {
auto it = colorScaleCache_.find(dsId);
if (it == colorScaleCache_.end())
@ -337,6 +289,7 @@ const geopro::core::ColorScale& VtkSceneController::colorScale(const std::string
void VtkSceneController::rebuildInternal() {
const unsigned long long gen = ++rebuildGeneration_; // 自增:作废此前所有在途增量回调
const bool is2D = (mode_ == ViewMode::Map2D);
view_.clear(); // 移除全部数据图元(保留底图)frame 重锚标志复位
loadingDs_.clear(); // 旧在途加载随之作废(回调按 gen 丢弃)
@ -348,30 +301,34 @@ void VtkSceneController::rebuildInternal() {
// 坏 dsIdloadGrid/loadColorScale 抛异常)= best-effort 跳过emit loadFailed 但不中断。
try {
// 回调用 QPointer<self> 守对象存活 + gen 守数据新鲜:迟到回调若已析构/作废则丢弃。
QPointer<VtkSceneController> self(this);
if (showTerrain_) {
sceneRepo_.loadTerrainPaths(
[self, gen](data::TerrainPaths p) {
if (!self || gen != self->rebuildGeneration_) return; // 已析构/迟到:丢弃
self->view_.addTerrain(std::move(p));
self->onDatasetArrived();
},
[self, gen](const std::string& m) {
if (!self || gen != self->rebuildGeneration_) return;
emit self->loadFailed(QString::fromStdString(m));
});
if (is2D) {
for (const auto& dsId : checkedDs_) view_.addSurveyLine(grid(dsId));
} else {
// 回调用 QPointer<self> 守对象存活 + gen 守数据新鲜:迟到回调若已析构/作废则丢弃。
QPointer<VtkSceneController> self(this);
if (showTerrain_) {
sceneRepo_.loadTerrainPaths(
[self, gen](data::TerrainPaths p) {
if (!self || gen != self->rebuildGeneration_) return; // 已析构/迟到:丢弃
self->view_.addTerrain(std::move(p));
self->onDatasetArrived();
},
[self, gen](const std::string& m) {
if (!self || gen != self->rebuildGeneration_) return;
emit self->loadFailed(QString::fromStdString(m));
});
}
for (const auto& dsId : checkedDs_) addDatasetAsync(dsId, gen);
// 二维足迹随全量重建一并重画clear 已移除其图元mode=0 关闭则跳过。
if (placement2dMode_ != 0)
for (const auto& dsId : checked2dDs_) add2DDatasetAsync(dsId, gen);
}
// 全量重建clear 已移除全部图元,据统一勾选集经各 ds 类型策略重放 add不动活跃计数
// 已在 setCheckedDatasets 计入;策略 add 内部转调 addDatasetAsync/add2DDatasetAsync
for (const auto& [dsId, typeId] : checked_)
if (auto* s = strategyForType(typeId)) s->add(typeId, dsId);
} catch (const std::exception& e) {
emit loadFailed(QString::fromStdString(e.what()));
}
// 保留相机重建(改VE):不 ResetCamera原地按新夸张重绘。视图恒三维(is2D=false)。
view_.render(/*is2D=*/false, /*resetCamera=*/!preserveCameraOnRebuild_);
// 保留相机重建(改VE):不 ResetCamera原地按新夸张重绘。
view_.render(is2D, /*resetCamera=*/!preserveCameraOnRebuild_);
}
} // namespace geopro::controller

View File

@ -1,17 +1,13 @@
#pragma once
#include <QObject>
#include <QPointer>
#include <QString>
#include <QStringList>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "I3dSceneView.hpp"
#include "controller/DatasetRenderStrategy.hpp"
#include "model/ColorScale.hpp"
#include "model/Field.hpp"
#include "repo/I3dSceneRepository.hpp"
@ -22,10 +18,8 @@ class IDatasetRepository;
namespace geopro::controller {
class DatasetViewState; // 跨视图共享色阶真源(统一同步机制)
// 中央视图模式:固定三维视图(帘面/体素/地形)。旧二维俯视测线(Map2D)路径已退役main 恒 View3D
enum class ViewMode { View3D };
// 中央视图模式:二维地图(俯视测线)/ 三维视图(帘面/体素/地形)。
enum class ViewMode { Map2D, View3D };
// 三维图层("视图详情"浮层勾选)。
enum class SceneLayer { Curtain, Voxel, Terrain };
@ -37,41 +31,23 @@ enum class SceneLayer { Curtain, Voxel, Terrain };
// 不持有 widget不认 vtkActor/vtkVolume全交给 I3dSceneView
class VtkSceneController : public QObject {
Q_OBJECT
// 渲染策略委托回控制器既有路径addDatasetAsync/add2DDatasetAsync/view_友元免widen公有面。
friend class VolumeRenderStrategy;
friend class CurtainRenderStrategy;
friend class Plane2DRenderStrategy;
public:
VtkSceneController(data::IDatasetRepository& dsRepo, data::I3dSceneRepository& sceneRepo,
I3dSceneView& view, QObject* parent = nullptr);
// 注入跨视图色阶真源(统一同步):连 colorScaleChanged → 就地按 dsId 重着色帘面/体。
// 构造后由 main.cpp 注入一次。
void setViewState(DatasetViewState* state);
// 注入 2D 平面底图工厂app 层闭包捕获 scene/渲染窗/frame/数据半径规则,造 TileBasemap 适配器):
// 转交 Plane2DRenderStrategy供各类型平面按需建底图。main.cpp 构造后一次性下发。
void setPlaneBasemapFactory(PlaneBasemapFactory factory);
public:
// 勾选并集统一入口(取代旧 setCheckedDatasets(QStringList)/setChecked2DDatasets
// 每项 = (dsId, typeId=描述符 id)。diff vs 上次后按 catalog[typeId].renderStrategyId 派给策略
// add/remove并维护「每 typeId 活跃数」在首勾/全消时调 onTypeActivated/Deactivated。
void setCheckedDatasets(const std::vector<std::pair<std::string, std::string>>& idType);
public slots:
void setCheckedDatasets(const QStringList& dsIds);
// 二维数据集栏勾选(足迹型测线/轨迹)→ 平铺进 View3D 地图。与 3D 勾选集独立,按 dsId 增量。
void setChecked2DDatasets(const QStringList& dsIds);
// 二维足迹摆放高度mode0关闭 /1 Z=0 /2 顶部 /3 底部 /4 自定义customZ 仅 mode=4 用)。
void set2DPlacement(int mode, double customZ);
void setViewMode(ViewMode mode);
void setLayer(SceneLayer layer, bool on);
void setVerticalExaggeration(double ve);
// 三维体透明度调节工具条滑块运行时更新已渲染体的不透明度并作为后续新体默认0~1
void setVolumeOpacity(double maxOpacity);
void rebuild(); // 主题切换等外部触发的重渲染
// 雷达体增益模式切换后重建:仓储已切模式+清缓存(setRadarGainMode),此处清控制器缓存/旧色阶
// 并(若勾选中)异步用新增益重建体、重渲。
void rebuildRadarVolume(const std::string& dsId);
// 色阶编辑器「确定」:写入色阶真源(state_),经 colorScaleChanged 统一就地重着色(体/帘面 + 切片)。
// 兼容旧调用点;真正的重着色在 recolorDataset()。无 state_ 时退化为直连重建
// 色阶编辑器「确定」:更新某三维体色阶并就地重渲染(体素 + 其切片随新色阶重建)。
// 后端 3D 色阶保存未就绪 → 缓存即会话级 mock 持久(再勾选命中 volumeScaleCache_
void setVolumeColorScale(const std::string& dsId, const geopro::core::ColorScale& cs);
// ── P2 三维数据集栏 ──
@ -80,12 +56,6 @@ public slots:
// 坐标轴设置面板「应用」:一次性下发 显示方式 + 单位 + per-axis 可见性/范围(单次重建)。
void setAxesConfig(AxesMode mode, AxesUnit unit, const AxisRangeCfg& x, const AxisRangeCfg& y,
const AxisRangeCfg& z);
// 2D 段「z 值」滑块:整体升降某 2D 类型平面(含其上全部已勾选足迹)。转交 Plane2DRenderStrategy。
void setPlaneZ(const QString& typeId, double z);
// 2D 段「底图」弹窗:切该类型平面底图 矢量平面(0)/无(1) + 透明度[0,1]。转交 Plane2DRenderStrategy。
void setBasemapKind(const QString& typeId, int kind);
void setBasemapOpacity(const QString& typeId, double opacity);
void applyView(ViewDir dir); // 6 向快捷视图
void zoomIn(); // Zoom In (×1.2)
void zoomOut(); // Zoom Out (×1/1.2)
@ -93,40 +63,33 @@ public slots:
signals:
void loadFailed(const QString& message);
// 三维体异步建体+落地渲染完成dsId。供 UI 撤回该体列表项的等待 spinner、复原复选框。
void volumeRendered(const QString& dsId);
// 任一数据集(剖面/体)异步加载开始 / 渲染完成:上层据此把该列表项复选框↔等待 spinner 切换。
// 仅异步路径发(缓存命中即时完成只发 rendered覆盖非三维体剖面首次渲染也较慢用户反馈
void datasetLoading(const QString& dsId);
void datasetRendered(const QString& dsId);
private:
void rebuildInternal();
// colorScaleChanged(dsId) 槽:从 state_ 取新色阶,就地重建该 dsId 的帘面/体(及体下切片)。只渲染,不回写。
void recolorDataset(const QString& dsId);
// 增量加入单个 ds帘面/体素,按图层开关);回调按 gen + 仍勾选 守护,落地后增量渲染。
void addDatasetAsync(const std::string& dsId, unsigned long long gen);
// 增量加入单个 2D 足迹(异步 loadMapLine → addMapLine at 摆放 z回调按 gen + 仍勾选 守护。
// z 为该 ds 所属 2D 类型的平面高程(由 Plane2DRenderStrategy 经 PlaneZRegistry 决定§E2
void add2DDatasetAsync(const std::string& dsId, unsigned long long gen, double z);
// 增量加入单个 2D 足迹(异步 loadMapLine → addMapLine at 当前摆放 Z回调按 gen + 仍勾选 守护。
void add2DDatasetAsync(const std::string& dsId, unsigned long long gen);
void onDatasetArrived(); // 单个 ds 落地后:增量渲染 + 首批数据自动取景
bool isChecked(const std::string& dsId) const;
bool is2DChecked(const std::string& dsId) const;
// 按 typeId 查其渲染策略catalog[typeId].renderStrategyId → registry_。未知 typeId 返回 nullptr
IDatasetRenderStrategy* strategyForType(const std::string& typeId) const;
// 当前摆放模式下足迹的世界 Zmode 0=关闭由调用方拦截;此处算 1/2/3/4 的 Z
double placementZ() const;
data::IDatasetRepository& dsRepo_;
data::I3dSceneRepository& sceneRepo_;
I3dSceneView& view_;
// 统一勾选集2D+3D 合一dsId→typeId(描述符 id)。增量 diff 的真源rebuildInternal 据此重放。
std::map<std::string, std::string> checked_;
// 每 typeId 活跃计数:首勾(0→1)调 onTypeActivated、全消(1→0)调 onTypeDeactivated。
std::map<std::string, int> typeActive_;
// 渲染策略注册表(构造时注册 volume/curtain/plane2d 三策略,各持本控制器引用)。
RenderStrategyRegistry registry_;
Plane2DRenderStrategy* plane2d_ = nullptr; // registry_ 中 plane2d 策略的裸指针setPlaneZ 免下转型)
ViewMode mode_ = ViewMode::View3D;
std::vector<std::string> checkedDs_;
// 二维足迹勾选集(与 checkedDs_ 独立;都画进 View3D 场景)。按 dsId 增量加/删。
std::vector<std::string> checked2dDs_;
// 二维足迹摆放mode 0关闭/1 Z=0/2顶部/3底部/4自定义customZ2d_ 仅 mode=4 用。
// 默认 Z=0(1) 与 Column2DDataset「2D视图」下拉可见默认项一致——避免「下拉显示 Z=0 但
// 控制器实为关闭」的初始信号丢失desync(组合框 setCurrentIndex 在 connect 前发射、且
// 组件早于 main.cpp 接线构造,初始 view2DModeChanged 永不送达),致勾选足迹静默不渲染。
int placement2dMode_ = 1;
double customZ2d_ = 0.0;
ViewMode mode_ = ViewMode::Map2D;
bool showCurtain_ = true;
bool showVoxel_ = false;
bool showTerrain_ = false;
@ -139,12 +102,9 @@ private:
AxisRangeCfg axisX_, axisY_, axisZ_; // 坐标轴设置面板的 per-axis 可见性 + 自定义范围
static constexpr int kAxesFontSize = 12;
QPointer<DatasetViewState> state_; // 跨视图色阶真源(注入;编辑/加载/重着色都经它QPointer 防悬挂)
// 缓存(按 dsId避免重复读盘/插值。
std::map<std::string, geopro::core::Grid> gridCache_;
std::map<std::string, geopro::core::ColorScale> colorScaleCache_;
// 帘面源网格缓存:帘面重着色需 grid 重建 addCurtainloadSection 的 s.grid 缓存于此)。
std::map<std::string, geopro::core::Grid> sectionGridCache_;
std::map<std::string, data::VolumeGrid> volumeCache_;
// 三维体色阶缓存mock 体在 dsRepo_ 无条目,色阶随 loadVolume 一起交付并缓存于此。
std::map<std::string, geopro::core::ColorScale> volumeScaleCache_;
@ -160,6 +120,7 @@ private:
// 正在加载的 ds防重复勾选竞态重复请求全量重建时清空。
std::set<std::string> loadingDs_;
const geopro::core::Grid& grid(const std::string& dsId);
const geopro::core::ColorScale& colorScale(const std::string& dsId);
};

Some files were not shown because too many files have changed in this diff Show More