geopro/src/app/main.cpp

857 lines
46 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 <initializer_list>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include <QActionGroup>
#include <QApplication>
#include <QCheckBox>
#include <QColor>
#include <QDialog>
#include <QEasingCurve>
#include <QEvent>
#include <QFile>
#include <QButtonGroup>
#include <QCheckBox>
#include <QFrame>
#include <QHBoxLayout>
#include <QGraphicsOpacityEffect>
#include <QLabel>
#include <QListWidget>
#include <QListWidgetItem>
#include <QToolButton>
#include <QKeySequence>
#include <QProcess>
#include <QSettings>
#include <QShortcut>
#include <QPropertyAnimation>
#include <QVariantAnimation>
#include <QStringList>
#include <QTabWidget>
#include <QMainWindow>
#include <QStatusBar>
#include <QStyle>
#include <QSurfaceFormat>
#include <QTimer>
#include <QToolBar>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QVBoxLayout>
#include <QWidget>
#include <DockAreaTitleBar.h>
#include <DockAreaWidget.h>
#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 "Credential.hpp"
#include "Glyphs.hpp"
#include "PanelHeader.hpp"
#include "Theme.hpp"
#include "SettingsDialog.hpp"
#include "TopBar.hpp"
#include "CentralScene.hpp"
#include "ProjectListDialog.hpp"
#include "WorkbenchNavController.hpp"
#include "DatasetDetailController.hpp"
#include "api/ApiProjectRepository.hpp"
#include "api/ApiDatasetRepository.hpp"
#include "panels/ObjectTreePanel.hpp"
#include "login/LoginWindow.hpp"
#include "panels/DatasetListPanel.hpp"
#include "panels/DatasetDetailPanel.hpp"
#include "panels/DynamicFormView.hpp"
#include "panels/ObjectExceptionPanel.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 <vtkActor.h>
#include <vtkCamera.h>
#include <vtkCameraInterpolator.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkImagePlaneWidget.h>
#include <vtkLookupTable.h>
#include <vtkProperty.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkSmartPointer.h>
namespace {
// 居中浮层定位器:监视 host中央 QVTK尺寸/显示变化,把 overlay 浮层
// (与 host 同父的兄弟控件)始终摆到 host 区域正中。用于中央“空状态”引导层。
// 仅外观,无业务逻辑;无信号槽故不需 Q_OBJECT/moc。
class CenterOverlay : public QObject {
public:
CenterOverlay(QWidget* overlay, QWidget* host)
: QObject(host), overlay_(overlay), host_(host)
{
host_->installEventFilter(this);
}
void reposition()
{
overlay_->adjustSize();
const QSize h = host_->size();
const QSize o = overlay_->size();
overlay_->move(host_->x() + (h.width() - o.width()) / 2,
host_->y() + (h.height() - o.height()) / 2);
overlay_->raise();
}
protected:
bool eventFilter(QObject* obj, QEvent* e) override
{
if (obj == host_ && (e->type() == QEvent::Resize || e->type() == QEvent::Show))
reposition();
return QObject::eventFilter(obj, e);
}
private:
QWidget* overlay_;
QWidget* host_;
};
// 读取 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]);
}
// 当前中央视图(默认二维地图)。二维地图=测线红线俯视;三维视图=断面墙。
using geopro::app::ViewMode;
// 纵向夸张倍数Z 基准统一M-3全项目共用同一倍数使 帘面(z) / 体素 / 切片 /
// 数据详情剖面(y) / 地形(relief) 的纵向比例一致——避免「剖面×1.5、帘面×3」不一致。
// 单一可调常量:要整体调纵向观感改这一处即可。
constexpr double kVerticalExaggeration = 2.0;
// 项目 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,
geopro::data::IProjectRepository& projectRepo,
geopro::controller::WorkbenchNavController& nav,
geopro::controller::DatasetDetailController& detailCtrl)
{
// ── 世界系:启动取一次 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();
}
// 停靠系统配置(必须在 CDockManager 构造前设置):对齐原型——面板固定、
// 标题栏不显示「关闭 / 浮动 / 标签菜单」等子窗口操作按钮,并关闭自动隐藏(钉住)。
ads::CDockManager::setConfigFlags(ads::CDockManager::DefaultOpaqueConfig);
ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasCloseButton, false);
ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasUndockButton, false);
ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasTabsMenuButton, false);
ads::CDockManager::setConfigFlag(ads::CDockManager::ActiveTabHasCloseButton, false);
ads::CDockManager::setConfigFlag(ads::CDockManager::AlwaysShowTabs, true); // 单面板也显示标题头
ads::CDockManager::setConfigFlag(ads::CDockManager::FocusHighlighting, false);
ads::CDockManager::setAutoHideConfigFlags(ads::CDockManager::AutoHideFlags()); // 禁用自动隐藏
auto* dockManager = new ads::CDockManager(&window);
window.setCentralWidget(dockManager);
// 覆盖 ADS 自带分隔条样式:其 default.css 用 palette(dark) 把面板间分隔画成深灰粗线,
// 且设在管理器自身、选择器更具体(优先级高于全局主题)。在其后追加同选择器、按主题着色的极淡分隔覆盖。
// 捕获 ADS 基样式一次(避免每次切换重复追加而无限增长),切主题时用 base + 重新着色的覆盖。
const QString dockBaseQss = dockManager->styleSheet();
auto applyDockSplitter = [dockManager, dockBaseQss]() {
dockManager->setStyleSheet(
dockBaseQss +
geopro::app::fillTokens(QStringLiteral(
"ads--CDockContainerWidget ads--CDockSplitter::handle { background: {{divider}}; }"
"ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: {{accent/primary}}; }")));
};
applyDockSplitter();
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed,
dockManager, [applyDockSplitter]() { applyDockSplitter(); });
// 面板包装:内容顶部加自绘表头(图标+标题+操作按钮ADS 自带标题栏随后隐藏,
// 从而让面板表头与原型完全一致。返回 [表头 + 内容] 容器供 setWidget。
auto wrapWithHeader = [](geopro::app::Glyph icon, const QString& title, QWidget* content,
const QVector<geopro::app::HeaderAction>& actions = {}) {
auto* box = new QWidget();
auto* v = new QVBoxLayout(box);
v->setContentsMargins(0, 0, 0, 0);
v->setSpacing(0);
v->addWidget(geopro::app::buildPanelHeader(icon, title, actions));
v->addWidget(content, 1);
return box;
};
// 中央容器:顶部「二维地图/三维视图」工具条 + 下方 QVTK 视图。
auto* centerWidget = new QWidget();
auto* centerLayout = new QVBoxLayout(centerWidget);
centerLayout->setContentsMargins(0, 0, 0, 0);
centerLayout->setSpacing(0);
// 「二维地图/三维视图」分段切换表头:与「异常/属性」面板表头同款42px 表头底 + 强调色下划线页签)。
auto seg = geopro::app::buildSegmentedHeader(
{QStringLiteral("二维地图"), QStringLiteral("三维视图")},
{{geopro::app::Glyph::Collapse, QStringLiteral("折叠")},
{geopro::app::Glyph::Download, QStringLiteral("导出")}});
auto* viewHeader = seg.header;
auto* act2D = seg.buttons[0];
auto* act3D = seg.buttons[1];
centerLayout->addWidget(viewHeader);
centerLayout->addWidget(vtkWidget, 1);
// ──「视图详情」图层浮层(对齐原型 3D 视图左上):浮在 QVTK 之上,控制三维图层显隐。
// 仅三维视图显示;含 帘面 / 体素 勾选(体素=两交叉测线散点配准 IDW 的派生层,正确归宿)。
auto* layerPanel = new QFrame(centerWidget);
layerPanel->setFrameShape(QFrame::StyledPanel);
geopro::app::applyTokenizedStyleSheet(
layerPanel,
// 不设 border-radius浮层是 centerWidget 的子控件,悬于原生 GL 画布上,圆角四角处会
// 露出父级浅底(浅色模式下即四个白直角)。改为直角矩形,不透明底铺满整块,无四角伪影。
QStringLiteral("QFrame{background:{{canvas/bg-soft}};border:1px solid {{canvas/grid}};}"
"QCheckBox{padding:2px 1px;color:{{canvas/text}};}"
"QCheckBox:disabled{color:{{canvas/text-dim}};}"));
auto* layerLayout = new QVBoxLayout(layerPanel);
// 浮层内边距取间距令牌:左右 lg(12)、上下 ml(10),对称(原 13/10/15/11 是手调奇数)。
layerLayout->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kMl,
geopro::app::space::kLg, geopro::app::space::kMl);
layerLayout->setSpacing(geopro::app::space::kSm);
auto* layerTitle = new QLabel(QStringLiteral("视图详情"));
geopro::app::applyTokenizedStyleSheet(
layerTitle, QStringLiteral("font-weight:%1;color:{{canvas/text}};border:none;background:transparent;"
"padding-bottom:3px;font-size:%2px;")
.arg(geopro::app::type::kWeightSemibold)
.arg(geopro::app::scaledPx(geopro::app::type::kTitle)));
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);
}
// 本轮中央不接真实派生层:体素/切片/地形勾选置灰,待下一轮接入对应数据源。
for (QCheckBox* c : {chkVoxel, chkSlice, chkTerrain}) {
c->setEnabled(false);
c->setToolTip(QStringLiteral("(下一轮接入真实数据源)"));
}
layerLayout->addWidget(layerTitle);
layerLayout->addWidget(chkCurtain);
layerLayout->addWidget(chkVoxel);
layerLayout->addWidget(chkSlice);
layerLayout->addWidget(chkTerrain);
layerPanel->setVisible(false); // 默认二维,不显示图层浮层
// ── 中央“空状态”引导浮层:未接入真实 sections 时,引导首次使用者从左侧入手。──
// 透明背景 + 鼠标穿透(不挡 QVTK 交互CenterOverlay 随视口尺寸保持居中;
// 接入真实中央数据后改成依 sections 是否为空调 setVisible 即可。
auto* emptyState = new QFrame(centerWidget);
emptyState->setObjectName(QStringLiteral("centralEmpty"));
emptyState->setAttribute(Qt::WA_TransparentForMouseEvents);
// 背景取 canvas/bg(#0B1320)——与画布同色:在原生 GL 上覆盖透明无法生效(会回退成不透明浅底),
// 故用与画布等色的不透明底,卡片即「无缝隐形」,浅色提示字稳稳浮于深底(与左上视图详情浮层同法)。
geopro::app::applyTokenizedStyleSheet(
emptyState, QStringLiteral("#centralEmpty { background: {{canvas/bg}}; }"
"#centralEmpty QLabel { background: transparent; }"));
auto* esLay = new QVBoxLayout(emptyState);
esLay->setContentsMargins(geopro::app::space::kXl, geopro::app::space::kXl,
geopro::app::space::kXl, geopro::app::space::kXl);
esLay->setSpacing(geopro::app::space::kMd);
esLay->setAlignment(Qt::AlignCenter);
auto* esIcon = new QLabel(emptyState);
esIcon->setPixmap(
geopro::app::makeGlyph(geopro::app::Glyph::Dataset,
geopro::app::tokenColor("canvas/text-dim"), 56)
.pixmap(56, 56));
esIcon->setAlignment(Qt::AlignCenter);
auto* esTitle = new QLabel(QStringLiteral("选择左侧数据集开始分析"), emptyState);
esTitle->setAlignment(Qt::AlignCenter);
geopro::app::applyTokenizedStyleSheet(
esTitle, QStringLiteral("color:{{canvas/text}}; font-size:%1px; font-weight:%2;")
.arg(geopro::app::scaledPx(geopro::app::type::kHeading))
.arg(geopro::app::type::kWeightSemibold));
auto* esHint = new QLabel(QStringLiteral("单击左侧采集批次,查看反演剖面与异常点\n"
"切到「三维视图」可叠加帘面、体素与地形图层"),
emptyState);
esHint->setAlignment(Qt::AlignCenter);
geopro::app::applyTokenizedStyleSheet(
esHint,
QStringLiteral("color:{{canvas/text-dim}}; font-size:%1px;").arg(geopro::app::scaledPx(geopro::app::type::kBody)));
esLay->addWidget(esIcon);
esLay->addWidget(esTitle);
esLay->addWidget(esHint);
auto* emptyCentering = new CenterOverlay(emptyState, vtkWidget);
emptyCentering->reposition();
auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图"));
vtkDock->setWidget(centerWidget);
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
// ── 下方「数据详情」dock平面图表多 Tab 面板QGraphicsViewVTK 仅算几何)──
// 单击数据集 → 聚焦已开页;双击 → 新建/聚焦页(真实反演剖面/散点/异常/色阶)。
auto* detailPanel = new geopro::app::DatasetDetailPanel();
auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情"));
detailDock->setWidget(wrapWithHeader(
geopro::app::Glyph::Detail, QStringLiteral("数据详情"), detailPanel));
// 放在中央视图下方。
dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea);
// 左上 dock对象树真实结构项目根 → GS → TM。被动视图数据由控制器推送。
auto* objectTree = new geopro::app::ObjectTreePanel();
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象"));
leftDock->setWidget(wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象"),
objectTree,
{{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}}));
auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
// 左下 dock数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
auto* datasetTabs = new QTabWidget();
auto* datasetList = new QListWidget();
geopro::app::applyDatasetCardDelegate(datasetList);
datasetTabs->addTab(datasetList, QStringLiteral("数据"));
auto* fileList = new QListWidget();
geopro::app::applyDatasetCardDelegate(fileList);
datasetTabs->addTab(fileList, QStringLiteral("文件"));
auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据集"));
auto* datasetBox = wrapWithHeader(
geopro::app::Glyph::Dataset, QStringLiteral("数据集"), datasetTabs,
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
{geopro::app::Glyph::Upload, QStringLiteral("上传")}});
datasetDock->setWidget(datasetBox);
// 动态标题:选中测线后改为「数据集显示栏 · ERTx」(对齐原型)。
auto* datasetTitle = datasetBox->findChild<QLabel*>(QStringLiteral("panelTitle"));
dockManager->addDockWidget(ads::BottomDockWidgetArea, datasetDock, leftArea);
// 右上 dock异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。
auto* exceptionPanel = new geopro::app::ObjectExceptionPanel();
auto* objAttrView = new geopro::app::DynamicFormView();
auto anomalyPanel = geopro::app::buildTabbedPanel(
{{geopro::app::Glyph::Anomaly, QStringLiteral("对象异常"), exceptionPanel, true},
{geopro::app::Glyph::Property, QStringLiteral("对象属性"), objAttrView, false}},
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
{geopro::app::Glyph::Plus, QStringLiteral("添加异常")}});
auto* anomalyBadge = anomalyPanel.badges.value(0); // 异常列表 Tab 的数量徽标
// colorize(C):异常计数用语义 warning“需注意”变体区别于普通中性计数徽标
// 提示“这些异常点待复查”。改 objectName 后重新 polish 以应用 #panelBadgeWarn 样式。
// 注:徽标的填充/显隐由 exceptionTreeLoaded 连接驱动(勾选对象后按异常计数更新)。
if (anomalyBadge) {
anomalyBadge->setObjectName(QStringLiteral("panelBadgeWarn"));
anomalyBadge->style()->unpolish(anomalyBadge);
anomalyBadge->style()->polish(anomalyBadge);
}
auto* rightDock = new ads::CDockWidget(QStringLiteral("异常/对象属性"));
rightDock->setWidget(anomalyPanel.container);
auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock);
// 右下 dock属性数据集属性键值对齐原型下半独立面板
auto* propView = new geopro::app::DynamicFormView();
auto* propDock = new ads::CDockWidget(QStringLiteral("数据集属性"));
propDock->setWidget(
wrapWithHeader(geopro::app::Glyph::Property, QStringLiteral("数据集属性"), propView));
dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea);
// 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。
// 同时隐藏 ADS 自带标题栏——表头已由各面板内的自绘 PanelHeader 承担,避免“双标题”。
// 抽成 lambdaADS restoreState() 恢复布局时会重建停靠区并重新显示标题栏,
// 故须在恢复布局之后再调用一次,确保任何已保存布局下标题栏都稳定隐藏。
const auto hideDockTitleBars = [&]() {
for (ads::CDockWidget* d : {vtkDock, detailDock, leftDock, datasetDock, rightDock, propDock}) {
d->setFeatures(ads::CDockWidget::NoDockWidgetFeatures);
if (auto* area = d->dockAreaWidget())
if (auto* bar = area->titleBar()) bar->setVisible(false);
}
};
hideDockTitleBars();
// 中央编排已解耦到 CentralScene::rebuildCentralScene数据驱动。本轮空 sections → 空背景占位。
// 下一轮:用真实 DS 数据构建 sections 调同一 helper 即复活。
auto rebuildCentral = [scene, rendererPtr, renderWindowPtr, viewMode, showCurtain, frame]() {
geopro::app::rebuildCentralScene(*scene, rendererPtr, renderWindowPtr, *viewMode,
std::vector<geopro::app::SectionInput>{}, *showCurtain,
*frame, kVerticalExaggeration);
};
// ── 单击左下数据列表的采集批次(DS) → 属性表单 + 聚焦详情已开页 ──
QObject::connect(datasetList, &QListWidget::itemClicked, datasetList,
[&nav, &detailCtrl](QListWidgetItem* item) {
if (item->data(geopro::app::kDsLoadMoreRole).toBool()) {
nav.loadMoreData();
return;
}
const QString dsId = item->data(geopro::app::kDsIdRole).toString();
if (dsId.isEmpty()) return;
nav.selectDataset(dsId); // 属性表单(现状)
detailCtrl.focusDataset(dsId); // 单击=聚焦已开页
});
// ── 双击 → 打开/聚焦该数据集的详情图表页(拉真实反演剖面/散点/异常/色阶)──
QObject::connect(datasetList, &QListWidget::itemDoubleClicked, datasetList,
[&detailCtrl](QListWidgetItem* item) {
const QString dsId = item->data(geopro::app::kDsIdRole).toString();
const QString ddCode = item->data(geopro::app::kDsDdCodeRole).toString();
if (!dsId.isEmpty()) detailCtrl.openDataset(dsId, ddCode);
});
// ── 控制器信号 → 详情面板:数据就绪开页 / 聚焦请求 ──
QObject::connect(
&detailCtrl, &geopro::controller::DatasetDetailController::chartReady, detailPanel,
[detailPanel](const geopro::controller::DatasetDetailController::ChartData& d) {
detailPanel->openOrUpdate(d);
});
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::focusRequested,
detailPanel, [detailPanel](const QString& dsId) {
detailPanel->focusDataset(dsId);
});
// ── 详情面板切 Tab → 反向高亮数据集列表对应行 ──
QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::activeDatasetChanged,
datasetList, [datasetList](const QString& dsId) {
for (int i = 0; i < datasetList->count(); ++i)
if (datasetList->item(i)->data(geopro::app::kDsIdRole).toString() ==
dsId)
datasetList->setCurrentRow(i);
});
// 「视图详情」浮层显隐:仅三维显示,置于 QVTK 左上(工具条下方)并置顶。
auto showLayerPanel = [layerPanel, viewHeader](bool show3D) {
if (show3D) {
layerPanel->move(14, viewHeader->height() + 12);
layerPanel->adjustSize();
layerPanel->setVisible(true);
layerPanel->raise();
} else {
layerPanel->setVisible(false);
}
};
// ── 工具条「二维地图/三维视图」:切换互斥视图 → 重建内容 + 图层浮层显隐 ──
QObject::connect(act2D, &QAbstractButton::clicked, vtkWidget,
[viewMode, rebuildCentral, showLayerPanel]() {
*viewMode = ViewMode::Map2D;
showLayerPanel(false);
rebuildCentral();
});
QObject::connect(act3D, &QAbstractButton::clicked, 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();
});
// ── 启动:建立一次空背景中央视图(真实 sections 数据由下一轮接入)。
rebuildCentral();
// VTK 背景随主题切换:直接重跑 rebuildCentral走完整渲染路径、末尾必 Render
// 比手动 SetBackground+Render 稳;兼顾 syncSystemTheme 异步切暗的时序)。
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed, &window,
[rebuildCentral]() {
rebuildCentral();
});
// 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备),
// 下=工具条(工作空间切换 + 项目 + 帮助/通知/设置 + 用户)。纵向堆叠后挂到主窗口顶部。
geopro::app::TopBar* topBar = nullptr;
{
auto* topChrome = new QWidget(&window);
auto* topLayout = new QVBoxLayout(topChrome);
topLayout->setContentsMargins(0, 0, 0, 0);
topLayout->setSpacing(0);
topLayout->addWidget(geopro::app::buildMenuBar(topChrome));
topBar = new geopro::app::TopBar(topChrome);
topLayout->addWidget(topBar);
window.setMenuWidget(topChrome);
}
// ── 控制器 ↔ UI 信号接线(导航壳)──────────────────────────────────────
// "加载更多"行:列表末尾若已加载数 < 总数,放一行可点击的"加载更多(已/共)"。
auto removeLoadMore = [](QListWidget* lw) {
if (lw->count() > 0 &&
lw->item(lw->count() - 1)->data(geopro::app::kDsLoadMoreRole).toBool())
delete lw->takeItem(lw->count() - 1);
};
auto addLoadMore = [](QListWidget* lw, int total) {
const int loaded = lw->count();
if (loaded < total) {
auto* m = new QListWidgetItem(QStringLiteral("加载更多(%1/%2").arg(loaded).arg(total), lw);
m->setData(geopro::app::kDsLoadMoreRole, true);
m->setTextAlignment(Qt::AlignCenter);
}
return loaded;
};
QObject::connect(topBar, &geopro::app::TopBar::workspaceSwitchRequested, &nav,
&geopro::controller::WorkbenchNavController::switchWorkspace);
QObject::connect(topBar, &geopro::app::TopBar::projectSwitchRequested, &nav,
&geopro::controller::WorkbenchNavController::switchProject);
// 退出登录:清除记住的凭证(QtKeychain+QSettings) → 重启应用回到登录页。
QObject::connect(topBar, &geopro::app::TopBar::logoutRequested, &window, []() {
geopro::app::forgetSession();
QProcess::startDetached(QCoreApplication::applicationFilePath(),
QCoreApplication::arguments().mid(1));
qApp->quit();
});
// 设置:点齿轮 → 打开设置对话框(外观/关于)。
QObject::connect(topBar, &geopro::app::TopBar::settingsRequested, &window, [&window]() {
geopro::app::SettingsDialog dlg(&window);
dlg.exec();
});
QObject::connect(topBar, &geopro::app::TopBar::allProjectsRequested, &window,
[&projectRepo, &nav, topBar, &window]() {
auto* dlg = new geopro::app::ProjectListDialog(projectRepo, &window);
dlg->setAttribute(Qt::WA_DeleteOnClose);
QObject::connect(dlg, &geopro::app::ProjectListDialog::projectChosen, &nav,
[&nav, topBar](const QString& id, const QString& name) {
topBar->setProjectButtonText(name);
nav.switchProject(id);
});
dlg->exec();
});
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::objectClicked, &nav,
&geopro::controller::WorkbenchNavController::selectObject);
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::checkedTmsChanged, &nav,
&geopro::controller::WorkbenchNavController::setCheckedTms);
// 控制器详情/异常/数据集表单 → 三个被动面板。
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::objectDetailLoaded, objAttrView,
[objAttrView](const QString&, const geopro::data::DynamicForm& form) {
objAttrView->setForm(form);
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::exceptionTreeLoaded,
exceptionPanel,
[exceptionPanel, anomalyBadge](
const std::vector<geopro::data::ObjectExceptionGroup>& groups, int total) {
exceptionPanel->setGroups(groups);
if (anomalyBadge) {
anomalyBadge->setText(QString::number(total));
anomalyBadge->setVisible(total > 0);
}
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetDetailLoaded, propView,
[propView](const geopro::data::DynamicForm& form) { propView->setForm(form); });
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::workspacesLoaded, topBar,
[topBar](const std::vector<geopro::data::Workspace>& list, const QString& cur) {
topBar->setWorkspaces(list, cur);
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::projectsLoaded, topBar,
[topBar](const std::vector<geopro::data::ProjectSummary>& list,
const QString& cur, int total) {
topBar->setProjects(list, cur, total > static_cast<int>(list.size()));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded, objectTree,
[objectTree, datasetList, fileList, datasetTitle, datasetTabs, exceptionPanel,
objAttrView, propView, anomalyBadge](
const QString& projectName,
const std::vector<geopro::data::StructNode>& nodes) {
objectTree->setStructure(projectName, nodes);
datasetList->clear();
fileList->clear();
exceptionPanel->showMessage(QStringLiteral("(勾选对象后显示其异常 / 异常体)"));
objAttrView->showMessage(QStringLiteral("(选中对象后显示其属性)"));
propView->showMessage(QStringLiteral("(单击数据集查看属性)"));
if (anomalyBadge) anomalyBadge->setVisible(false);
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集"));
datasetTabs->setTabText(0, QStringLiteral("数据"));
datasetTabs->setTabText(1, QStringLiteral("文件"));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList,
[removeLoadMore, addLoadMore, datasetList, datasetTitle, datasetTabs](
const QString&, const std::vector<geopro::data::DsRow>& rows, int total,
bool append) {
removeLoadMore(datasetList);
geopro::app::populateDatasetList(datasetList, rows, append);
const int loaded = addLoadMore(datasetList, total);
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集"));
datasetTabs->setTabText(
0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total)
: QStringLiteral("数据"));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::filesLoaded, fileList,
[removeLoadMore, addLoadMore, fileList, datasetTabs](
const QString&, const std::vector<geopro::data::DsRow>& rows, int total,
bool append) {
removeLoadMore(fileList);
geopro::app::populateFileList(fileList, rows, append);
const int loaded = addLoadMore(fileList, total);
datasetTabs->setTabText(
1, total > 0 ? QStringLiteral("文件 (%1/%2)").arg(loaded).arg(total)
: QStringLiteral("文件"));
});
QObject::connect(fileList, &QListWidget::itemClicked, fileList,
[&nav](QListWidgetItem* item) {
if (item->data(geopro::app::kDsLoadMoreRole).toBool()) nav.loadMoreFiles();
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::loadFailed, objectTree,
[objectTree, &window](const QString& stage, const QString& msg) {
if (stage == QStringLiteral("structure") ||
stage == QStringLiteral("projects"))
objectTree->showMessage(QStringLiteral("加载失败:%1").arg(msg));
// 状态栏错误反馈:临时染 danger 红8s 后随消息超时还原全局中性色。
auto* sb = window.statusBar();
sb->setStyleSheet(QStringLiteral("QStatusBar{color:%1;}")
.arg(QString::fromUtf8(geopro::app::semantic::kDanger)));
sb->showMessage(QStringLiteral("加载失败(%1%2").arg(stage, msg), 8000);
QTimer::singleShot(8000, sb, [sb]() { sb->setStyleSheet(QString()); });
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::busyChanged, &window,
[](bool busy) {
if (busy)
QApplication::setOverrideCursor(Qt::WaitCursor);
else
QApplication::restoreOverrideCursor();
});
// 底部状态栏常驻显示坐标系与世界系原点wayfinding用户随时知道当前空间基准
window.statusBar()->showMessage(
QStringLiteral("就绪 | 坐标系 %1 | 世界系原点 %2, %3")
.arg(QString::fromUtf8(kProjectCrs))
.arg(lat0, 0, 'f', 5)
.arg(lon0, 0, 'f', 5));
// ── dock 布局/窗口几何持久化 ──────────────────────────────────────────
// 恢复上次的停靠布局与窗口几何(须在全部 dock 创建后ADS 按 dock 标题作键匹配)。
{
const QSettings settings;
const QByteArray geo = settings.value(QStringLiteral("ui/geometry")).toByteArray();
if (!geo.isEmpty()) window.restoreGeometry(geo);
// 注意ADS 按 dock 唯一名作键。改过 dock 名后旧布局会失配 → bump 此键丢弃旧布局,
// 回落到下方 addDockWidget 的默认排布(再改 dock 名时同样要 bump 版本号)。
const QByteArray dockState = settings.value(QStringLiteral("ui/dockState_v2")).toByteArray();
if (!dockState.isEmpty()) {
dockManager->restoreState(dockState);
// restoreState 重建停靠区会重新显示 ADS 标题栏,再隐藏一次保持“无双标题”。
hideDockTitleBars();
}
}
// 退出时保存当前布局与几何aboutToQuit 早于 window 析构dockManager/window 仍存活)。
QObject::connect(qApp, &QCoreApplication::aboutToQuit, dockManager, [dockManager, &window]() {
QSettings settings;
settings.setValue(QStringLiteral("ui/geometry"), window.saveGeometry());
settings.setValue(QStringLiteral("ui/dockState_v2"), dockManager->saveState());
});
}
} // namespace
int main(int argc, char* argv[])
{
// 高 DPI 缩放采用直通策略:在 125%/150% 等分数缩放下字体/图标按真实比例渲染,更清晰。
// 必须在 QApplication 构造前设置。
QApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
// QVTK 默认 surface format 必须在 QApplication 之前设置(全局一次,两个 QVTK widget 共用)。
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
QApplication app(argc, argv);
// 组织/应用名QSettings 持久化dock 布局、登录记忆等)按此定位存储位置。
QCoreApplication::setOrganizationName(QStringLiteral("Geomative"));
QCoreApplication::setApplicationName(QStringLiteral("Geopro3"));
// 主题与字号:应用持久化偏好(跟随系统/浅/深 + 字号缩放),再套用 Fusion + 全局 QSS + 调色板。
// 在弹登录窗之前完成 → 登录页与主页 明暗/字号 统一。
geopro::app::applyPersistedThemeMode();
geopro::app::applyPersistedFontScale();
geopro::app::applyThemeMode(app, geopro::app::isDarkTheme());
// 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);
// 记住登录:若上次勾选「记住」且未超 30 天,凭证库里有有效 token → 免登录直接进。
QString token = geopro::app::recallValidToken(30);
if (token.isEmpty()) {
geopro::app::LoginWindow login(auth);
if (login.exec() != QDialog::Accepted) return 0;
token = login.token();
if (login.remember())
geopro::app::rememberSession(token); // 安全存 token + 时间戳
else
geopro::app::forgetSession(); // 未勾选:清除旧记忆
}
api.setToken(token); // 注入 token 供后续 API 使用
// 登录成功 → 构建并显示工作台。
geopro::data::LocalSampleRepository repo(
"D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/");
// 导航仓储 + 控制器(接口/逻辑层):用同一共享会话 ApiClient。
geopro::data::ApiProjectRepository projectRepo(api);
geopro::controller::WorkbenchNavController nav(projectRepo);
// 数据详情仓储 + 控制器(接真实反演 API同一共享会话 ApiClient。
geopro::data::ApiDatasetRepository datasetRepo(api);
geopro::controller::DatasetDetailController detailCtrl(datasetRepo);
// ── 外壳:标准 QMainWindow原生标题栏。buildWorkbench 直接用其
// setCentralWidget/setMenuWidget/statusBar 承载工作台。
const QString kTitle = QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)");
auto* window = new QMainWindow;
window->setWindowTitle(kTitle);
window->resize(1280, 800);
window->setMinimumSize(1024, 680);
buildWorkbench(*window, repo, projectRepo, nav, detailCtrl);
// 主题桥ThemeManager 明/暗切换 → 重应用全局 QSS+调色板(标准控件 + ADS内联 chrome 经各自连接)。
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed,
window, [&app]() { geopro::app::applyThemeMode(app, geopro::app::isDarkTheme()); });
// 主题切换快捷键 Ctrl+Shift+T持久化设置→外观 亦可改)。
auto* themeSc = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Shift+T")), window);
QObject::connect(themeSc, &QShortcut::activated, window, [] {
geopro::app::setThemeModePreference(geopro::app::isDarkTheme() ? QStringLiteral("light")
: QStringLiteral("dark"));
});
window->show();
nav.start(); // 进入工作台后拉真实 空间/项目/结构
return app.exec();
}