geopro/src/app/main.cpp

529 lines
26 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 体素引擎(render::buildVoxelFromScatters)已就绪并验证,但**不**作为工具条开关
// (那样与二维/三维平级会令人困惑、且不在原型);待做 3D 图层控制(对齐原型「视图详情」浮层)再接。
// - 下方「数据详情」:独立 QVTK 小视图 + 工具条「原数据 / 网格数据」切换 +「显示异常」开关(对齐原型)。
// 单击某 DS → 显示该数据集:
// 网格数据 = #18 banded 等值面+等值线(两 actor SetScale(1,1.5,1) 纵向夸张)。
// 原数据 = #17 彩色散点buildScatterx=距离/y=深度,按散点自带色阶上色)。
// 显示异常 = 在上图叠加异常圈定buildAnomaliesdashed 折线,同纵向夸张对齐)。
// 两者皆平躺俯视正交 + 属性。
// - 右 属性:选中数据集属性文本。
// 世界系:启动 loadGrid("grid1") 取一次,用其 lat/lon 中位/均值作 GeoLocalFrame全项目共享保证多视图配准
#include <fstream>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include <QActionGroup>
#include <QApplication>
#include <QDialog>
#include <QLabel>
#include <QListWidget>
#include <QListWidgetItem>
#include <QSignalBlocker>
#include <QTabWidget>
#include <QMainWindow>
#include <QSurfaceFormat>
#include <QToolBar>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QVBoxLayout>
#include <QWidget>
#include <DockManager.h>
#include <DockWidget.h>
#include "model/ColorScale.hpp"
#include "model/Field.hpp"
#include "repo/LocalSampleRepository.hpp"
#include "ApiClient.hpp"
#include "AuthService.hpp"
#include "login/LoginWindow.hpp"
#include "panels/AnomalyListPanel.hpp"
#include "panels/DatasetListPanel.hpp"
#include "CameraPreset.hpp"
#include "Scene.hpp"
#include "actors/AnomalyActor.hpp"
#include "actors/CurtainActor.hpp"
#include "actors/GridContourActor.hpp"
#include "actors/MapLineActor.hpp"
#include "actors/ScatterActor.hpp"
#include "geo/GeoLocalFrame.hpp"
#include <algorithm>
#include <memory>
#include <set>
#include <vector>
#include <QVTKOpenGLStereoWidget.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkRenderer.h>
#include <vtkSmartPointer.h>
namespace {
// 角色:树 TM 项存 tmId(UserRole+2);数据列表 DS 项的 dsId/ddType 由 panels/DatasetListPanel 定义。
constexpr int kRoleTmId = Qt::UserRole + 2;
// 从对象结构树构建 QTreeWidgetGS → TM 两层对齐原型DS=采集批次在左下「数据列表」,不进树)。
// TM(测线) 项可勾选(复选框):勾选驱动该测线的 dd_section 在中央场景显示UserRole+2 存 tmId。
// 含 dd_section 的测线默认勾选,启动即显示。
void populateTree(QTreeWidget* tree, const std::vector<geopro::data::GsNode>& gss)
{
for (const auto& gs : gss) {
auto* gsItem = new QTreeWidgetItem(tree);
gsItem->setText(0, QString::fromStdString(gs.name));
for (const auto& tm : gs.tms) {
auto* tmItem = new QTreeWidgetItem(gsItem);
tmItem->setText(0, QString::fromStdString(tm.name));
tmItem->setData(0, kRoleTmId, QString::fromStdString(tm.id));
tmItem->setFlags(tmItem->flags() | Qt::ItemIsUserCheckable);
const bool hasSection =
std::any_of(tm.dss.begin(), tm.dss.end(),
[](const geopro::data::DsNode& d) { return d.ddType == "dd_section"; });
tmItem->setCheckState(0, hasSection ? Qt::Checked : Qt::Unchecked);
}
}
tree->expandAll();
}
// 在结构中按 tmId 查 TM找不到返回 nullptr。
const geopro::data::TmNode* findTm(const std::vector<geopro::data::GsNode>& gss,
const std::string& tmId)
{
for (const auto& gs : gss)
for (const auto& tm : gs.tms)
if (tm.id == tmId) return &tm;
return nullptr;
}
// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。
std::string readPem(const std::string& path)
{
std::ifstream in(path, std::ios::binary);
if (!in) return {};
std::ostringstream ss;
ss << in.rdbuf();
return ss.str();
}
// 取 vector 中位数(用于由测线 lat/lon 推世界系原点)。空则返回 0。
double median(std::vector<double> v)
{
if (v.empty()) return 0.0;
std::sort(v.begin(), v.end());
const size_t n = v.size();
return n % 2 ? v[n / 2] : 0.5 * (v[n / 2 - 1] + v[n / 2]);
}
// 当前中央视图(默认二维地图)。二维地图=测线红线俯视;三维视图=断面墙。
enum class ViewMode { Map2D, View3D };
// 数据详情显示内容(默认网格数据)。网格数据=#18 banded原数据=#17 散点(对齐原型命名)。
enum class DetailMode { Section18, Scatter17 };
// #17 散点屏幕像素方块边长。
constexpr float kScatterPointSize = 4.0F;
// 纵向夸张倍数:三维断面墙沿 z 拉伸成墙;数据详情 #18 沿 y 拉伸填面板。
constexpr double kCurtainZScale = 3.0;
constexpr double kDetailYScale = 1.5;
// 在给定 QMainWindow 上构建 M1 工作台。
// repo 生命周期须覆盖到事件循环结束(由调用方保证)。
void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo)
{
// ── 世界系:启动取一次 grid1 的 lat/lon用中位数作 GeoLocalFrame 原点 ──
// 全项目共享shared_ptr 持有):所有帘面用同一 frame 投影,保证多条测线空间配准。
const auto baseGrid = repo.loadGrid("grid1");
const double lat0 = median(baseGrid.lat);
const double lon0 = median(baseGrid.lon);
auto frame = std::make_shared<geopro::core::GeoLocalFrame>(lat0, lon0);
// ── 中央 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);
auto* dockManager = new ads::CDockManager(&window);
window.setCentralWidget(dockManager);
// 中央容器:顶部「二维地图/三维视图」工具条 + 下方 QVTK 视图。
auto* centerWidget = new QWidget();
auto* centerLayout = new QVBoxLayout(centerWidget);
centerLayout->setContentsMargins(0, 0, 0, 0);
centerLayout->setSpacing(0);
// 工具条:「二维地图/三维视图」两个互斥可勾选 action。切换=按当前勾选集重建对应内容。默认二维地图。
auto* viewToolBar = new QToolBar();
auto* viewGroup = new QActionGroup(viewToolBar);
viewGroup->setExclusive(true);
auto* act2D = viewToolBar->addAction(QStringLiteral("二维地图"));
auto* act3D = viewToolBar->addAction(QStringLiteral("三维视图"));
act2D->setCheckable(true);
act3D->setCheckable(true);
viewGroup->addAction(act2D);
viewGroup->addAction(act3D);
act2D->setChecked(true); // 默认二维地图
centerLayout->addWidget(viewToolBar);
centerLayout->addWidget(vtkWidget, 1);
auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图"));
vtkDock->setWidget(centerWidget);
auto* centerDockArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, vtkDock);
// ── 下方「数据详情」dock独立 QVTK 小视图(独立 renderer/renderWindow──
// 单击 DS → 显示该数据集平面反演剖面(#18 banded平躺俯视正交
auto* detailWidget = new QVTKOpenGLStereoWidget();
vtkNew<vtkGenericOpenGLRenderWindow> detailRenderWindow;
vtkNew<vtkRenderer> detailRenderer;
detailRenderer->SetBackground(1.0, 1.0, 1.0); // 白底
detailWidget->setRenderWindow(detailRenderWindow);
detailRenderWindow->AddRenderer(detailRenderer);
vtkRenderer* detailRendererPtr = detailRenderer.Get();
vtkGenericOpenGLRenderWindow* detailRenderWindowPtr = detailRenderWindow.Get();
// 数据详情容器:顶部「反演剖面/原数据」工具条 + 下方 QVTK 小视图。
auto* detailContainer = new QWidget();
auto* detailLayout = new QVBoxLayout(detailContainer);
detailLayout->setContentsMargins(0, 0, 0, 0);
detailLayout->setSpacing(0);
// 工具条对齐原型:「原数据 | 网格数据」互斥 +「显示异常」开关。
auto* detailToolBar = new QToolBar();
auto* detailGroup = new QActionGroup(detailToolBar);
detailGroup->setExclusive(true);
auto* actScatter = detailToolBar->addAction(QStringLiteral("原数据"));
auto* actSection = detailToolBar->addAction(QStringLiteral("网格数据"));
actScatter->setCheckable(true);
actSection->setCheckable(true);
detailGroup->addAction(actScatter);
detailGroup->addAction(actSection);
actSection->setChecked(true); // 默认网格数据 (#18)
detailToolBar->addSeparator();
auto* actShowAnomaly = detailToolBar->addAction(QStringLiteral("显示异常"));
actShowAnomaly->setCheckable(true);
actShowAnomaly->setChecked(true); // 默认显示异常(对齐原型 ☑显示异常)
detailLayout->addWidget(detailToolBar);
detailLayout->addWidget(detailWidget, 1);
auto* detailDock = new ads::CDockWidget(QStringLiteral("数据详情"));
detailDock->setWidget(detailContainer);
// 放在中央视图下方。
dockManager->addDockWidget(ads::BottomDockWidgetArea, detailDock, centerDockArea);
// 项目结构(GS→TM→DS):取一次共享,供树/中央/数据列表查 TM 的数据集。
auto structure = std::make_shared<std::vector<geopro::data::GsNode>>(repo.loadStructure());
// 左上 dock对象树(GS→TM,测线复选)。
auto* tree = new QTreeWidget();
tree->setHeaderLabel(QStringLiteral("对象显示栏"));
populateTree(tree, *structure);
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象显示栏"));
leftDock->setWidget(tree);
auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
// 左下 dock数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
auto* datasetTabs = new QTabWidget();
auto* datasetList = new QListWidget();
datasetList->setAlternatingRowColors(true);
datasetTabs->addTab(datasetList, QStringLiteral("数据"));
auto* fileList = new QListWidget(); // M1 文件 tab 占位
datasetTabs->addTab(fileList, QStringLiteral("文件"));
auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏"));
datasetDock->setWidget(datasetTabs);
dockManager->addDockWidget(ads::BottomDockWidgetArea, datasetDock, leftArea);
// 右上 dock异常列表对齐原型颜色块 + 名称 + 位置/深/尺寸 + 勾选显隐,与数据详情异常联动)。
auto* anomalyList = new QListWidget();
anomalyList->setAlternatingRowColors(true);
auto* anomalyDock = new ads::CDockWidget(QStringLiteral("异常列表"));
anomalyDock->setWidget(anomalyList);
auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, anomalyDock);
// 右下 dock属性。
auto* propLabel = new QLabel(QStringLiteral("(单击左侧数据集查看属性与平面剖面)"));
propLabel->setWordWrap(true);
propLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
propLabel->setMargin(8);
auto* propDock = new ads::CDockWidget(QStringLiteral("属性"));
propDock->setWidget(propLabel);
dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea);
// ── 中央视图重建(核心)─────────────────────────────────────────────
// 按勾选的测线(TM)整体重建scene.clear() → 对每个勾选 TM 的 dd_section 加对应 actor。
// 二维地图 = buildSurveyLine红线俯视浅底背景+ applyTop2D。
// 三维视图 = buildCurtain断面墙SetScale(1,1,kCurtainZScale) + applyFree3D白底
// frame/structure 全局共享;切视图/勾选变化都调用此函数重建当前视图。
auto rebuildCentral = [scene, rendererPtr, renderWindowPtr, viewMode, &repo, frame, tree,
structure]() {
scene->clear();
const bool is2D = (*viewMode == ViewMode::Map2D);
rendererPtr->SetBackground(is2D ? 0.96 : 1.0, is2D ? 0.97 : 1.0, is2D ? 0.99 : 1.0);
// 渲染单个 dd_section 数据集到当前视图。
auto renderSection = [&](const std::string& id) {
const auto g = repo.loadGrid(id);
if (is2D) {
auto line = geopro::render::buildSurveyLine(g, *frame);
if (line) scene->addActor(line);
} else {
const auto cs = repo.loadColorScale(id);
auto curtain = geopro::render::buildCurtain(g, cs, *frame);
if (curtain) {
curtain->SetScale(1.0, 1.0, kCurtainZScale); // 纵向夸张成墙
scene->addActor(curtain);
}
}
};
// 遍历对象树收集所有勾选的测线(TM),渲染其 dd_section 数据集(可多条共存)。
QList<QTreeWidgetItem*> stack;
for (int i = 0; i < tree->topLevelItemCount(); ++i) stack.append(tree->topLevelItem(i));
while (!stack.isEmpty()) {
QTreeWidgetItem* cur = stack.takeFirst();
for (int i = 0; i < cur->childCount(); ++i) stack.append(cur->child(i));
const QString tmId = cur->data(0, kRoleTmId).toString();
if (tmId.isEmpty()) continue; // GS 节点忽略
if (cur->checkState(0) != Qt::Checked) continue; // 仅显示勾选的测线
const auto* tm = findTm(*structure, tmId.toStdString());
if (!tm) continue;
for (const auto& ds : tm->dss)
if (ds.ddType == "dd_section") renderSection(ds.id);
}
if (is2D)
geopro::render::applyTop2D(rendererPtr);
else
geopro::render::applyFree3D(rendererPtr);
rendererPtr->ResetCamera();
renderWindowPtr->Render();
};
// 勾选/取消某测线(TM) → 重建当前视图内容(勾的才显示;可多条共存)。
QObject::connect(tree, &QTreeWidget::itemChanged, tree,
[rebuildCentral](QTreeWidgetItem* item, int) {
if (item->data(0, kRoleTmId).toString().isEmpty()) return; // GS 忽略
rebuildCentral();
});
// 单击测线(TM) → 左下数据列表填充其采集批次(数据集)。
QObject::connect(tree, &QTreeWidget::itemClicked, tree,
[structure, datasetList](QTreeWidgetItem* item, int) {
const QString tmId = item->data(0, kRoleTmId).toString();
if (tmId.isEmpty()) return; // GS 节点无数据集
const auto* tm = findTm(*structure, tmId.toStdString());
if (tm) geopro::app::populateDatasetList(datasetList, tm->dss);
});
// ── 数据详情共享状态 + 重建 ──────────────────────────────────────────
// 当前选中数据集 id空=未选)与详情显示模式(反演剖面/原数据);切模式或换选中都重建。
auto currentDsId = std::make_shared<QString>();
auto detailMode = std::make_shared<DetailMode>(DetailMode::Section18);
auto showAnomalies = std::make_shared<bool>(true); // 默认显示异常(对齐原型)
auto hiddenAnoms = std::make_shared<std::set<int>>(); // 异常列表中被取消勾选(隐藏)的异常下标
// 按当前选中 DS + 详情模式重建下方数据详情(平躺俯视正交,纵向夸张填面板)。
// 勾选「显示异常」时在 #18/#17 上叠加异常 dashed 折线(同纵向夸张对齐)。
auto rebuildDetail = [&repo, detailRendererPtr, detailRenderWindowPtr, currentDsId, detailMode,
showAnomalies, hiddenAnoms]() {
detailRendererPtr->RemoveAllViewProps();
if (currentDsId->isEmpty()) { // 未选数据集:清空即可
detailRenderWindowPtr->Render();
return;
}
const std::string id = currentDsId->toStdString();
if (*detailMode == DetailMode::Section18) {
// 网格数据:#18 banded 等值面 + 等值线,两 actor 纵向夸张 1.5x(沿 y
const auto g = repo.loadGrid(id);
const auto cs = repo.loadColorScale(id);
const auto actors = geopro::render::buildGridContour(g, cs);
if (actors.bands) {
actors.bands->SetScale(1.0, kDetailYScale, 1.0);
detailRendererPtr->AddViewProp(actors.bands);
}
if (actors.edges) {
actors.edges->SetScale(1.0, kDetailYScale, 1.0);
detailRendererPtr->AddViewProp(actors.edges);
}
} else {
// 原数据:#17 彩色散点,用散点自带色阶;纵向夸张同剖面以对齐观感。
const auto s = repo.loadScatter(id);
const auto scs = repo.loadScatterColorScale(id);
auto a = geopro::render::buildScatter(s, scs, kScatterPointSize);
if (a) {
a->SetScale(1.0, kDetailYScale, 1.0);
detailRendererPtr->AddViewProp(a);
}
}
// 异常叠加(与剖面同坐标系/同纵向夸张)。逐异常构建以按列表显隐(下标=原 vector 序)过滤。
if (*showAnomalies) {
const auto anomalies = repo.loadAnomalies(id);
for (int i = 0; i < static_cast<int>(anomalies.size()); ++i) {
if (hiddenAnoms->count(i)) continue; // 列表中取消勾选→隐藏
for (auto& act : geopro::render::buildAnomalies({anomalies[i]})) {
act->SetScale(1.0, kDetailYScale, 1.0);
detailRendererPtr->AddViewProp(act);
}
}
}
geopro::render::applyTop2D(detailRendererPtr);
detailRendererPtr->ResetCamera();
detailRenderWindowPtr->Render();
};
// 加载某数据集到「数据详情 + 异常列表 + 属性」(数据列表单击与启动默认共用)。
auto loadDataset = [&repo, propLabel, currentDsId, rebuildDetail, anomalyList, hiddenAnoms](
const QString& dsId, const QString& name) {
if (dsId.isEmpty()) return;
*currentDsId = dsId;
// 右上异常列表:按该数据集异常重填(默认全显);先清隐藏集再填,避免重建时阻塞信号回灌。
const auto anomalies = repo.loadAnomalies(dsId.toStdString());
hiddenAnoms->clear();
{
const QSignalBlocker block(anomalyList); // 重填触发 itemChanged先屏蔽
geopro::app::populateAnomalyList(anomalyList, anomalies);
}
rebuildDetail();
// 右下属性(数据集级,与详情模式无关)。
const auto g = repo.loadGrid(dsId.toStdString());
propLabel->setText(
QStringLiteral("数据集: %1\n类型: 剖面网格 (dd_section)\n网格: %2 x %3\n"
"vmin / vmax: %4 / %5\n异常: %6 个")
.arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax)
.arg(anomalies.size()));
};
// ── 单击左下数据列表的采集批次(DS) → 加载到数据详情/异常/属性 ──
QObject::connect(datasetList, &QListWidget::itemClicked, datasetList,
[loadDataset](QListWidgetItem* item) {
const QString dsId = item->data(geopro::app::kDsIdRole).toString();
const QString ddType = item->data(geopro::app::kDsDdTypeRole).toString();
if (ddType != "dd_section") return; // 仅剖面网格有详情图
const QString name =
item->data(Qt::DisplayRole).toString().section('\n', 0, 0);
loadDataset(dsId, name);
});
// ── 异常列表勾选(显隐) → 更新隐藏集 → 重建数据详情 ──
QObject::connect(anomalyList, &QListWidget::itemChanged, anomalyList,
[hiddenAnoms, rebuildDetail](QListWidgetItem* item) {
const int idx = item->data(geopro::app::kAnomalyIndexRole).toInt();
if (item->checkState() == Qt::Checked)
hiddenAnoms->erase(idx);
else
hiddenAnoms->insert(idx);
rebuildDetail();
});
// ── 数据详情工具条「反演剖面/原数据」:切模式 → 重建数据详情 ──
QObject::connect(actSection, &QAction::triggered, detailWidget,
[detailMode, rebuildDetail]() {
*detailMode = DetailMode::Section18;
rebuildDetail();
});
QObject::connect(actScatter, &QAction::triggered, detailWidget,
[detailMode, rebuildDetail]() {
*detailMode = DetailMode::Scatter17;
rebuildDetail();
});
// ──「显示异常」开关:切换异常叠加 → 重建数据详情 ──
QObject::connect(actShowAnomaly, &QAction::toggled, detailWidget,
[showAnomalies, rebuildDetail](bool on) {
*showAnomalies = on;
rebuildDetail();
});
// ── 工具条「二维地图/三维视图」:切换互斥视图 → 按当前勾选集重建对应内容 ──
QObject::connect(act2D, &QAction::triggered, vtkWidget, [viewMode, rebuildCentral]() {
*viewMode = ViewMode::Map2D;
rebuildCentral();
});
QObject::connect(act3D, &QAction::triggered, vtkWidget, [viewMode, rebuildCentral]() {
*viewMode = ViewMode::View3D;
rebuildCentral();
});
// ── 启动默认:测线已勾选,但 itemChanged 在 connect 之前触发故未渲染;这里重建一次中央内容。
rebuildCentral();
// 启动默认:选第一个含 dd_section 的测线 → 填充数据列表 + 加载其首个 dd_section 详情(对齐原型)。
for (const auto& gs : *structure) {
const geopro::data::TmNode* picked = nullptr;
for (const auto& tm : gs.tms) {
const bool hasSection =
std::any_of(tm.dss.begin(), tm.dss.end(),
[](const geopro::data::DsNode& d) { return d.ddType == "dd_section"; });
if (hasSection) { picked = &tm; break; }
}
if (!picked) continue;
geopro::app::populateDatasetList(datasetList, picked->dss);
for (const auto& ds : picked->dss)
if (ds.ddType == "dd_section") {
loadDataset(QString::fromStdString(ds.id), QString::fromStdString(ds.name));
break;
}
break;
}
}
} // namespace
int main(int argc, char* argv[])
{
// QVTK 默认 surface format 必须在 QApplication 之前设置(全局一次,两个 QVTK widget 共用)。
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
QApplication app(argc, argv);
// 网络层:共享会话 ApiClient + 登录编排 AuthServiceRSA 公钥从 resources 读取)。
geopro::net::ApiClient api(QStringLiteral("http://tenant.geomative.cn/pop-api"));
const std::string pem = readPem("D:/Git/lanbingtech/geopro/resources/rsa_public_key.pem");
geopro::net::AuthService auth(api, pem);
// 先弹登录窗;用户取消/未登录则退出。
geopro::app::LoginWindow login(auth);
if (login.exec() != QDialog::Accepted) return 0;
api.setToken(login.token()); // 注入 token 供后续 API 使用
// 登录成功 → 构建并显示工作台。
geopro::data::LocalSampleRepository repo(
"D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/");
QMainWindow window;
window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"));
window.resize(1280, 800);
buildWorkbench(window, repo);
window.show();
return app.exec();
}