geopro/src/app/main.cpp

725 lines
36 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// M1 工作台(视图重构 Task B正确产品模型。
// - 左上 对象显示栏GS→TM(测线,复选框)。勾选测线 → 在中央显示其 dd_section可多条共存。
// - 左下 数据真实显示栏:单击测线 → 列其采集批次(数据集,tab 数据/文件)。单击采集批次 → 数据详情+异常+属性。
// - 中央「二维地图 / 三维视图」:两个互斥视图(内容不同,不是同一物体换相机)。
// 二维地图 = 对每个勾选数据集 buildSurveyLinelat/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 彩色散点buildScatterx=距离/y=深度,按散点自带色阶上色)。
// 显示异常 = 在上图叠加异常圈定buildAnomaliesdashed 折线,同纵向夸张对齐)。
// 两者皆平躺俯视正交 + 属性。
// - 右 属性:选中数据集属性文本。
// 世界系:启动 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;
// 从对象结构树构建 QTreeWidgetGS → 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 + 登录编排 AuthServiceRSA 公钥从 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();
}