1395 lines
82 KiB
C++
1395 lines
82 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 <initializer_list>
|
||
#include <memory>
|
||
#include <sstream>
|
||
#include <string>
|
||
#include <typeinfo>
|
||
#include <vector>
|
||
|
||
#include <QActionGroup>
|
||
#include <QApplication>
|
||
#include <QCheckBox>
|
||
#include <QColor>
|
||
#include <QDialog>
|
||
#include <QEasingCurve>
|
||
#include <QEvent>
|
||
#include <QFile>
|
||
#include <QButtonGroup>
|
||
#include <QCheckBox>
|
||
#include <QComboBox>
|
||
#include <QFrame>
|
||
#include <QHBoxLayout>
|
||
#include <QPushButton>
|
||
#include <QSlider>
|
||
#include <QGraphicsOpacityEffect>
|
||
#include <QDate>
|
||
#include <QLabel>
|
||
#include <QListWidget>
|
||
#include <QListWidgetItem>
|
||
#include <QJsonObject>
|
||
#include <QMenu>
|
||
#include <QMessageBox>
|
||
#include <QPoint>
|
||
#include <QSet>
|
||
#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 <QSignalBlocker>
|
||
#include <QTimer>
|
||
#include <QToolBar>
|
||
#include <QTreeWidget>
|
||
#include <QTreeWidgetItem>
|
||
#include <QTreeWidgetItemIterator>
|
||
#include <QVBoxLayout>
|
||
#include <QWidget>
|
||
|
||
#include <QtWebEngineQuick/QtWebEngineQuick>
|
||
|
||
#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 "DatasetDimension.hpp"
|
||
#include "Credential.hpp"
|
||
#include "Glyphs.hpp"
|
||
#include "Logging.hpp"
|
||
#include "PanelHeader.hpp"
|
||
#include "Theme.hpp"
|
||
#include "SettingsDialog.hpp"
|
||
#include "TopBar.hpp"
|
||
#include "ProjectListDialog.hpp"
|
||
#include "ObjectFormDialog.hpp"
|
||
#include "ImportDatasetDialog.hpp"
|
||
#include "WorkbenchNavController.hpp"
|
||
#include "VtkSceneController.hpp"
|
||
#include "VtkSceneView.hpp"
|
||
#include "api/NavRequest.hpp"
|
||
#include "api/NavLoads.hpp"
|
||
#include "DatasetDetailController.hpp"
|
||
#include "panels/chart/ErtInversionStrategy.hpp"
|
||
#include "panels/chart/MeasurementStrategy.hpp"
|
||
#include "panels/chart/GrMeasurementStrategy.hpp"
|
||
#include "panels/chart/TrajectoryStrategy.hpp"
|
||
#include "panels/chart/GridStrategy.hpp"
|
||
#include "api/ApiProjectRepository.hpp"
|
||
#include "api/ApiDatasetRepository.hpp"
|
||
#include "api/Api3dRepository.hpp"
|
||
#include "panels/ObjectTreePanel.hpp"
|
||
#include "login/LoginWindow.hpp"
|
||
#include "panels/DatasetListPanel.hpp"
|
||
#include "panels/DatasetDetailPanel.hpp"
|
||
#include "panels/DynamicFormView.hpp"
|
||
#include "panels/ObjectAttrPanel.hpp"
|
||
#include "panels/DatasetAttrPanel.hpp"
|
||
#include "panels/ObjectExceptionPanel.hpp"
|
||
#include "TileBasemap.hpp"
|
||
#include "panels/columns/ColumnDrawer.hpp"
|
||
#include "panels/columns/Column3DDataset.hpp"
|
||
#include "panels/columns/Column2DDataset.hpp"
|
||
#include "panels/columns/Column3DAnalysis.hpp"
|
||
|
||
#include "CameraPreset.hpp"
|
||
#include "ColorLutBuilder.hpp"
|
||
#include "Scene.hpp"
|
||
#include "VoxelFromScatters.hpp"
|
||
#include "interact/InteractionManager.hpp"
|
||
#include "interact/SlicePlaneMath.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 <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]);
|
||
}
|
||
|
||
// 纵向夸张倍数(Z 基准统一,M-3):全项目共用同一倍数,使 帘面(z) / 体素 / 切片 /
|
||
// 数据详情剖面(y) / 地形(relief) 的纵向比例一致——避免「剖面×1.5、帘面×3」不一致。
|
||
// 单一可调常量:要整体调纵向观感改这一处即可。
|
||
constexpr double kVerticalExaggeration = 1.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::IAsyncProjectRepository& projectRepo,
|
||
geopro::data::IAsyncDatasetRepository& datasetRepo,
|
||
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();
|
||
vtkNew<vtkGenericOpenGLRenderWindow> renderWindow;
|
||
vtkWidget->setRenderWindow(renderWindow);
|
||
renderWindow->AddRenderer(scene->renderer());
|
||
|
||
vtkGenericOpenGLRenderWindow* renderWindowPtr = renderWindow.Get();
|
||
|
||
// 中央渲染编排(VtkSceneController + VtkSceneView,取代旧 rebuildCentral lambda 与裸 show* 标志)。
|
||
// 3D 场景仓储用 Api3dRepository(真实后端:loadSection 走真实 ERT 反演端点,委托 datasetRepo)。
|
||
// 视图(VtkSceneView)非 QObject、控制器/3D 仓储亦然:随 scene 一并在 vtkWidget 销毁时清理。
|
||
auto* scene3dRepo = new geopro::data::Api3dRepository(datasetRepo);
|
||
auto* sceneView = new geopro::app::VtkSceneView(*scene, renderWindowPtr,
|
||
frame, refElev);
|
||
auto* sceneCtrl = new geopro::controller::VtkSceneController(repo, *scene3dRepo, *sceneView,
|
||
vtkWidget);
|
||
sceneCtrl->setVerticalExaggeration(kVerticalExaggeration);
|
||
|
||
// ── P3 切片交互编排(InteractionManager)─────────────────────────────────
|
||
// interactor 由 QVTK 在 setRenderWindow 后提供(renderWindow->GetInteractor())。
|
||
// 安装自定义拾取样式 + 持活动切片。仅三维 + 有体素可用;切到二维 closeAll。
|
||
auto* interactionMgr = new geopro::render::interact::InteractionManager(
|
||
renderWindowPtr->GetInteractor(), renderWindowPtr, scene->renderer());
|
||
// sceneView->onVolumeChanged 在三栏接线处设置(把体素 image 推给 InteractionManager,见下)。
|
||
// 非 QObject 堆对象统一在此清理,按构造逆序:
|
||
// interactionMgr(持 interactor/切片观察者) → sceneView(持 scene&) → scene3dRepo → scene。
|
||
// interactionMgr 先析构:closeAll() 解绑所有切片观察者,再拆 scene/interactor,防悬挂崩溃。
|
||
QObject::connect(vtkWidget, &QObject::destroyed,
|
||
[scene, scene3dRepo, sceneView, interactionMgr]() {
|
||
delete interactionMgr;
|
||
delete sceneView;
|
||
delete scene3dRepo;
|
||
delete scene;
|
||
});
|
||
|
||
// PROJ 可用性探测(体素/地形/切片层都需配准):三栏重构后浮层勾选已移除,
|
||
// 仅保留探测以便将来在三栏里据此禁用相关项;本期结果暂未消费。
|
||
bool crsAvailable = false;
|
||
try {
|
||
geopro::core::CrsTransform probe(kProjectCrs, kWgs84);
|
||
crsAvailable = true;
|
||
} catch (const std::exception&) {
|
||
crsAvailable = false;
|
||
}
|
||
(void)crsAvailable;
|
||
|
||
// 停靠系统配置(必须在 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;
|
||
};
|
||
|
||
// 中央容器:顶部「VTK视图」表头 + 下方 [左三栏抽屉 | 右 QVTK 画布]。
|
||
auto* centerWidget = new QWidget();
|
||
auto* centerLayout = new QVBoxLayout(centerWidget);
|
||
centerLayout->setContentsMargins(0, 0, 0, 0);
|
||
centerLayout->setSpacing(0);
|
||
|
||
// VTK视图面板表头(Task 7):图标 + 标题「VTK视图」+ 全屏操作按钮(全屏 connect 见 Task 8)。
|
||
auto* viewHeader = geopro::app::buildPanelHeader(
|
||
geopro::app::Glyph::Map, QStringLiteral("VTK视图"),
|
||
{{geopro::app::Glyph::Fullscreen, QStringLiteral("全屏")}});
|
||
|
||
// 左侧内嵌三栏抽屉(自带折叠按钮)+ 右侧 GL 画布,水平并列(非 GL 覆盖层,避免 z-order/圆角伪影)。
|
||
auto* drawer = new geopro::app::ColumnDrawer(centerWidget);
|
||
auto* viewRow = new QHBoxLayout();
|
||
viewRow->setContentsMargins(0, 0, 0, 0);
|
||
viewRow->setSpacing(0);
|
||
viewRow->addWidget(drawer); // 左侧抽屉(自带折叠按钮)
|
||
viewRow->addWidget(vtkWidget, 1); // 右侧 GL 画布
|
||
centerLayout->addWidget(viewHeader);
|
||
centerLayout->addLayout(viewRow, 1);
|
||
|
||
// 体素变化(重建/清场)后把体素 image 推给 InteractionManager(切片基底)。
|
||
sceneView->onVolumeChanged = [interactionMgr, sceneView]() {
|
||
if (sceneView->hasVolume())
|
||
interactionMgr->setVolumeImage(sceneView->currentVolumeImage(),
|
||
sceneView->currentColorScale(), sceneView->currentVmin(),
|
||
sceneView->currentVmax());
|
||
else
|
||
interactionMgr->setVolumeImage(nullptr, sceneView->currentColorScale(), 0.0, 0.0);
|
||
};
|
||
|
||
// ── 三栏抽屉信号 → 控制器/交互(Task 7 接线)──────────────────────────────
|
||
auto* c3 = drawer->col3D();
|
||
QObject::connect(c3, &geopro::app::Column3DDataset::axesModeChanged, sceneCtrl,
|
||
&geopro::controller::VtkSceneController::setAxesMode);
|
||
QObject::connect(c3, &geopro::app::Column3DDataset::axesUnitChanged, sceneCtrl,
|
||
&geopro::controller::VtkSceneController::setAxesUnit);
|
||
QObject::connect(c3, &geopro::app::Column3DDataset::verticalExaggerationChanged, sceneCtrl,
|
||
&geopro::controller::VtkSceneController::setVerticalExaggeration);
|
||
QObject::connect(c3, &geopro::app::Column3DDataset::viewRequested, sceneCtrl,
|
||
&geopro::controller::VtkSceneController::applyView);
|
||
QObject::connect(c3, &geopro::app::Column3DDataset::zoomInRequested, sceneCtrl,
|
||
&geopro::controller::VtkSceneController::zoomIn);
|
||
QObject::connect(c3, &geopro::app::Column3DDataset::zoomOutRequested, sceneCtrl,
|
||
&geopro::controller::VtkSceneController::zoomOut);
|
||
QObject::connect(c3, &geopro::app::Column3DDataset::fitRequested, sceneCtrl,
|
||
&geopro::controller::VtkSceneController::fit);
|
||
// 渲染勾选的 3D 数据集:真实 ds id 直达控制器异步帘面路径
|
||
// (setCheckedDatasets → Api3dRepository.loadSection(realId) → 真实 ERT 反演端点 → 真实帘面)。
|
||
QObject::connect(c3, &geopro::app::Column3DDataset::checkedDatasetsChanged, sceneCtrl,
|
||
&geopro::controller::VtkSceneController::setCheckedDatasets);
|
||
// O点位置/字体本期 stub(TODO P4:弹框)。
|
||
QObject::connect(c3, &geopro::app::Column3DDataset::oPointClicked, vtkWidget,
|
||
[]() { /* TODO P4: O点位置弹框 */ });
|
||
QObject::connect(c3, &geopro::app::Column3DDataset::fontClicked, vtkWidget,
|
||
[]() { /* TODO P4: 字体弹框 */ });
|
||
|
||
auto* ca = drawer->colAnalysis();
|
||
QObject::connect(ca, &geopro::app::Column3DAnalysis::sliceRequested, vtkWidget,
|
||
[interactionMgr](geopro::render::interact::SliceAxis axis) {
|
||
interactionMgr->addSlice(axis);
|
||
});
|
||
QObject::connect(ca, &geopro::app::Column3DAnalysis::detailRequested, &detailCtrl,
|
||
[&detailCtrl](const QString& dsId, const QString& ddCode, const QString& name) {
|
||
detailCtrl.openDataset(dsId, ddCode, name);
|
||
});
|
||
|
||
// ── 二维数据集栏:天地图底图开关(③,复用轨迹图 token,经同一共享 GeoLocalFrame 配准)──
|
||
auto* basemap = new geopro::app::TileBasemap(*scene, renderWindowPtr, frame, &window);
|
||
// 当前底图选择(默认 天地图=Satellite,对齐 Column2DDataset 默认项);数据重锚后据此在数据位置加载。
|
||
auto basemapKind =
|
||
std::make_shared<geopro::app::TileBasemap::Kind>(geopro::app::TileBasemap::Satellite);
|
||
QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::basemapChanged, basemap,
|
||
[basemap, basemapKind](int idx) {
|
||
// 地图下拉:0 天地图(卫星影像) / 1 Google(暂未实现→隐藏) / 2 隐藏。
|
||
*basemapKind = (idx == 0) ? geopro::app::TileBasemap::Satellite
|
||
: geopro::app::TileBasemap::Hidden;
|
||
basemap->show(*basemapKind);
|
||
});
|
||
// 首个真实剖面到达 → frame 重锚到数据 lat/lon 后,把选中的底图加载到数据所在位置
|
||
// (默认天地图即在此刻出现,避免启动时在样本原点拉无关瓦片)。
|
||
sceneView->onFrameReanchored = [basemap, basemapKind]() {
|
||
if (*basemapKind != geopro::app::TileBasemap::Hidden) basemap->show(*basemapKind);
|
||
};
|
||
// 相机程序化变化(取景/预设/缩放)后,底图按新视锥重算覆盖(治首帧部分瓦片需手动微动才出)。
|
||
sceneView->onCameraChanged = [basemap]() { basemap->refresh(); };
|
||
// 垂直夸张:地形须与剖面用同一 VE 才对齐(都按真实高程×VE)。
|
||
QObject::connect(drawer->col3D(), &geopro::app::Column3DDataset::verticalExaggerationChanged,
|
||
basemap, [basemap](double ve) { basemap->setVerticalExaggeration(ve); });
|
||
// 单一来源:kVerticalExaggeration 一处定义,组合根下发到 控制器(上方259) / 底图 / UI 显示。
|
||
basemap->setVerticalExaggeration(kVerticalExaggeration);
|
||
drawer->col3D()->setVerticalExaggeration(kVerticalExaggeration);
|
||
|
||
// ── 中央“空状态”引导浮层:未接入真实 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("VTK视图"));
|
||
vtkDock->setWidget(centerWidget);
|
||
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
|
||
|
||
// ── 下方「数据详情」dock:平面图表多 Tab 面板(QGraphicsView,VTK 仅算几何)──
|
||
// 单击数据集 → 聚焦已开页;双击 → 新建/聚焦页(真实反演剖面/散点/异常/色阶)。
|
||
auto* detailPanel = new geopro::app::DatasetDetailPanel();
|
||
auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情"));
|
||
// ForceNoScrollArea:禁止 ADS 默认把整块内容(含标题栏/页签栏)包进 QScrollArea。
|
||
// 否则内容最小尺寸超过 dock 时,整个面板(标题+页签+图)一起滚动。改为内容自适应填充;
|
||
// 需要时由内层(图表内容区)自行滚动,标题/页签固定。
|
||
auto* detailHeader = wrapWithHeader(
|
||
geopro::app::Glyph::Detail, QStringLiteral("数据详情"), detailPanel,
|
||
{{geopro::app::Glyph::Fullscreen, QStringLiteral("全屏")}});
|
||
detailDock->setWidget(detailHeader, ads::CDockWidget::ForceNoScrollArea);
|
||
// 放在中央视图下方。
|
||
dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea);
|
||
|
||
// 左上 dock:对象树(真实结构:项目根 → GS → TM)。被动视图,数据由控制器推送。
|
||
auto* objectTree = new geopro::app::ObjectTreePanel();
|
||
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象"));
|
||
auto* objectBox = wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象"), objectTree,
|
||
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
|
||
{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}});
|
||
leftDock->setWidget(objectBox);
|
||
auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
|
||
|
||
// 左下 dock:数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
|
||
auto* datasetTabs = new QTabWidget();
|
||
// 数据页签:树形列表(原版 el-table 树——派生数据挂源数据下,按 DsRow.parentId 嵌套)。
|
||
auto* datasetList = new QTreeWidget();
|
||
datasetList->setHeaderHidden(true);
|
||
datasetList->setColumnCount(1);
|
||
datasetList->setRootIsDecorated(true); // 显展开/折叠箭头
|
||
datasetList->setIndentation(geopro::app::scaledPx(14));
|
||
datasetList->setExpandsOnDoubleClick(false); // 双击=打开详情,不切展开(展开靠箭头)
|
||
datasetList->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||
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::ObjectAttrPanel(projectRepo);
|
||
|
||
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::DatasetAttrPanel(projectRepo);
|
||
auto* propDock = new ads::CDockWidget(QStringLiteral("数据集属性"));
|
||
propDock->setWidget(
|
||
wrapWithHeader(geopro::app::Glyph::Property, QStringLiteral("数据集属性"), propView));
|
||
dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea);
|
||
|
||
// 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。
|
||
// 同时隐藏 ADS 自带标题栏——表头已由各面板内的自绘 PanelHeader 承担,避免“双标题”。
|
||
// 抽成 lambda:ADS 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();
|
||
|
||
// 中央渲染由 sceneCtrl(VtkSceneController)驱动:勾选对象/2D-3D切换/图层勾选/主题 → 重建场景。
|
||
// (旧 rebuildCentral lambda + 裸 show* 标志已由控制器取代。)
|
||
|
||
// ── 单击左下数据列表的采集批次(DS) → 属性表单 + 聚焦详情已开页 ──
|
||
QObject::connect(datasetList, &QTreeWidget::itemClicked, datasetList,
|
||
[&nav, &detailCtrl, propView](QTreeWidgetItem* item, int) {
|
||
if (item->data(0, geopro::app::kDsLoadMoreRole).toBool()) {
|
||
nav.loadMoreData();
|
||
return;
|
||
}
|
||
const QString dsId = item->data(0, geopro::app::kDsIdRole).toString();
|
||
if (dsId.isEmpty()) return;
|
||
nav.selectDataset(dsId); // 只读元字段表单(datasetDetailLoaded)
|
||
propView->selectDataset(dsId); // 可编辑描述:回填 + 启用保存
|
||
detailCtrl.focusDataset(dsId); // 单击=聚焦已开页
|
||
});
|
||
|
||
// ── 双击 → 打开/聚焦该数据集的详情图表页(拉真实反演剖面/散点/异常/色阶)──
|
||
QObject::connect(datasetList, &QTreeWidget::itemDoubleClicked, datasetList,
|
||
[&detailCtrl](QTreeWidgetItem* item, int) {
|
||
const QString dsId = item->data(0, geopro::app::kDsIdRole).toString();
|
||
const QString ddCode = item->data(0, geopro::app::kDsDdCodeRole).toString();
|
||
const QString dsName = item->data(0, geopro::app::kDsNameRole).toString();
|
||
if (!dsId.isEmpty()) detailCtrl.openDataset(dsId, ddCode, dsName);
|
||
});
|
||
|
||
// ── 控制器信号 → 详情面板(tab 引擎):开页 / 页签就绪 / 加载中 / 聚焦 ──
|
||
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::datasetOpened,
|
||
detailPanel, &geopro::app::DatasetDetailPanel::onDatasetOpened);
|
||
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::tabReady,
|
||
detailPanel, &geopro::app::DatasetDetailPanel::onTabReady);
|
||
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::tabLoadStarted,
|
||
detailPanel, &geopro::app::DatasetDetailPanel::onTabLoadStarted);
|
||
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::focusRequested,
|
||
detailPanel, &geopro::app::DatasetDetailPanel::focusDataset);
|
||
// ── 页签懒加载:lazy 页签首次激活 → 控制器按 (dsId,ddCode,tabIndex) 拉载荷 → 回填 ──
|
||
QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::tabNeeded, &detailCtrl,
|
||
&geopro::controller::DatasetDetailController::loadTab);
|
||
// ── 分页:分页器翻页/改每页条数 → 控制器按页加载 → 回填(同 tabReady 路径,刷新表格+分页器)──
|
||
QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::tabPageNeeded, &detailCtrl,
|
||
&geopro::controller::DatasetDetailController::loadTabPaged);
|
||
// context 用 detailPanel:析构即自动断连,避免野指针。window 比 detailPanel 活得久,
|
||
// 捕 &window 取状态栏安全。失败时清该页 lazy 遮罩(幂等)并状态栏提示。
|
||
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::loadFailed, detailPanel,
|
||
[&window, detailPanel](const QString& dsId, const QString& msg) {
|
||
detailPanel->onLoadFailed(dsId, msg);
|
||
window.statusBar()->showMessage(
|
||
QStringLiteral("数据集 %1 加载失败:%2").arg(dsId, msg), 5000);
|
||
});
|
||
|
||
// ── 详情面板切 Tab → 反向高亮数据集列表对应行 ──
|
||
QObject::connect(detailPanel, &geopro::app::DatasetDetailPanel::activeDatasetChanged,
|
||
datasetList, [datasetList](const QString& dsId) {
|
||
for (QTreeWidgetItemIterator it(datasetList); *it; ++it)
|
||
if ((*it)->data(0, geopro::app::kDsIdRole).toString() == dsId) {
|
||
datasetList->setCurrentItem(*it);
|
||
break;
|
||
}
|
||
});
|
||
|
||
// ── 左上对象树勾选 → 拉取各 TM 的 ds 子树,按维度分发到三栏列表(spec §6.1/§8)──
|
||
// 渲染由三栏勾选框驱动(Task 7:Column3DDataset::checkedDatasetsChanged → setCheckedDatasets)。
|
||
auto generation = std::make_shared<unsigned long long>(0);
|
||
QObject::connect(
|
||
objectTree, &geopro::app::ObjectTreePanel::checkedTmsChanged, &window,
|
||
[&projectRepo, &nav, drawer, emptyState, generation](const QStringList& tmIds) {
|
||
const unsigned long long myGen = ++(*generation);
|
||
emptyState->setVisible(tmIds.isEmpty()); // 有勾选→隐藏引导层,露出中央渲染
|
||
if (tmIds.isEmpty()) {
|
||
drawer->col3D()->setDatasets({});
|
||
drawer->col2D()->setDatasets({});
|
||
drawer->colAnalysis()->setDatasets({});
|
||
return;
|
||
}
|
||
// 多 TM 异步汇总:每个 TM 取整棵 ds 子树,全部回来后按维度分发到三栏。
|
||
auto acc = std::make_shared<std::vector<geopro::data::DsRow>>();
|
||
auto remaining = std::make_shared<int>(tmIds.size());
|
||
auto finish = [acc, drawer, generation, myGen]() {
|
||
if (*generation != myGen) return; // 已被更新的勾选批次取代→丢弃陈旧结果
|
||
geopro::app::DimBuckets b = geopro::app::splitByDimension(*acc);
|
||
drawer->col3D()->setDatasets(b.dim3D);
|
||
drawer->col2D()->setDatasets(b.dim2D);
|
||
drawer->colAnalysis()->setDatasets(b.analysis);
|
||
};
|
||
for (const QString& tm : tmIds) {
|
||
geopro::data::NavRequest* req = projectRepo.loadRowsAsync(
|
||
nav.currentProjectId().toStdString(), tm.toStdString(), 2, 3, 1, 100000);
|
||
QObject::connect(req, &geopro::data::NavRequest::done, drawer,
|
||
[acc, remaining, finish](const QVariant& v) {
|
||
auto page = qvariant_cast<geopro::data::DsPage>(v);
|
||
acc->insert(acc->end(), page.rows.begin(), page.rows.end());
|
||
if (--(*remaining) == 0) finish();
|
||
});
|
||
QObject::connect(req, &geopro::data::NavRequest::failed, drawer,
|
||
[remaining, finish](const QString&) {
|
||
if (--(*remaining) == 0) finish(); // 单个失败不卡死,其余照常分发
|
||
});
|
||
}
|
||
});
|
||
|
||
// ── 启动:建立一次中央视图。三栏重构后删除了 2D/3D 切换,统一固定为三维视图
|
||
// (帘面默认开启 showCurtain_=true,勾选 dd_section → 帘面)。无勾选 → 空场景 + 背景。
|
||
sceneCtrl->setViewMode(geopro::controller::ViewMode::View3D);
|
||
|
||
// VTK 背景随主题切换:控制器重渲染(走完整渲染路径、末尾必 Render)。
|
||
// context 用 sceneCtrl(非 window):ThemeManager 是进程级单例,连接须随 sceneCtrl 析构自动断开,
|
||
// 否则 window 析构期间 sceneCtrl(其孙级子对象)已销毁、主题异步变化会触悬垂指针。
|
||
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed, sceneCtrl,
|
||
[sceneCtrl]() { sceneCtrl->rebuild(); });
|
||
|
||
// 顶部应用区(静态视觉壳,对齐原型):上=菜单栏(视图/项目管理/业务工具/设备),
|
||
// 下=工具条(工作空间切换 + 项目 + 帮助/通知/设置 + 用户)。纵向堆叠后挂到主窗口顶部。
|
||
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;
|
||
};
|
||
// 数据树的「加载更多」:末尾顶层项;已加载数=树中非"加载更多"项总数(含各层子节点)。
|
||
auto removeTreeLoadMore = [](QTreeWidget* tw) {
|
||
const int n = tw->topLevelItemCount();
|
||
if (n > 0 && tw->topLevelItem(n - 1)->data(0, geopro::app::kDsLoadMoreRole).toBool())
|
||
delete tw->takeTopLevelItem(n - 1);
|
||
};
|
||
// total = 根节点总数(控制器按根分页);loaded 也按「第一层节点(根)」计 → 加载更多/页签数一致。
|
||
auto addTreeLoadMore = [](QTreeWidget* tw, int total) {
|
||
int loaded = 0;
|
||
for (int i = 0; i < tw->topLevelItemCount(); ++i)
|
||
if (!tw->topLevelItem(i)->data(0, geopro::app::kDsLoadMoreRole).toBool()) ++loaded;
|
||
if (loaded < total) {
|
||
auto* m = new QTreeWidgetItem(tw);
|
||
m->setText(0, QStringLiteral("加载更多(%1/%2)").arg(loaded).arg(total));
|
||
m->setData(0, geopro::app::kDsLoadMoreRole, true);
|
||
m->setTextAlignment(0, 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);
|
||
// 单击对象 → 对象属性面板渲染可编辑表单(projectId 取当前项目;项目根只读占位)。
|
||
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::objectSelectedForEdit, objAttrView,
|
||
[objAttrView, objectTree, &nav](const QString& id, int confType,
|
||
const QString& typeId, const QString& name,
|
||
bool isRoot) {
|
||
objAttrView->loadObject(nav.currentProjectId(), typeId, id, confType, name,
|
||
isRoot, objectTree->parentObjectId(id));
|
||
});
|
||
|
||
// 当前选中的 TM id(confType==2 时记录,其它选中清空):数据集面板「上传」按钮据此定父对象。
|
||
auto currentTmId = std::make_shared<QString>();
|
||
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::objectClicked, &window,
|
||
[currentTmId](const QString& objectId, int confType) {
|
||
*currentTmId = (confType == 2) ? objectId : QString();
|
||
});
|
||
// 切项目/重建结构 → 旧选中 TM 失效,清空(避免「上传」按钮误用跨项目的 TM)。
|
||
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded, &window,
|
||
[currentTmId](const QString&, const std::vector<geopro::data::StructNode>&) {
|
||
currentTmId->clear();
|
||
});
|
||
|
||
// ── 交互操作接线(对象/数据集视图:右键菜单 + 快速筛选 + 增删,2D/3D 相关占位)────────
|
||
auto* anomalyTabGroup = anomalyPanel.tabGroup; // 捕指针(anomalyPanel 为局部,勿按引用捕获)
|
||
// 状态栏轻提示(toast 替代;window 生命周期覆盖整个会话,按引用捕获安全)。
|
||
auto toast = [&window](const QString& msg) { window.statusBar()->showMessage(msg, 4000); };
|
||
// 表头操作按钮定位器:按 glyphId 在已包装面板表头里找具体 QToolButton。
|
||
auto findHeaderAction = [](QWidget* box, geopro::app::Glyph g) -> QToolButton* {
|
||
const int gid = static_cast<int>(g);
|
||
for (auto* b : box->findChildren<QToolButton*>(QStringLiteral("panelAction")))
|
||
if (b->property("glyphId").toInt() == gid) return b;
|
||
return nullptr;
|
||
};
|
||
|
||
// ── 全屏切换:VTK视图 / 数据详情 表头右上角「全屏」按钮 ──────────────────────────
|
||
// 点击 → 目标 dock 全屏(隐藏其余所有 dock);再点 → 还原(全部显示)。
|
||
// 使用 ADS CDockWidget::toggleView(bool) 控制可见性(标准 ADS API,v4+)。
|
||
{
|
||
const QList<ads::CDockWidget*> allDocks{vtkDock, detailDock, leftDock, datasetDock,
|
||
rightDock, propDock};
|
||
auto applyFullscreen = [](ads::CDockWidget* target,
|
||
const QList<ads::CDockWidget*>& all, bool on) {
|
||
for (ads::CDockWidget* d : all) {
|
||
if (d == target) continue;
|
||
d->toggleView(!on); // on=进入全屏→隐藏其它; off=还原→全部显示
|
||
}
|
||
};
|
||
|
||
auto* vtkFsBtn = findHeaderAction(viewHeader, geopro::app::Glyph::Fullscreen);
|
||
auto* detailFsBtn = findHeaderAction(detailHeader, geopro::app::Glyph::Fullscreen);
|
||
|
||
if (vtkFsBtn) {
|
||
vtkFsBtn->setCheckable(true);
|
||
QObject::connect(vtkFsBtn, &QToolButton::toggled, &window,
|
||
[applyFullscreen, vtkDock, allDocks, detailFsBtn, drawer](bool on) {
|
||
if (on && detailFsBtn && detailFsBtn->isChecked()) {
|
||
QSignalBlocker b(detailFsBtn);
|
||
detailFsBtn->setChecked(false);
|
||
}
|
||
// VTK 全屏含左侧三栏(drawer 本就在 vtkDock 内):进入时确保展开可见。
|
||
if (on) drawer->expand();
|
||
applyFullscreen(vtkDock, allDocks, on);
|
||
});
|
||
}
|
||
if (detailFsBtn) {
|
||
detailFsBtn->setCheckable(true);
|
||
QObject::connect(detailFsBtn, &QToolButton::toggled, &window,
|
||
[applyFullscreen, detailDock, allDocks, vtkFsBtn](bool on) {
|
||
if (on && vtkFsBtn && vtkFsBtn->isChecked()) {
|
||
QSignalBlocker b(vtkFsBtn);
|
||
vtkFsBtn->setChecked(false);
|
||
}
|
||
applyFullscreen(detailDock, allDocks, on);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 对象树右键菜单动作路由。
|
||
QObject::connect(
|
||
objectTree, &geopro::app::ObjectTreePanel::contextActionRequested, &window,
|
||
[&nav, &projectRepo, &window, anomalyTabGroup, toast, objAttrView, objectTree](
|
||
const QString& action, const QString& id, int confType, const QString& typeId,
|
||
const QString& name) {
|
||
if (action == QStringLiteral("properties")) {
|
||
nav.selectObject(id, confType);
|
||
// 右键「属性」:用可编辑面板渲染(与左键单击同口径)。
|
||
objAttrView->loadObject(nav.currentProjectId(), typeId, id, confType, name, false,
|
||
objectTree->parentObjectId(id));
|
||
if (anomalyTabGroup)
|
||
if (auto* b = anomalyTabGroup->button(1)) b->click(); // 切到「对象属性」页签
|
||
} else if (action == QStringLiteral("exceptionDetail")) {
|
||
nav.showObjectExceptions(id, confType);
|
||
if (anomalyTabGroup)
|
||
if (auto* b = anomalyTabGroup->button(0)) b->click(); // 切到「对象异常」页签
|
||
} else if (action == QStringLiteral("delete")) {
|
||
const auto r = QMessageBox::question(
|
||
&window, QStringLiteral("删除确认"),
|
||
QStringLiteral("确定删除「%1」?该操作不可撤销。").arg(name),
|
||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||
if (r == QMessageBox::Yes) nav.deleteObject(id, confType);
|
||
} else if (action == QStringLiteral("edit")) {
|
||
// 动态表单编辑器:拉 project/getDynamicForm 真实 schema 渲染可编辑表单;
|
||
// 确定→校验+提交(PUT,body 为推断结构,确切性以服务端为准)→成功刷新结构。
|
||
auto* dlg = new geopro::app::ObjectFormDialog(projectRepo, nav.currentProjectId(),
|
||
&window);
|
||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||
dlg->editObject(typeId, id, confType, name, objectTree->parentObjectId(id));
|
||
QObject::connect(dlg, &geopro::app::ObjectFormDialog::submitted, &window,
|
||
[&nav, toast](int) {
|
||
toast(QStringLiteral("保存成功"));
|
||
nav.switchProject(nav.currentProjectId());
|
||
});
|
||
dlg->open();
|
||
} else if (action == QStringLiteral("newTm")) {
|
||
// 新建 TM:对话框拉 tmList(全局方法类型)选类型 → getDynamicForm(type=2) → POST /tmObject。
|
||
// 父对象:在 GS/项目根上=该节点;在 TM 上=其父 GS/根(即新建同级 TM)。
|
||
const QString tmParent =
|
||
(confType == 2) ? objectTree->parentObjectId(id) : id;
|
||
auto* dlg = new geopro::app::ObjectFormDialog(projectRepo, nav.currentProjectId(),
|
||
&window);
|
||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||
dlg->newTm(tmParent);
|
||
QObject::connect(dlg, &geopro::app::ObjectFormDialog::submitted, &window,
|
||
[&nav, toast](int) {
|
||
toast(QStringLiteral("新建成功"));
|
||
nav.switchProject(nav.currentProjectId());
|
||
});
|
||
dlg->open();
|
||
} else if (action == QStringLiteral("newGs")) {
|
||
// 新建 GS:对话框拉 gsList 选类型 → getDynamicForm(type=1) → POST /gsObject。
|
||
// 父对象 = 右键所在节点(GS/项目根) id。
|
||
auto* dlg = new geopro::app::ObjectFormDialog(projectRepo, nav.currentProjectId(),
|
||
&window);
|
||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||
dlg->newGs(id);
|
||
QObject::connect(dlg, &geopro::app::ObjectFormDialog::submitted, &window,
|
||
[&nav, toast](int) {
|
||
toast(QStringLiteral("新建成功"));
|
||
nav.switchProject(nav.currentProjectId());
|
||
});
|
||
dlg->open();
|
||
} else if (action == QStringLiteral("importDs")) {
|
||
// 导入 DS:TM 右键 → 选数据类型/脚本/文件 → checkImport → import(multipart)。
|
||
auto* dlg = new geopro::app::ImportDatasetDialog(projectRepo, nav.currentProjectId(),
|
||
id, &window);
|
||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||
QObject::connect(dlg, &geopro::app::ImportDatasetDialog::imported, &window,
|
||
[&nav, toast]() {
|
||
toast(QStringLiteral("导入成功"));
|
||
nav.switchProject(nav.currentProjectId());
|
||
});
|
||
dlg->open();
|
||
} else if (action == QStringLiteral("showHide") || action == QStringLiteral("locate")) {
|
||
toast(QStringLiteral("「%1」需要二维/三维视图,开发中").arg(name));
|
||
} else {
|
||
toast(QStringLiteral("该功能开发中,即将接入"));
|
||
}
|
||
});
|
||
|
||
// 对象属性面板保存成功 → toast + 刷新结构(重载当前项目,回填最新属性)。
|
||
QObject::connect(objAttrView, &geopro::app::ObjectAttrPanel::saved, &window,
|
||
[&nav, toast]() {
|
||
toast(QStringLiteral("保存成功"));
|
||
nav.switchProject(nav.currentProjectId());
|
||
});
|
||
// 数据集属性面板描述保存成功 → toast。
|
||
QObject::connect(propView, &geopro::app::DatasetAttrPanel::saved, &window,
|
||
[toast]() { toast(QStringLiteral("描述已保存")); });
|
||
|
||
// 增删改结果 → 状态栏反馈(成功后控制器已自行刷新)。
|
||
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::mutationSucceeded, &window,
|
||
[toast](const QString& msg) { toast(msg); });
|
||
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::mutationFailed, &window,
|
||
[&window](const QString& msg) {
|
||
auto* sb = window.statusBar();
|
||
sb->setStyleSheet(QStringLiteral("QStatusBar{color:%1;}")
|
||
.arg(QString::fromUtf8(geopro::app::semantic::kDanger)));
|
||
sb->showMessage(QStringLiteral("操作失败:%1").arg(msg), 6000);
|
||
QTimer::singleShot(6000, sb, [sb]() { sb->setStyleSheet(QString()); });
|
||
});
|
||
|
||
// 对象树表头「筛选」按钮 → 快速筛选弹出菜单(按类型批量勾选/反选 TM)。
|
||
if (auto* objFilterBtn = findHeaderAction(objectBox, geopro::app::Glyph::Filter)) {
|
||
objFilterBtn->setToolTip(QStringLiteral("快速筛选"));
|
||
QObject::connect(objFilterBtn, &QToolButton::clicked, objectTree,
|
||
[objectTree, objFilterBtn]() {
|
||
QMenu m(objectTree);
|
||
m.addAction(QStringLiteral("全选测线"), objectTree,
|
||
[objectTree]() { objectTree->setAllTmsChecked(true); });
|
||
m.addAction(QStringLiteral("取消全选"), objectTree,
|
||
[objectTree]() { objectTree->setAllTmsChecked(false); });
|
||
m.addAction(QStringLiteral("反选"), objectTree,
|
||
[objectTree]() { objectTree->invertTmChecks(); });
|
||
m.exec(objFilterBtn->mapToGlobal(QPoint(0, objFilterBtn->height())));
|
||
});
|
||
}
|
||
// 对象树表头「新建对象」按钮 → 小菜单(新建检测对象/新建方法对象,复用右键 newGs/newTm 流程)。
|
||
// 父对象 = 当前选中节点;未选中则取项目根(由 ObjectTreePanel::currentParentForNew 决定)。
|
||
if (auto* objAddBtn = findHeaderAction(objectBox, geopro::app::Glyph::Plus)) {
|
||
objAddBtn->setToolTip(QStringLiteral("新建对象"));
|
||
QObject::connect(
|
||
objAddBtn, &QToolButton::clicked, objectTree,
|
||
[objAddBtn, objectTree, &projectRepo, &nav, &window, toast]() {
|
||
const QString parentId = objectTree->currentParentForNew();
|
||
if (parentId.isEmpty()) {
|
||
toast(QStringLiteral("请先选择项目"));
|
||
return;
|
||
}
|
||
// 与右键 newGs/newTm 完全一致的对话框流程(文案统一:新建检测对象/新建方法对象)。
|
||
auto openForm = [&projectRepo, &nav, &window, toast, parentId](bool gs) {
|
||
auto* dlg = new geopro::app::ObjectFormDialog(projectRepo, nav.currentProjectId(),
|
||
&window);
|
||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||
if (gs) dlg->newGs(parentId); else dlg->newTm(parentId);
|
||
QObject::connect(dlg, &geopro::app::ObjectFormDialog::submitted, &window,
|
||
[&nav, toast](int) {
|
||
toast(QStringLiteral("新建成功"));
|
||
nav.switchProject(nav.currentProjectId());
|
||
});
|
||
dlg->open();
|
||
};
|
||
// 按选中类型决定菜单项:选 项目根/GS → 新建GS+TM;选 TM → 仅新建TM(同级)。
|
||
// 父对象由 currentParentForNew() 统一给出(TM→父GS、GS/根→自身、未选→根),三种情况均正确。
|
||
QMenu m(objectTree);
|
||
if (objectTree->currentSelectedConfType() != 2) // 非 TM:可新建检测对象(GS)
|
||
m.addAction(QStringLiteral("新建检测对象"), objectTree,
|
||
[openForm]() { openForm(true); });
|
||
m.addAction(QStringLiteral("新建方法对象"), objectTree,
|
||
[openForm]() { openForm(false); });
|
||
m.exec(objAddBtn->mapToGlobal(QPoint(0, objAddBtn->height())));
|
||
});
|
||
}
|
||
|
||
// 模型/插件目录:全局一次性拉取并缓存,供数据集右键「插件」子菜单同步填充。
|
||
auto modelsCache = std::make_shared<std::vector<geopro::data::ModelInfo>>();
|
||
{
|
||
auto* mReq = projectRepo.listModelsAsync();
|
||
QObject::connect(mReq, &geopro::data::NavRequest::done, &window,
|
||
[modelsCache](const QVariant& v) {
|
||
*modelsCache = qvariant_cast<std::vector<geopro::data::ModelInfo>>(v);
|
||
});
|
||
}
|
||
|
||
// ── 数据集列表右键菜单(数据集详情 / 属性 / 插件 / 导出 / 删除)──
|
||
datasetList->setContextMenuPolicy(Qt::CustomContextMenu);
|
||
QObject::connect(
|
||
datasetList, &QWidget::customContextMenuRequested, datasetList,
|
||
[datasetList, &detailCtrl, &nav, &projectRepo, &window, toast, modelsCache, propView](
|
||
const QPoint& pos) {
|
||
QTreeWidgetItem* item = datasetList->itemAt(pos);
|
||
if (!item || item->data(0, geopro::app::kDsLoadMoreRole).toBool()) return;
|
||
const QString dsId = item->data(0, geopro::app::kDsIdRole).toString();
|
||
if (dsId.isEmpty()) return;
|
||
const QString ddCode = item->data(0, geopro::app::kDsDdCodeRole).toString();
|
||
const QString dsName = item->data(0, geopro::app::kDsNameRole).toString();
|
||
QMenu menu(datasetList);
|
||
menu.addAction(QStringLiteral("数据集详情"), datasetList,
|
||
[&detailCtrl, dsId, ddCode, dsName]() {
|
||
detailCtrl.openDataset(dsId, ddCode, dsName);
|
||
});
|
||
menu.addAction(QStringLiteral("属性"), datasetList, [&nav, propView, dsId]() {
|
||
nav.selectDataset(dsId); // 只读元字段
|
||
propView->selectDataset(dsId); // 可编辑描述
|
||
});
|
||
menu.addSeparator();
|
||
QMenu* plugins = menu.addMenu(QStringLiteral("插件"));
|
||
if (modelsCache->empty()) {
|
||
plugins->addAction(QStringLiteral("(模型列表加载中…)"))->setEnabled(false);
|
||
} else {
|
||
for (const auto& m : *modelsCache) {
|
||
const QString mn = QString::fromStdString(m.scriptName);
|
||
plugins->addAction(mn, datasetList, [toast, mn]() {
|
||
toast(QStringLiteral("插件「%1」调用待接入").arg(mn));
|
||
});
|
||
}
|
||
}
|
||
menu.addAction(QStringLiteral("导出…"), datasetList, [toast]() {
|
||
// ds 右键上下文拿不到 tmTypeBaseConfId,空配置打开会直接「加载模板失败」。
|
||
// 暂不开对话框,提示改用批量导出(ExportDatasetDialog 保留供有 confId 场景调用)。
|
||
toast(QStringLiteral("从此处导出暂不可用(缺方法配置),请从批量导出使用"));
|
||
});
|
||
menu.addSeparator();
|
||
menu.addAction(QStringLiteral("删除"), datasetList, [&nav, &window, dsId, dsName]() {
|
||
const auto r = QMessageBox::question(
|
||
&window, QStringLiteral("删除确认"),
|
||
QStringLiteral("确定删除数据集「%1」?该操作不可撤销。").arg(dsName),
|
||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||
if (r == QMessageBox::Yes) nav.deleteDataset(dsId);
|
||
});
|
||
menu.exec(datasetList->viewport()->mapToGlobal(pos));
|
||
});
|
||
|
||
// 数据集表头「筛选」按钮 → 按类型 + 创建日期快速筛选(客户端隐藏不匹配行;状态跨弹出保留)。
|
||
if (auto* dsFilterBtn = findHeaderAction(datasetBox, geopro::app::Glyph::Filter)) {
|
||
dsFilterBtn->setToolTip(QStringLiteral("快速筛选"));
|
||
auto hiddenTypes = std::make_shared<QSet<QString>>(); // 当前被取消勾选的类型
|
||
auto minDate = std::make_shared<QDate>(); // 创建日期下限(无效=不限)
|
||
auto reapply = [datasetList, hiddenTypes, minDate]() {
|
||
QSet<QString> visible;
|
||
for (const QString& x : geopro::app::collectDatasetTypeNames(datasetList))
|
||
if (!hiddenTypes->contains(x)) visible.insert(x);
|
||
geopro::app::applyDatasetFilter(datasetList, visible, *minDate);
|
||
};
|
||
QObject::connect(
|
||
dsFilterBtn, &QToolButton::clicked, datasetList,
|
||
[datasetList, dsFilterBtn, hiddenTypes, minDate, reapply]() {
|
||
const QStringList types = geopro::app::collectDatasetTypeNames(datasetList);
|
||
QMenu m(datasetList);
|
||
if (types.isEmpty()) {
|
||
m.addAction(QStringLiteral("(当前无数据集)"))->setEnabled(false);
|
||
}
|
||
for (const QString& t : types) {
|
||
QAction* a = m.addAction(t);
|
||
a->setCheckable(true);
|
||
a->setChecked(!hiddenTypes->contains(t));
|
||
QObject::connect(a, &QAction::toggled, datasetList,
|
||
[hiddenTypes, reapply, t](bool on) {
|
||
if (on) hiddenTypes->remove(t);
|
||
else hiddenTypes->insert(t);
|
||
reapply();
|
||
});
|
||
}
|
||
m.addSeparator();
|
||
QMenu* dm = m.addMenu(QStringLiteral("创建日期"));
|
||
dm->addAction(QStringLiteral("全部"), datasetList,
|
||
[minDate, reapply]() { *minDate = QDate(); reapply(); });
|
||
dm->addAction(QStringLiteral("近 7 天"), datasetList, [minDate, reapply]() {
|
||
*minDate = QDate::currentDate().addDays(-7);
|
||
reapply();
|
||
});
|
||
dm->addAction(QStringLiteral("近 30 天"), datasetList, [minDate, reapply]() {
|
||
*minDate = QDate::currentDate().addDays(-30);
|
||
reapply();
|
||
});
|
||
m.addAction(QStringLiteral("清除筛选"), datasetList,
|
||
[hiddenTypes, minDate, reapply]() {
|
||
hiddenTypes->clear();
|
||
*minDate = QDate();
|
||
reapply();
|
||
});
|
||
m.exec(dsFilterBtn->mapToGlobal(QPoint(0, dsFilterBtn->height())));
|
||
});
|
||
}
|
||
// 数据集表头「上传」按钮 → 导入数据集(复用 ImportDatasetDialog,父对象=当前选中 TM)。
|
||
// 未选中 TM 时按钮禁用并提示先选方法对象(随选中变化动态启停)。
|
||
if (auto* dsUploadBtn = findHeaderAction(datasetBox, geopro::app::Glyph::Upload)) {
|
||
dsUploadBtn->setToolTip(QStringLiteral("导入数据集(需先选方法对象)"));
|
||
auto syncUploadEnabled = [dsUploadBtn, currentTmId]() {
|
||
const bool hasTm = !currentTmId->isEmpty();
|
||
dsUploadBtn->setEnabled(hasTm);
|
||
dsUploadBtn->setToolTip(hasTm ? QStringLiteral("导入数据集…")
|
||
: QStringLiteral("导入数据集(需先选方法对象)"));
|
||
};
|
||
syncUploadEnabled(); // 初始:未选 TM → 禁用
|
||
QObject::connect(objectTree, &geopro::app::ObjectTreePanel::objectClicked, dsUploadBtn,
|
||
[syncUploadEnabled](const QString&, int) { syncUploadEnabled(); });
|
||
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::structureLoaded,
|
||
dsUploadBtn,
|
||
[syncUploadEnabled](const QString&,
|
||
const std::vector<geopro::data::StructNode>&) {
|
||
syncUploadEnabled();
|
||
});
|
||
QObject::connect(
|
||
dsUploadBtn, &QToolButton::clicked, &window,
|
||
[currentTmId, &projectRepo, &nav, &window, toast]() {
|
||
if (currentTmId->isEmpty()) {
|
||
toast(QStringLiteral("请先选择方法对象(TM)再导入数据集"));
|
||
return;
|
||
}
|
||
auto* dlg = new geopro::app::ImportDatasetDialog(
|
||
projectRepo, nav.currentProjectId(), *currentTmId, &window);
|
||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||
QObject::connect(dlg, &geopro::app::ImportDatasetDialog::imported, &window,
|
||
[&nav, toast]() {
|
||
toast(QStringLiteral("导入成功"));
|
||
nav.switchProject(nav.currentProjectId());
|
||
});
|
||
dlg->open();
|
||
});
|
||
}
|
||
|
||
// 控制器异常/数据集表单 → 被动面板。
|
||
// 对象属性改为可编辑面板:由 ObjectTreePanel::objectSelectedForEdit 直接驱动(见下),
|
||
// 不再消费只读的 objectDetailLoaded。
|
||
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,
|
||
[removeTreeLoadMore, addTreeLoadMore, datasetList, datasetTitle, datasetTabs](
|
||
const QString&, const std::vector<geopro::data::DsRow>& rows, int total,
|
||
bool append) {
|
||
removeTreeLoadMore(datasetList);
|
||
geopro::app::populateDatasetList(datasetList, rows, append);
|
||
const int loaded = addTreeLoadMore(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_v3")).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_v3"), dockManager->saveState());
|
||
});
|
||
}
|
||
|
||
} // namespace
|
||
|
||
namespace {
|
||
// 顶层异常护栏:任何 slot / 事件处理器抛出的 C++ 异常都会经过 QApplication::notify。
|
||
// 默认 Qt 不允许异常穿透事件循环 → terminate(崩溃)。这里拦截 + 记录(异常信息 + 接收者
|
||
// 对象类名 + 事件类型,足以定位崩点),并吞掉以**保证后端故障等异常不致整个客户端退出**。
|
||
// 注:吞异常后该次事件处理中断,可能留下局部不一致;但"不崩 + 有日志"优先于直接退出。
|
||
class GuardedApplication : public QApplication {
|
||
public:
|
||
using QApplication::QApplication;
|
||
bool notify(QObject* receiver, QEvent* e) override {
|
||
try {
|
||
return QApplication::notify(receiver, e);
|
||
} catch (const std::exception& ex) {
|
||
qCritical("[guard] 拦截未捕获异常: %s | type=%s | receiver=%s | event=%d",
|
||
ex.what(), typeid(ex).name(),
|
||
receiver ? receiver->metaObject()->className() : "null",
|
||
e ? static_cast<int>(e->type()) : 0);
|
||
} catch (...) {
|
||
qCritical("[guard] 拦截未捕获非 std 异常 | receiver=%s | event=%d",
|
||
receiver ? receiver->metaObject()->className() : "null",
|
||
e ? static_cast<int>(e->type()) : 0);
|
||
}
|
||
return false;
|
||
}
|
||
};
|
||
} // namespace
|
||
|
||
int main(int argc, char* argv[])
|
||
{
|
||
// Qt WebEngine(地图页签的 QWebEngineView):必须在 QApplication 构造前初始化,
|
||
// 且需启用跨上下文共享 OpenGL(QtWebEngine 与 QVTK 同进程共用 GL context,避免黑屏/崩溃)。
|
||
// AA_ShareOpenGLContexts 须在 QApplication 之前设置;QtWebEngineQuick::initialize() 同样须前置。
|
||
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
|
||
QtWebEngineQuick::initialize();
|
||
|
||
// 高 DPI 缩放采用直通策略:在 125%/150% 等分数缩放下字体/图标按真实比例渲染,更清晰。
|
||
// 必须在 QApplication 构造前设置。
|
||
QApplication::setHighDpiScaleFactorRoundingPolicy(
|
||
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||
|
||
// QVTK 默认 surface format 必须在 QApplication 之前设置(全局一次,两个 QVTK widget 共用)。
|
||
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
|
||
GuardedApplication app(argc, argv); // 顶层异常护栏:slot/事件里的异常不致客户端崩溃
|
||
|
||
// 异步 ApiCall::finished 等信号携带 ApiResponse;注册元类型以支持跨 QueuedConnection 传递
|
||
// (当前详情链路为同线程 DirectConnection,非严格必需,但作防御性注册,见 spec §5.1)。
|
||
qRegisterMetaType<geopro::net::ApiResponse>();
|
||
|
||
// 组织/应用名:QSettings 持久化(dock 布局、登录记忆等)按此定位存储位置。
|
||
QCoreApplication::setOrganizationName(QStringLiteral("Geomative"));
|
||
QCoreApplication::setApplicationName(QStringLiteral("Geopro3"));
|
||
|
||
// 日志 + 崩溃捕获:尽早安装(依赖上面的 Org/App 名定位日志目录)。生产桌面端问题可回溯。
|
||
geopro::app::initLogging();
|
||
|
||
// 主题与字号:应用持久化偏好(跟随系统/浅/深 + 字号缩放),再套用 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 + 登录编排 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);
|
||
|
||
// 记住登录:若上次勾选「记住」且未超 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);
|
||
// 策略注册表须比 detailCtrl 活得久(同作用域、在前声明)。
|
||
geopro::controller::ChartStrategyRegistry chartRegistry;
|
||
chartRegistry.add(std::make_unique<geopro::app::ErtInversionStrategy>());
|
||
chartRegistry.add(std::make_unique<geopro::app::MeasurementStrategy>());
|
||
chartRegistry.add(std::make_unique<geopro::app::GrMeasurementStrategy>());
|
||
chartRegistry.add(std::make_unique<geopro::app::TrajectoryStrategy>());
|
||
chartRegistry.add(std::make_unique<geopro::app::GridStrategy>());
|
||
geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry);
|
||
|
||
// ── 外壳:标准 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, datasetRepo, 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();
|
||
}
|