// M1 工作台(视图重构 Task B):正确产品模型。 // - 左上 对象显示栏:GS→TM(测线,复选框)。勾选测线 → 在中央显示其 dd_section,可多条共存。 // - 左下 数据真实显示栏:单击测线 → 列其采集批次(数据集,tab 数据/文件)。单击采集批次 → 数据详情+异常+属性。 // - 中央「二维地图 / 三维视图」:两个互斥视图(内容不同,不是同一物体换相机)。 // 二维地图 = 对每个勾选数据集 buildSurveyLine(lat/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 彩色散点(buildScatter,x=距离/y=深度,按散点自带色阶上色)。 // 显示异常 = 在上图叠加异常圈定(buildAnomalies,dashed 折线,同纵向夸张对齐)。 // 两者皆平躺俯视正交 + 属性。 // - 右 属性:选中数据集属性文本。 // 世界系:启动 loadGrid("grid1") 取一次,用其 lat/lon 中位/均值作 GeoLocalFrame(全项目共享,保证多视图配准)。 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 #include #include #include #include #include #include #include namespace { // 角色:树 TM 项存 tmId(UserRole+2);数据列表 DS 项的 dsId/ddType 由 panels/DatasetListPanel 定义。 constexpr int kRoleTmId = Qt::UserRole + 2; // 从对象结构树构建 QTreeWidget:GS → TM 两层(对齐原型;DS=采集批次在左下「数据列表」,不进树)。 // TM(测线) 项可勾选(复选框):勾选驱动该测线的 dd_section 在中央场景显示;UserRole+2 存 tmId。 // 含 dd_section 的测线默认勾选,启动即显示。 void populateTree(QTreeWidget* tree, const std::vector& 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& 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 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(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 renderWindow; vtkWidget->setRenderWindow(renderWindow); renderWindow->AddRenderer(scene->renderer()); vtkRenderer* rendererPtr = scene->renderer(); vtkGenericOpenGLRenderWindow* renderWindowPtr = renderWindow.Get(); // 当前视图模式(全局共享,切视图/勾选时据此重建内容)。默认二维地图。 auto viewMode = std::make_shared(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 detailRenderWindow; vtkNew 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>(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 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(); auto detailMode = std::make_shared(DetailMode::Section18); auto showAnomalies = std::make_shared(true); // 默认显示异常(对齐原型) auto hiddenAnoms = std::make_shared>(); // 异常列表中被取消勾选(隐藏)的异常下标 // 按当前选中 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(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 + 登录编排 AuthService(RSA 公钥从 resources 读取)。 geopro::net::ApiClient api(QStringLiteral("http://tenant.geomative.cn/pop-api")); const std::string pem = readPem("D:/Git/lanbingtech/geopro/resources/rsa_public_key.pem"); geopro::net::AuthService auth(api, pem); // 先弹登录窗;用户取消/未登录则退出。 geopro::app::LoginWindow login(auth); if (login.exec() != QDialog::Accepted) return 0; api.setToken(login.token()); // 注入 token 供后续 API 使用 // 登录成功 → 构建并显示工作台。 geopro::data::LocalSampleRepository repo( "D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/"); QMainWindow window; window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)")); window.resize(1280, 800); buildWorkbench(window, repo); window.show(); return app.exec(); }