725 lines
36 KiB
C++
725 lines
36 KiB
C++
// M1 工作台(视图重构 Task B):正确产品模型。
|
||
// - 左上 对象显示栏:GS→TM(测线,复选框)。勾选测线 → 在中央显示其 dd_section,可多条共存。
|
||
// - 左下 数据真实显示栏:单击测线 → 列其采集批次(数据集,tab 数据/文件)。单击采集批次 → 数据详情+异常+属性。
|
||
// - 中央「二维地图 / 三维视图」:两个互斥视图(内容不同,不是同一物体换相机)。
|
||
// 二维地图 = 对每个勾选数据集 buildSurveyLine(lat/lon 红线俯视,z=0)+ applyTop2D(浅底背景)。
|
||
// 三维视图 = 勾选测线的 buildCurtain(竖直断面墙),actor SetScale(1,1,3) 纵向夸张 + applyFree3D(白底)。
|
||
// 三维左上「视图详情」浮层(对齐原型):图层勾选 帘面 / 体素(dd_voxel,散点经 EPSG:4547 配准 IDW)
|
||
// / 切片(dd_slice,vtkImagePlaneWidget 在体素 image 上交互拖切面) / 地形(DEM 高程面 + 影像纹理)。
|
||
// 切视图 / 勾选变化 / 图层变化 → 重建对应内容。
|
||
// - 下方「数据详情」:独立 QVTK 小视图 + 工具条「原数据 / 网格数据」切换 +「显示异常」开关(对齐原型)。
|
||
// 单击某 DS → 显示该数据集:
|
||
// 网格数据 = #18 banded 等值面+等值线(两 actor SetScale(1,1.5,1) 纵向夸张)。
|
||
// 原数据 = #17 彩色散点(buildScatter,x=距离/y=深度,按散点自带色阶上色)。
|
||
// 显示异常 = 在上图叠加异常圈定(buildAnomalies,dashed 折线,同纵向夸张对齐)。
|
||
// 两者皆平躺俯视正交 + 属性。
|
||
// - 右 属性:选中数据集属性文本。
|
||
// 世界系:启动 loadGrid("grid1") 取一次,用其 lat/lon 中位/均值作 GeoLocalFrame(全项目共享,保证多视图配准)。
|
||
|
||
#include <fstream>
|
||
#include <memory>
|
||
#include <sstream>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#include <QActionGroup>
|
||
#include <QApplication>
|
||
#include <QCheckBox>
|
||
#include <QDialog>
|
||
#include <QFile>
|
||
#include <QFrame>
|
||
#include <QLabel>
|
||
#include <QListWidget>
|
||
#include <QListWidgetItem>
|
||
#include <QSignalBlocker>
|
||
#include <QStringList>
|
||
#include <QTabWidget>
|
||
#include <QMainWindow>
|
||
#include <QSurfaceFormat>
|
||
#include <QToolBar>
|
||
#include <QTreeWidget>
|
||
#include <QTreeWidgetItem>
|
||
#include <QVBoxLayout>
|
||
#include <QWidget>
|
||
|
||
#include <DockManager.h>
|
||
#include <DockWidget.h>
|
||
|
||
#include "model/ColorScale.hpp"
|
||
#include "model/Field.hpp"
|
||
#include "repo/LocalSampleRepository.hpp"
|
||
|
||
#include "ApiClient.hpp"
|
||
#include "AuthService.hpp"
|
||
#include "login/LoginWindow.hpp"
|
||
#include "panels/AnomalyListPanel.hpp"
|
||
#include "panels/DatasetListPanel.hpp"
|
||
|
||
#include "CameraPreset.hpp"
|
||
#include "ColorLutBuilder.hpp"
|
||
#include "Scene.hpp"
|
||
#include "VoxelFromScatters.hpp"
|
||
#include "actors/AnomalyActor.hpp"
|
||
#include "actors/CurtainActor.hpp"
|
||
#include "actors/ElectrodeActor.hpp"
|
||
#include "actors/GridContourActor.hpp"
|
||
#include "actors/MapLineActor.hpp"
|
||
#include "actors/ScatterActor.hpp"
|
||
#include "actors/TerrainActor.hpp"
|
||
|
||
#include "geo/CrsTransform.hpp"
|
||
#include "geo/GeoLocalFrame.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <exception>
|
||
#include <memory>
|
||
#include <set>
|
||
#include <vector>
|
||
|
||
#include <QVTKOpenGLStereoWidget.h>
|
||
#include <vtkGenericOpenGLRenderWindow.h>
|
||
#include <vtkImagePlaneWidget.h>
|
||
#include <vtkLookupTable.h>
|
||
#include <vtkRenderWindowInteractor.h>
|
||
#include <vtkRenderer.h>
|
||
#include <vtkSmartPointer.h>
|
||
|
||
namespace {
|
||
|
||
// 角色:树 TM 项存 tmId(UserRole+2);数据列表 DS 项的 dsId/ddType 由 panels/DatasetListPanel 定义。
|
||
constexpr int kRoleTmId = Qt::UserRole + 2;
|
||
|
||
// 从对象结构树构建 QTreeWidget:GS → TM 两层(对齐原型;DS=采集批次在左下「数据列表」,不进树)。
|
||
// TM(测线) 项可勾选(复选框):勾选驱动该测线的 dd_section 在中央场景显示;UserRole+2 存 tmId。
|
||
// 含 dd_section 的测线默认勾选,启动即显示。
|
||
void populateTree(QTreeWidget* tree, const std::vector<geopro::data::GsNode>& gss)
|
||
{
|
||
for (const auto& gs : gss) {
|
||
auto* gsItem = new QTreeWidgetItem(tree);
|
||
gsItem->setText(0, QString::fromStdString(gs.name));
|
||
for (const auto& tm : gs.tms) {
|
||
auto* tmItem = new QTreeWidgetItem(gsItem);
|
||
tmItem->setText(0, QString::fromStdString(tm.name));
|
||
tmItem->setData(0, kRoleTmId, QString::fromStdString(tm.id));
|
||
tmItem->setFlags(tmItem->flags() | Qt::ItemIsUserCheckable);
|
||
const bool hasSection =
|
||
std::any_of(tm.dss.begin(), tm.dss.end(),
|
||
[](const geopro::data::DsNode& d) { return d.ddType == "dd_section"; });
|
||
tmItem->setCheckState(0, hasSection ? Qt::Checked : Qt::Unchecked);
|
||
}
|
||
}
|
||
tree->expandAll();
|
||
}
|
||
|
||
// 在结构中按 tmId 查 TM;找不到返回 nullptr。
|
||
const geopro::data::TmNode* findTm(const std::vector<geopro::data::GsNode>& gss,
|
||
const std::string& tmId)
|
||
{
|
||
for (const auto& gs : gss)
|
||
for (const auto& tm : gs.tms)
|
||
if (tm.id == tmId) return &tm;
|
||
return nullptr;
|
||
}
|
||
|
||
// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。
|
||
std::string readPem(const std::string& path)
|
||
{
|
||
std::ifstream in(path, std::ios::binary);
|
||
if (!in) return {};
|
||
std::ostringstream ss;
|
||
ss << in.rdbuf();
|
||
return ss.str();
|
||
}
|
||
|
||
// 取 vector 中位数(用于由测线 lat/lon 推世界系原点)。空则返回 0。
|
||
double median(std::vector<double> v)
|
||
{
|
||
if (v.empty()) return 0.0;
|
||
std::sort(v.begin(), v.end());
|
||
const size_t n = v.size();
|
||
return n % 2 ? v[n / 2] : 0.5 * (v[n / 2 - 1] + v[n / 2]);
|
||
}
|
||
|
||
// 当前中央视图(默认二维地图)。二维地图=测线红线俯视;三维视图=断面墙。
|
||
enum class ViewMode { Map2D, View3D };
|
||
|
||
// 数据详情显示内容(默认网格数据)。网格数据=#18 banded;原数据=#17 散点(对齐原型命名)。
|
||
enum class DetailMode { Section18, Scatter17 };
|
||
|
||
// #17 散点屏幕像素方块边长。
|
||
constexpr float kScatterPointSize = 4.0F;
|
||
|
||
// 纵向夸张倍数:三维断面墙沿 z 拉伸成墙;数据详情 #18 沿 y 拉伸填面板。
|
||
constexpr double kCurtainZScale = 3.0;
|
||
constexpr double kDetailYScale = 1.5;
|
||
|
||
// 项目 CRS(实证确定,STATUS §4):散点 projX/Y→经纬→GeoLocalFrame 配准体素到世界系。
|
||
constexpr const char* kProjectCrs = "EPSG:4547"; // CGCS2000 / 3-degree GK CM 114E
|
||
constexpr const char* kWgs84 = "EPSG:4326";
|
||
|
||
// 在给定 QMainWindow 上构建 M1 工作台。
|
||
// repo 生命周期须覆盖到事件循环结束(由调用方保证)。
|
||
void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo)
|
||
{
|
||
// ── 世界系:启动取一次 grid1 的 lat/lon,用中位数作 GeoLocalFrame 原点 ──
|
||
// 全项目共享(shared_ptr 持有):所有帘面用同一 frame 投影,保证多条测线空间配准。
|
||
const auto baseGrid = repo.loadGrid("grid1");
|
||
const double lat0 = median(baseGrid.lat);
|
||
const double lon0 = median(baseGrid.lon);
|
||
auto frame = std::make_shared<geopro::core::GeoLocalFrame>(lat0, lon0);
|
||
// 测线地表高程基准(地形 z rebase 用,使地形落在测线附近而非按绝对高程浮空)。
|
||
const double refElev = baseGrid.elevation.empty() ? 0.0 : median(baseGrid.elevation);
|
||
|
||
// ── 中央 QVTK + Scene(竖直帘面场景)─────────────────────────────────
|
||
// Scene 非 QObject:堆分配,用 widget 销毁信号清理(widget 随 window 销毁)。
|
||
auto* scene = new geopro::render::Scene();
|
||
auto* vtkWidget = new QVTKOpenGLStereoWidget();
|
||
QObject::connect(vtkWidget, &QObject::destroyed, [scene]() { delete scene; });
|
||
vtkNew<vtkGenericOpenGLRenderWindow> renderWindow;
|
||
vtkWidget->setRenderWindow(renderWindow);
|
||
renderWindow->AddRenderer(scene->renderer());
|
||
|
||
vtkRenderer* rendererPtr = scene->renderer();
|
||
vtkGenericOpenGLRenderWindow* renderWindowPtr = renderWindow.Get();
|
||
|
||
// 当前视图模式(全局共享,切视图/勾选时据此重建内容)。默认二维地图。
|
||
auto viewMode = std::make_shared<ViewMode>(ViewMode::Map2D);
|
||
|
||
// 三维图层显隐(由「视图详情」浮层控制)+ 项目 CRS→WGS84(体素配准)。
|
||
auto showCurtain = std::make_shared<bool>(true); // 帘面,默认显示
|
||
auto showVoxel = std::make_shared<bool>(false); // 体素,默认关
|
||
auto showTerrain = std::make_shared<bool>(false); // 地形(DEM+影像),默认关
|
||
auto showSlice = std::make_shared<bool>(false); // dd_slice 交互切片,默认关
|
||
// 持久的切片 widget(挂 interactor,跨重建保活;rebuildCentral 据条件创建/拆除)。
|
||
auto slicePlane = std::make_shared<vtkSmartPointer<vtkImagePlaneWidget>>();
|
||
std::shared_ptr<geopro::core::CrsTransform> crs; // PROJ 失败→空→体素层无效(不崩)
|
||
try {
|
||
crs = std::make_shared<geopro::core::CrsTransform>(kProjectCrs, kWgs84);
|
||
} catch (const std::exception&) {
|
||
crs.reset();
|
||
}
|
||
|
||
auto* dockManager = new ads::CDockManager(&window);
|
||
window.setCentralWidget(dockManager);
|
||
|
||
// 中央容器:顶部「二维地图/三维视图」工具条 + 下方 QVTK 视图。
|
||
auto* centerWidget = new QWidget();
|
||
auto* centerLayout = new QVBoxLayout(centerWidget);
|
||
centerLayout->setContentsMargins(0, 0, 0, 0);
|
||
centerLayout->setSpacing(0);
|
||
|
||
// 工具条:「二维地图/三维视图」两个互斥可勾选 action。切换=按当前勾选集重建对应内容。默认二维地图。
|
||
auto* viewToolBar = new QToolBar();
|
||
auto* viewGroup = new QActionGroup(viewToolBar);
|
||
viewGroup->setExclusive(true);
|
||
auto* act2D = viewToolBar->addAction(QStringLiteral("二维地图"));
|
||
auto* act3D = viewToolBar->addAction(QStringLiteral("三维视图"));
|
||
act2D->setCheckable(true);
|
||
act3D->setCheckable(true);
|
||
viewGroup->addAction(act2D);
|
||
viewGroup->addAction(act3D);
|
||
act2D->setChecked(true); // 默认二维地图
|
||
centerLayout->addWidget(viewToolBar);
|
||
centerLayout->addWidget(vtkWidget, 1);
|
||
|
||
// ──「视图详情」图层浮层(对齐原型 3D 视图左上):浮在 QVTK 之上,控制三维图层显隐。
|
||
// 仅三维视图显示;含 帘面 / 体素 勾选(体素=两交叉测线散点配准 IDW 的派生层,正确归宿)。
|
||
auto* layerPanel = new QFrame(centerWidget);
|
||
layerPanel->setFrameShape(QFrame::StyledPanel);
|
||
layerPanel->setStyleSheet(
|
||
QStringLiteral("QFrame{background:rgba(255,255,255,0.92);border:1px solid #b0b4bb;"
|
||
"border-radius:6px;} QCheckBox{padding:1px;}"));
|
||
auto* layerLayout = new QVBoxLayout(layerPanel);
|
||
layerLayout->setContentsMargins(10, 8, 12, 8);
|
||
layerLayout->setSpacing(4);
|
||
auto* layerTitle = new QLabel(QStringLiteral("视图详情"));
|
||
layerTitle->setStyleSheet(QStringLiteral("font-weight:bold;border:none;background:transparent;"));
|
||
auto* chkCurtain = new QCheckBox(QStringLiteral("帘面(断面墙)"));
|
||
chkCurtain->setChecked(true);
|
||
auto* chkVoxel = new QCheckBox(QStringLiteral("体素(dd_voxel)"));
|
||
chkVoxel->setChecked(false);
|
||
auto* chkTerrain = new QCheckBox(QStringLiteral("地形(DEM+影像)"));
|
||
chkTerrain->setChecked(false);
|
||
auto* chkSlice = new QCheckBox(QStringLiteral("切片(dd_slice)"));
|
||
chkSlice->setChecked(false);
|
||
if (!crs) { // PROJ 不可用 → 体素/切片/地形层(都需配准)禁用并提示
|
||
const QString tip = QStringLiteral("PROJ 数据(proj.db)缺失,配准不可用");
|
||
chkVoxel->setEnabled(false); chkVoxel->setToolTip(tip);
|
||
chkTerrain->setEnabled(false); chkTerrain->setToolTip(tip);
|
||
chkSlice->setEnabled(false); chkSlice->setToolTip(tip);
|
||
}
|
||
layerLayout->addWidget(layerTitle);
|
||
layerLayout->addWidget(chkCurtain);
|
||
layerLayout->addWidget(chkVoxel);
|
||
layerLayout->addWidget(chkSlice);
|
||
layerLayout->addWidget(chkTerrain);
|
||
layerPanel->setVisible(false); // 默认二维,不显示图层浮层
|
||
|
||
auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图"));
|
||
vtkDock->setWidget(centerWidget);
|
||
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
|
||
|
||
// ── 下方「数据详情」dock:独立 QVTK 小视图(独立 renderer/renderWindow)──
|
||
// 单击 DS → 显示该数据集平面反演剖面(#18 banded,平躺俯视正交)。
|
||
auto* detailWidget = new QVTKOpenGLStereoWidget();
|
||
vtkNew<vtkGenericOpenGLRenderWindow> detailRenderWindow;
|
||
vtkNew<vtkRenderer> detailRenderer;
|
||
detailRenderer->SetBackground(1.0, 1.0, 1.0); // 白底
|
||
detailWidget->setRenderWindow(detailRenderWindow);
|
||
detailRenderWindow->AddRenderer(detailRenderer);
|
||
vtkRenderer* detailRendererPtr = detailRenderer.Get();
|
||
vtkGenericOpenGLRenderWindow* detailRenderWindowPtr = detailRenderWindow.Get();
|
||
|
||
// 数据详情容器:顶部「反演剖面/原数据」工具条 + 下方 QVTK 小视图。
|
||
auto* detailContainer = new QWidget();
|
||
auto* detailLayout = new QVBoxLayout(detailContainer);
|
||
detailLayout->setContentsMargins(0, 0, 0, 0);
|
||
detailLayout->setSpacing(0);
|
||
|
||
// 工具条对齐原型:「原数据 | 网格数据」互斥 +「显示异常」开关。
|
||
auto* detailToolBar = new QToolBar();
|
||
auto* detailGroup = new QActionGroup(detailToolBar);
|
||
detailGroup->setExclusive(true);
|
||
auto* actScatter = detailToolBar->addAction(QStringLiteral("原数据"));
|
||
auto* actSection = detailToolBar->addAction(QStringLiteral("网格数据"));
|
||
actScatter->setCheckable(true);
|
||
actSection->setCheckable(true);
|
||
detailGroup->addAction(actScatter);
|
||
detailGroup->addAction(actSection);
|
||
actSection->setChecked(true); // 默认网格数据 (#18)
|
||
detailToolBar->addSeparator();
|
||
auto* actShowAnomaly = detailToolBar->addAction(QStringLiteral("显示异常"));
|
||
actShowAnomaly->setCheckable(true);
|
||
actShowAnomaly->setChecked(true); // 默认显示异常(对齐原型 ☑显示异常)
|
||
auto* actShowElectrodes = detailToolBar->addAction(QStringLiteral("显示电极"));
|
||
actShowElectrodes->setCheckable(true);
|
||
actShowElectrodes->setChecked(true); // 默认显示电极 ▼(对齐原型)
|
||
auto* actShowContour = detailToolBar->addAction(QStringLiteral("显示等值线"));
|
||
actShowContour->setCheckable(true);
|
||
actShowContour->setChecked(true); // 默认显示等值线(对齐原型)
|
||
detailLayout->addWidget(detailToolBar);
|
||
detailLayout->addWidget(detailWidget, 1);
|
||
|
||
auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情"));
|
||
detailDock->setWidget(detailContainer);
|
||
// 放在中央视图下方。
|
||
dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea);
|
||
|
||
// 项目结构(GS→TM→DS):取一次共享,供树/中央/数据列表查 TM 的数据集。
|
||
auto structure = std::make_shared<std::vector<geopro::data::GsNode>>(repo.loadStructure());
|
||
|
||
// 左上 dock:对象树(GS→TM,测线复选)。
|
||
auto* tree = new QTreeWidget();
|
||
tree->setHeaderLabel(QStringLiteral("对象显示栏"));
|
||
populateTree(tree, *structure);
|
||
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象显示栏"));
|
||
leftDock->setWidget(tree);
|
||
auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
|
||
|
||
// 左下 dock:数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
|
||
auto* datasetTabs = new QTabWidget();
|
||
auto* datasetList = new QListWidget();
|
||
datasetList->setAlternatingRowColors(true);
|
||
datasetTabs->addTab(datasetList, QStringLiteral("数据"));
|
||
auto* fileList = new QListWidget(); // M1 文件 tab 占位
|
||
datasetTabs->addTab(fileList, QStringLiteral("文件"));
|
||
auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏"));
|
||
datasetDock->setWidget(datasetTabs);
|
||
dockManager->addDockWidget(ads::BottomDockWidgetArea, datasetDock, leftArea);
|
||
|
||
// 右上 dock:异常列表(对齐原型;颜色块 + 名称 + 位置/深/尺寸 + 勾选显隐,与数据详情异常联动)。
|
||
auto* anomalyList = new QListWidget();
|
||
anomalyList->setAlternatingRowColors(true);
|
||
auto* anomalyDock = new ads::CDockWidget(QStringLiteral("异常列表"));
|
||
anomalyDock->setWidget(anomalyList);
|
||
auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, anomalyDock);
|
||
|
||
// 右下 dock:属性。
|
||
auto* propLabel = new QLabel(QStringLiteral("(单击左侧数据集查看属性与平面剖面)"));
|
||
propLabel->setWordWrap(true);
|
||
propLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||
propLabel->setMargin(8);
|
||
auto* propDock = new ads::CDockWidget(QStringLiteral("属性"));
|
||
propDock->setWidget(propLabel);
|
||
dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea);
|
||
|
||
// ── 中央视图重建(核心)─────────────────────────────────────────────
|
||
// 按勾选的测线(TM)整体重建:scene.clear() → 对每个勾选 TM 的 dd_section 加对应 actor。
|
||
// 二维地图 = buildSurveyLine(红线俯视,浅底背景)+ applyTop2D。
|
||
// 三维视图 = buildCurtain(断面墙)SetScale(1,1,kCurtainZScale) + applyFree3D(白底)。
|
||
// frame/structure 全局共享;切视图/勾选变化都调用此函数重建当前视图。
|
||
auto rebuildCentral = [scene, rendererPtr, renderWindowPtr, viewMode, &repo, frame, tree,
|
||
structure, showCurtain, showVoxel, showTerrain, showSlice, slicePlane,
|
||
crs, refElev]() {
|
||
// 先拆除上次的切片 widget(独立于 scene actor,须显式关闭),再按条件重建。
|
||
if (*slicePlane) { (*slicePlane)->Off(); *slicePlane = nullptr; }
|
||
scene->clear();
|
||
|
||
const bool is2D = (*viewMode == ViewMode::Map2D);
|
||
rendererPtr->SetBackground(is2D ? 0.96 : 1.0, is2D ? 0.97 : 1.0, is2D ? 0.99 : 1.0);
|
||
|
||
// 渲染单个 dd_section 数据集:二维=测线线;三维=帘面(受「帘面」图层开关控制)。
|
||
auto renderSection = [&](const std::string& id) {
|
||
const auto g = repo.loadGrid(id);
|
||
if (is2D) {
|
||
auto line = geopro::render::buildSurveyLine(g, *frame);
|
||
if (line) scene->addActor(line);
|
||
} else if (*showCurtain) {
|
||
const auto cs = repo.loadColorScale(id);
|
||
auto curtain = geopro::render::buildCurtain(g, cs, *frame);
|
||
if (curtain) {
|
||
curtain->SetScale(1.0, 1.0, kCurtainZScale); // 纵向夸张成墙
|
||
scene->addActor(curtain);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 遍历对象树收集所有勾选的测线(TM),渲染其 dd_section 数据集(可多条共存)。
|
||
QList<QTreeWidgetItem*> stack;
|
||
for (int i = 0; i < tree->topLevelItemCount(); ++i) stack.append(tree->topLevelItem(i));
|
||
while (!stack.isEmpty()) {
|
||
QTreeWidgetItem* cur = stack.takeFirst();
|
||
for (int i = 0; i < cur->childCount(); ++i) stack.append(cur->child(i));
|
||
|
||
const QString tmId = cur->data(0, kRoleTmId).toString();
|
||
if (tmId.isEmpty()) continue; // GS 节点忽略
|
||
if (cur->checkState(0) != Qt::Checked) continue; // 仅显示勾选的测线
|
||
const auto* tm = findTm(*structure, tmId.toStdString());
|
||
if (!tm) continue;
|
||
for (const auto& ds : tm->dss)
|
||
if (ds.ddType == "dd_section") renderSection(ds.id);
|
||
}
|
||
|
||
// 三维「体素 / 切片」图层:两交叉测线散点经 CRS 配准 IDW 成体素。
|
||
// 体素=GPU 体绘制(与帘面同纵向夸张);切片=vtkImagePlaneWidget 在体素 image 上交互拖切面。
|
||
// 注:切片 widget 作用于 image 原始米坐标(无 actor 夸张),与夸张后的体绘制存在纵向比例差
|
||
// (spec M-3 Z 基准统一待办);切片本身演示 dd_slice 交互正确。
|
||
if (!is2D && (*showVoxel || *showSlice) && crs) {
|
||
const auto profs = repo.loadVoxelScatters();
|
||
const auto vcs = repo.loadScatterColorScale("grid1");
|
||
auto vr = geopro::render::buildVoxelFromScatters(profs, vcs, *crs, *frame);
|
||
if (vr.valid()) {
|
||
if (*showVoxel) {
|
||
vr.volume->SetScale(1.0, 1.0, kCurtainZScale);
|
||
rendererPtr->AddVolume(vr.volume);
|
||
}
|
||
vtkRenderWindowInteractor* interactor = renderWindowPtr->GetInteractor();
|
||
if (*showSlice && interactor) {
|
||
const std::vector<double> stops = vcs.stopValues();
|
||
const double vmn = stops.size() >= 2 ? stops.front() : 0.0;
|
||
const double vmx = stops.size() >= 2 ? stops.back() : 1.0;
|
||
auto lut = geopro::render::buildLut(vcs, vmn, vmx, 256);
|
||
int dims[3] = {1, 1, 1};
|
||
vr.image->GetDimensions(dims);
|
||
auto plane = vtkSmartPointer<vtkImagePlaneWidget>::New();
|
||
plane->SetInteractor(interactor);
|
||
plane->SetInputData(vr.image);
|
||
plane->SetPlaneOrientationToXAxes();
|
||
plane->SetSliceIndex(dims[0] / 2);
|
||
plane->SetLookupTable(lut);
|
||
plane->DisplayTextOn();
|
||
// 左键拖动=移动切面(默认左键是取值光标十字,不直观);中键仍可取值。
|
||
plane->SetLeftButtonAction(vtkImagePlaneWidget::VTK_SLICE_MOTION_ACTION);
|
||
plane->SetMiddleButtonAction(vtkImagePlaneWidget::VTK_CURSOR_ACTION);
|
||
plane->On();
|
||
*slicePlane = plane;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 三维「地形」图层:GDAL 读 DEM(高程)+影像(EPSG:3857),重投影到世界系,warp 面 + 纹理。
|
||
if (!is2D && *showTerrain && crs) {
|
||
// zOffset=refElev 使地形落在测线地表高程附近(不按绝对高程浮空);zScale=1 真实起伏。
|
||
auto terr = geopro::render::buildTerrain(repo.demPath(), repo.imagePath(), *frame,
|
||
refElev, 1.0);
|
||
if (terr) scene->addActor(terr);
|
||
}
|
||
|
||
if (is2D)
|
||
geopro::render::applyTop2D(rendererPtr);
|
||
else
|
||
geopro::render::applyFree3D(rendererPtr);
|
||
rendererPtr->ResetCamera();
|
||
renderWindowPtr->Render();
|
||
};
|
||
|
||
// 勾选/取消某测线(TM) → 重建当前视图内容(勾的才显示;可多条共存)。
|
||
QObject::connect(tree, &QTreeWidget::itemChanged, tree,
|
||
[rebuildCentral](QTreeWidgetItem* item, int) {
|
||
if (item->data(0, kRoleTmId).toString().isEmpty()) return; // GS 忽略
|
||
rebuildCentral();
|
||
});
|
||
|
||
// 单击测线(TM) → 左下数据列表填充其采集批次(数据集)。
|
||
QObject::connect(tree, &QTreeWidget::itemClicked, tree,
|
||
[structure, datasetList](QTreeWidgetItem* item, int) {
|
||
const QString tmId = item->data(0, kRoleTmId).toString();
|
||
if (tmId.isEmpty()) return; // GS 节点无数据集
|
||
const auto* tm = findTm(*structure, tmId.toStdString());
|
||
if (tm) geopro::app::populateDatasetList(datasetList, tm->dss);
|
||
});
|
||
|
||
// ── 数据详情共享状态 + 重建 ──────────────────────────────────────────
|
||
// 当前选中数据集 id(空=未选)与详情显示模式(反演剖面/原数据);切模式或换选中都重建。
|
||
auto currentDsId = std::make_shared<QString>();
|
||
auto detailMode = std::make_shared<DetailMode>(DetailMode::Section18);
|
||
auto showAnomalies = std::make_shared<bool>(true); // 默认显示异常(对齐原型)
|
||
auto showElectrodes = std::make_shared<bool>(true); // 默认显示电极 ▼
|
||
auto showContour = std::make_shared<bool>(true); // 默认显示等值线
|
||
auto hiddenAnoms = std::make_shared<std::set<int>>(); // 异常列表中被取消勾选(隐藏)的异常下标
|
||
|
||
// 按当前选中 DS + 详情模式重建下方数据详情(平躺俯视正交,纵向夸张填面板)。
|
||
// 勾选「显示异常/电极/等值线」控制对应叠加(同纵向夸张对齐)。
|
||
auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, currentDsId, detailMode,
|
||
showAnomalies, showElectrodes, showContour, hiddenAnoms]() {
|
||
detailRendererPtr->RemoveAllViewProps();
|
||
if (currentDsId->isEmpty()) { // 未选数据集:清空即可
|
||
detailRenderWindowPtr->Render();
|
||
return;
|
||
}
|
||
const std::string id = currentDsId->toStdString();
|
||
if (*detailMode == DetailMode::Section18) {
|
||
// 网格数据:#18 banded 等值面(+「显示等值线」时叠黑色等值线),纵向夸张 1.5x(沿 y)。
|
||
const auto g = repo.loadGrid(id);
|
||
const auto cs = repo.loadColorScale(id);
|
||
const auto actors = geopro::render::buildGridContour(g, cs);
|
||
if (actors.bands) {
|
||
actors.bands->SetScale(1.0, kDetailYScale, 1.0);
|
||
detailRendererPtr->AddViewProp(actors.bands);
|
||
}
|
||
if (actors.edges && *showContour) {
|
||
actors.edges->SetScale(1.0, kDetailYScale, 1.0);
|
||
detailRendererPtr->AddViewProp(actors.edges);
|
||
}
|
||
// 顶部电极标记 ▼(仅网格数据;同纵向夸张对齐)。
|
||
if (*showElectrodes) {
|
||
auto elec = geopro::render::buildElectrodes(g);
|
||
if (elec) {
|
||
elec->SetScale(1.0, kDetailYScale, 1.0);
|
||
detailRendererPtr->AddViewProp(elec);
|
||
}
|
||
}
|
||
} else {
|
||
// 原数据:#17 彩色散点,用散点自带色阶;纵向夸张同剖面以对齐观感。
|
||
const auto s = repo.loadScatter(id);
|
||
const auto scs = repo.loadScatterColorScale(id);
|
||
auto a = geopro::render::buildScatter(s, scs, kScatterPointSize);
|
||
if (a) {
|
||
a->SetScale(1.0, kDetailYScale, 1.0);
|
||
detailRendererPtr->AddViewProp(a);
|
||
}
|
||
}
|
||
// 异常叠加(与剖面同坐标系/同纵向夸张)。逐异常构建以按列表显隐(下标=原 vector 序)过滤。
|
||
if (*showAnomalies) {
|
||
const auto anomalies = repo.loadAnomalies(id);
|
||
for (int i = 0; i < static_cast<int>(anomalies.size()); ++i) {
|
||
if (hiddenAnoms->count(i)) continue; // 列表中取消勾选→隐藏
|
||
for (auto& act : geopro::render::buildAnomalies({anomalies[i]})) {
|
||
act->SetScale(1.0, kDetailYScale, 1.0);
|
||
detailRendererPtr->AddViewProp(act);
|
||
}
|
||
}
|
||
}
|
||
geopro::render::applyTop2D(detailRendererPtr);
|
||
detailRendererPtr->ResetCamera();
|
||
detailRenderWindowPtr->Render();
|
||
};
|
||
|
||
// 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。
|
||
auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyList, hiddenAnoms](
|
||
const QString& dsId, const QString& name) {
|
||
if (dsId.isEmpty()) return;
|
||
*currentDsId = dsId;
|
||
|
||
// 右上异常列表:按该数据集异常重填(默认全显);先清隐藏集再填,避免重建时阻塞信号回灌。
|
||
const auto anomalies = repo.loadAnomalies(dsId.toStdString());
|
||
hiddenAnoms->clear();
|
||
{
|
||
const QSignalBlocker block(anomalyList); // 重填触发 itemChanged,先屏蔽
|
||
geopro::app::populateAnomalyList(anomalyList, anomalies);
|
||
}
|
||
|
||
rebuildDetail();
|
||
|
||
// 右下属性(数据集级,与详情模式无关)。
|
||
const auto g = repo.loadGrid(dsId.toStdString());
|
||
propLabel->setText(
|
||
QStringLiteral("数据集: %1\n类型: 剖面网格 (dd_section)\n网格: %2 x %3\n"
|
||
"vmin / vmax: %4 / %5\n异常: %6 个")
|
||
.arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax)
|
||
.arg(anomalies.size()));
|
||
};
|
||
|
||
// ── 单击左下数据列表的采集批次(DS) → 加载到数据详情/异常/属性 ──
|
||
QObject::connect(datasetList, &QListWidget::itemClicked, datasetList,
|
||
[loadDataset](QListWidgetItem* item) {
|
||
const QString dsId = item->data(geopro::app::kDsIdRole).toString();
|
||
const QString ddType = item->data(geopro::app::kDsDdTypeRole).toString();
|
||
if (ddType != "dd_section") return; // 仅剖面网格有详情图
|
||
const QString name =
|
||
item->data(Qt::DisplayRole).toString().section('\n', 0, 0);
|
||
loadDataset(dsId, name);
|
||
});
|
||
|
||
// ── 异常列表勾选(显隐) → 更新隐藏集 → 重建数据详情 ──
|
||
QObject::connect(anomalyList, &QListWidget::itemChanged, anomalyList,
|
||
[hiddenAnoms, rebuildDetail](QListWidgetItem* item) {
|
||
const int idx = item->data(geopro::app::kAnomalyIndexRole).toInt();
|
||
if (item->checkState() == Qt::Checked)
|
||
hiddenAnoms->erase(idx);
|
||
else
|
||
hiddenAnoms->insert(idx);
|
||
rebuildDetail();
|
||
});
|
||
|
||
// ── 数据详情工具条「反演剖面/原数据」:切模式 → 重建数据详情 ──
|
||
QObject::connect(actSection, &QAction::triggered, detailWidget,
|
||
[detailMode, rebuildDetail]() {
|
||
*detailMode = DetailMode::Section18;
|
||
rebuildDetail();
|
||
});
|
||
QObject::connect(actScatter, &QAction::triggered, detailWidget,
|
||
[detailMode, rebuildDetail]() {
|
||
*detailMode = DetailMode::Scatter17;
|
||
rebuildDetail();
|
||
});
|
||
|
||
// ──「显示异常 / 显示电极 / 显示等值线」开关:切换叠加 → 重建数据详情 ──
|
||
QObject::connect(actShowAnomaly, &QAction::toggled, detailWidget,
|
||
[showAnomalies, rebuildDetail](bool on) {
|
||
*showAnomalies = on;
|
||
rebuildDetail();
|
||
});
|
||
QObject::connect(actShowElectrodes, &QAction::toggled, detailWidget,
|
||
[showElectrodes, rebuildDetail](bool on) {
|
||
*showElectrodes = on;
|
||
rebuildDetail();
|
||
});
|
||
QObject::connect(actShowContour, &QAction::toggled, detailWidget,
|
||
[showContour, rebuildDetail](bool on) {
|
||
*showContour = on;
|
||
rebuildDetail();
|
||
});
|
||
|
||
// 「视图详情」浮层显隐:仅三维显示,置于 QVTK 左上(工具条下方)并置顶。
|
||
auto showLayerPanel = [layerPanel, viewToolBar](bool show3D) {
|
||
if (show3D) {
|
||
layerPanel->move(14, viewToolBar->height() + 12);
|
||
layerPanel->adjustSize();
|
||
layerPanel->setVisible(true);
|
||
layerPanel->raise();
|
||
} else {
|
||
layerPanel->setVisible(false);
|
||
}
|
||
};
|
||
|
||
// ── 工具条「二维地图/三维视图」:切换互斥视图 → 重建内容 + 图层浮层显隐 ──
|
||
QObject::connect(act2D, &QAction::triggered, vtkWidget,
|
||
[viewMode, rebuildCentral, showLayerPanel]() {
|
||
*viewMode = ViewMode::Map2D;
|
||
showLayerPanel(false);
|
||
rebuildCentral();
|
||
});
|
||
QObject::connect(act3D, &QAction::triggered, vtkWidget,
|
||
[viewMode, rebuildCentral, showLayerPanel]() {
|
||
*viewMode = ViewMode::View3D;
|
||
showLayerPanel(true);
|
||
rebuildCentral();
|
||
});
|
||
|
||
// ──「视图详情」图层勾选 → 更新图层显隐 → 重建中央 ──
|
||
QObject::connect(chkCurtain, &QCheckBox::toggled, vtkWidget,
|
||
[showCurtain, rebuildCentral](bool on) {
|
||
*showCurtain = on;
|
||
rebuildCentral();
|
||
});
|
||
QObject::connect(chkVoxel, &QCheckBox::toggled, vtkWidget,
|
||
[showVoxel, rebuildCentral](bool on) {
|
||
*showVoxel = on;
|
||
rebuildCentral();
|
||
});
|
||
QObject::connect(chkTerrain, &QCheckBox::toggled, vtkWidget,
|
||
[showTerrain, rebuildCentral](bool on) {
|
||
*showTerrain = on;
|
||
rebuildCentral();
|
||
});
|
||
QObject::connect(chkSlice, &QCheckBox::toggled, vtkWidget,
|
||
[showSlice, rebuildCentral](bool on) {
|
||
*showSlice = on;
|
||
rebuildCentral();
|
||
});
|
||
|
||
// ── 启动默认:测线已勾选,但 itemChanged 在 connect 之前触发故未渲染;这里重建一次中央内容。
|
||
rebuildCentral();
|
||
|
||
// 启动默认:选第一个含 dd_section 的测线 → 填充数据列表 + 加载其首个 dd_section 详情(对齐原型)。
|
||
for (const auto& gs : *structure) {
|
||
const geopro::data::TmNode* picked = nullptr;
|
||
for (const auto& tm : gs.tms) {
|
||
const bool hasSection =
|
||
std::any_of(tm.dss.begin(), tm.dss.end(),
|
||
[](const geopro::data::DsNode& d) { return d.ddType == "dd_section"; });
|
||
if (hasSection) { picked = &tm; break; }
|
||
}
|
||
if (!picked) continue;
|
||
geopro::app::populateDatasetList(datasetList, picked->dss);
|
||
for (const auto& ds : picked->dss)
|
||
if (ds.ddType == "dd_section") {
|
||
loadDataset(QString::fromStdString(ds.id), QString::fromStdString(ds.name));
|
||
break;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
} // namespace
|
||
|
||
int main(int argc, char* argv[])
|
||
{
|
||
// QVTK 默认 surface format 必须在 QApplication 之前设置(全局一次,两个 QVTK widget 共用)。
|
||
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
|
||
QApplication app(argc, argv);
|
||
|
||
// PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量;
|
||
// 否则按 exe 旁 / 构建目录候选设置。部署时须随包附带 proj 数据并设此变量。
|
||
if (qEnvironmentVariableIsEmpty("PROJ_DATA")) {
|
||
const QString appDir = QCoreApplication::applicationDirPath();
|
||
const QStringList candidates = {
|
||
appDir + "/proj",
|
||
appDir + "/../../vcpkg_installed/x64-windows/share/proj",
|
||
QStringLiteral(
|
||
"D:/Git/lanbingtech/geopro/build/release/vcpkg_installed/x64-windows/share/proj"),
|
||
};
|
||
for (const auto& c : candidates) {
|
||
if (QFile::exists(c + "/proj.db")) {
|
||
qputenv("PROJ_DATA", c.toUtf8());
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 网络层:共享会话 ApiClient + 登录编排 AuthService(RSA 公钥从 resources 读取)。
|
||
geopro::net::ApiClient api(QStringLiteral("http://tenant.geomative.cn/pop-api"));
|
||
const std::string pem = readPem("D:/Git/lanbingtech/geopro/resources/rsa_public_key.pem");
|
||
geopro::net::AuthService auth(api, pem);
|
||
|
||
// 先弹登录窗;用户取消/未登录则退出。
|
||
geopro::app::LoginWindow login(auth);
|
||
if (login.exec() != QDialog::Accepted) return 0;
|
||
|
||
api.setToken(login.token()); // 注入 token 供后续 API 使用
|
||
|
||
// 登录成功 → 构建并显示工作台。
|
||
geopro::data::LocalSampleRepository repo(
|
||
"D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/");
|
||
|
||
QMainWindow window;
|
||
window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"));
|
||
window.resize(1280, 800);
|
||
|
||
buildWorkbench(window, repo);
|
||
window.show();
|
||
|
||
return app.exec();
|
||
}
|