#include "api/Api3dRepository.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "algo/VolumeBuilder.hpp" // core::PointSet / BuiltVolume / buildVolume(含 Field.hpp) #include "api/DatasetLoadHandles.hpp" #include "GprVolumeRepository.hpp" // createGprVolumeGrid(§6 接入:GPR 体直产) #include "model/ColorScale.hpp" #include "model/detail/DetailPayloads.hpp" #include "repo/IAsyncDatasetRepository.hpp" namespace geopro::data { namespace { constexpr const char* kNotReady = "后端三维端点未就绪"; } // namespace Api3dRepository::Api3dRepository(IAsyncDatasetRepository& dsRepo, std::shared_ptr frame) : dsRepo_(dsRepo), frame_(std::move(frame)) {} DsDimension Api3dRepository::dimensionOf(const DsRow& ds) const { // 与 LocalSample3dRepository::dimensionOf 同口径(spec §6.1 ddCode→维度)。 // TODO(P3): 与 LocalSample3dRepository 重复,宜提取共享映射(后续清理)。 const std::string& c = ds.ddCode; if (c == "dd_voxel" || c == "dd_Structual3D" || c == "dd_Property3D" || c == "dd_section" || c == "dd_inversion_data") { return DsDimension::Dim3D; } if (c == "dd_slice") return DsDimension::Analysis3D; // 足迹型 → 二维数据集:地面 lat/lon 序列,平铺进地图。dd_trajectory_data = 统一通用轨迹 // (数据字典 DD0623「保留」,已并入 dd_radar_rtk_trajectory);瞬变电磁/雷达通道/RTK 轨迹字典均「删除」。 if (c == "dd_trajectory_data") return DsDimension::Dim2D; return DsDimension::Other; } void Api3dRepository::loadSection(const std::string& dsId, std::function onOk, OnError onErr) { // 真实帘面:复用 ApiDatasetRepository 的 ERT 反演网格端点(loaderKey="inversion.grid")。 // 命中载荷 = core::ContourPayload{grid, scale, anomalies};取 grid+scale 填 SectionData。 DetailLoad* load = dsRepo_.loadAsync("inversion.grid", dsId); if (load == nullptr) { onErr("Api3dRepository::loadSection: loadAsync 返回空句柄"); return; } // 以 load 为连接上下文 → 它 deleteLater 时自动断开;单线程下创建后立即连接安全。 QObject::connect(load, &DetailLoad::done, load, [onOk = std::move(onOk)](const QVariant& payload) { const auto cp = qvariant_cast(payload); SectionData s; s.grid = cp.grid; s.scale = cp.scale; onOk(std::move(s)); }); QObject::connect(load, &DetailLoad::failed, load, [onErr = std::move(onErr)](const QString& message) { onErr(message.toStdString()); }); } void Api3dRepository::loadMapLine(const std::string& dsId, std::function onOk, OnError onErr) { // 真实足迹:复用 ApiDatasetRepository 轨迹地图端点(loaderKey="traj.map" → dd/ert/trajectory/line, // frontCrsCode 固定 EPSG:4326)。命中载荷 = core::MapPayload{points[].lat/lon};取经纬填 MapLine。 DetailLoad* load = dsRepo_.loadAsync("traj.map", dsId); if (load == nullptr) { onErr("Api3dRepository::loadMapLine: loadAsync 返回空句柄"); return; } // 以 load 为连接上下文 → 它 deleteLater 时自动断开(与 loadSection 同范式)。 QObject::connect(load, &DetailLoad::done, load, [onOk = std::move(onOk)](const QVariant& payload) { const auto mp = qvariant_cast(payload); MapLine line; line.lat.reserve(mp.points.size()); line.lon.reserve(mp.points.size()); for (const auto& p : mp.points) { line.lat.push_back(p.lat); line.lon.push_back(p.lon); } onOk(std::move(line)); }); QObject::connect(load, &DetailLoad::failed, load, [onErr = std::move(onErr)](const QString& message) { onErr(message.toStdString()); }); } bool Api3dRepository::isVolumeDataset(const std::string& dsId) const { return volumes_.find(dsId) != volumes_.end(); } std::string Api3dRepository::createVolume(VolumeBuildParams params, const std::string& name) { const std::string id = "vol-" + std::to_string(++volumeCounter_); StoredVolume sv; sv.params = std::move(params); sv.name = name; sv.createTime = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString(); volumes_[id] = std::move(sv); return id; } std::string Api3dRepository::createVolume(const VoxelGenerateRequest& req) { const std::string id = createVolume(fromRequest(req), req.name); // 复用 mock 存储 + 惰性插值 if (auto it = volumes_.find(id); it != volumes_.end()) it->second.request = req; qInfo().noquote() << "[volreq] createVolume 请求体:" << QJsonDocument(req.toJson()).toJson(QJsonDocument::Compact); return id; } std::string Api3dRepository::createGprVolume(const std::string& lineDir, const std::string& linePrefix, const std::string& name, int coarse) { // 走 io::gpr 逐线管线(含线内通道插值)直接产体(抛异常透传给调用方)。 VolumeGrid grid = geopro::data::createGprVolumeGrid(lineDir, linePrefix, coarse); // 简易灰度色阶(负→暗、零→灰、正→亮)覆盖体值域,使体素渲染可见。 core::ColorScale scale; const double mid = 0.5 * (grid.vmin + grid.vmax); scale.addStop(grid.vmin, core::Rgba{20, 24, 40, 255}); scale.addStop(mid, core::Rgba{140, 140, 150, 255}); scale.addStop(grid.vmax, core::Rgba{235, 232, 220, 255}); const std::string id = "vol-" + std::to_string(++volumeCounter_); StoredVolume sv; sv.name = name; sv.createTime = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString(); sv.cachedGrid = std::move(grid); // 预填 → loadVolume 直接命中渲染(不走 mock IDW) sv.cachedScale = scale; volumes_[id] = std::move(sv); return id; } const VoxelGenerateRequest* Api3dRepository::lastVoxelRequest(const std::string& dsId) const { const auto it = volumes_.find(dsId); return (it != volumes_.end() && it->second.request) ? &*it->second.request : nullptr; } void Api3dRepository::clearMockData() { // 切换项目:清空内存态三维体/切片/异常,避免上个项目的产物残留进新项目列表。 volumes_.clear(); slices_.clear(); anomalies_.clear(); } std::vector Api3dRepository::volumeRows() const { std::vector rows; rows.reserve(volumes_.size()); for (const auto& [id, sv] : volumes_) { DsRow r; r.id = id; r.dsName = sv.name; r.ddCode = "dd_voxel"; r.typeName = "三维体"; r.structParentId = sv.request ? sv.request->structParentId : std::string(); // 结构归属(生成位置) r.createTime = sv.createTime; rows.push_back(std::move(r)); } return rows; } std::vector Api3dRepository::anomalyRows() const { std::vector rows; rows.reserve(anomalies_.size()); for (const auto& [id, sa] : anomalies_) { DsRow r; r.id = id; r.dsName = sa.a.name; r.ddCode = "dd_anomaly"; r.typeName = sa.a.typeName.empty() ? std::string("异常") : sa.a.typeName; r.createTime = sa.a.createTime; r.parentId = sa.a.remarkSourceId; // 挂归属实体(体/切片)下;三级树按 parentId 自动挂载 rows.push_back(std::move(r)); } return rows; } bool Api3dRepository::volumeInfo(const std::string& dsId, VolumeInfo& out) const { auto it = volumes_.find(dsId); if (it == volumes_.end()) return false; const StoredVolume& sv = it->second; out = VolumeInfo{}; out.params = sv.params; out.name = sv.name; out.loaded = sv.cachedGrid.has_value(); if (out.loaded) { const VolumeGrid& g = *sv.cachedGrid; out.vmin = g.vmin; out.vmax = g.vmax; out.nx = g.vol.nx(); out.ny = g.vol.ny(); out.nz = g.vol.nz(); out.dx = g.spacing[0]; out.dy = g.spacing[1]; out.dz = g.spacing[2]; out.pointCount = sv.pointCount.value_or(0); } return true; } void Api3dRepository::appendGridPoints(const core::Grid& g, core::PointSet& pts) const { const int nx = g.nx(), ny = g.ny(); if (nx < 1 || ny < 1 || g.y.size() < static_cast(ny)) return; // 与 CurtainActor::buildCurtain 同口径:有 lat/lon 用 frame.toLocal,否则退化用 g.x/0。 const bool hasLatLon = g.lat.size() >= static_cast(nx) && g.lon.size() >= static_cast(nx); for (int j = 0; j < ny; ++j) { for (int i = 0; i < nx; ++i) { const double val = g.valueAt(i, j); if (!std::isfinite(val)) continue; // 跳过无数据格(与帘面消隐一致,避免 NaN 入 IDW) double px, py; if (hasLatLon) { const auto p = frame_->toLocal(g.lat[i], g.lon[i]); px = p.x; py = p.y; } else { px = (g.x.size() > static_cast(i)) ? g.x[i] : static_cast(i); py = 0.0; } pts.x.push_back(px); pts.y.push_back(py); pts.z.push_back(g.y[j]); // 世界 Z = 高程(与 CurtainActor 一致) pts.v.push_back(val); } } } void Api3dRepository::loadSectionWithRetry(const std::string& dsId, int attemptsLeft, std::function onOk, OnError onErr) { loadSection(dsId, onOk, [this, dsId, attemptsLeft, onOk, onErr](const std::string& m) { if (attemptsLeft > 0) { // 瞬时失败(502 等)→ 重试,不立刻判整体失败 qInfo().noquote() << "[volbuild] source" << QString::fromStdString(dsId) << "加载失败,重试(剩" << attemptsLeft << "次):" << QString::fromStdString(m); loadSectionWithRetry(dsId, attemptsLeft - 1, onOk, onErr); return; } onErr(m); }); } void Api3dRepository::finalizeVolume(const std::string& dsId, const core::PointSet& pts, const core::ColorScale& scale, const VolumeBuildParams& params, std::function onOk, OnError onErr) { if (pts.v.empty()) { onErr("Api3dRepository::loadVolume: 配准后无有效散点(源数据为空或全无数据)"); return; } // 重 IDW 建体放后台线程,避免阻塞 UI(用户要求:渲染必须异步)。纯计算(无 Qt/VTK),算完 // 经事件循环回主线程做缓存 + 交付(缓存写 volumes_ / onOk 触碰 VTK 必须在主线程)。 // 兜底:无 QCoreApplication(headless/单测)时退化为同步,保证可测/可离屏。 auto deliver = [this, dsId, scale, onOk, onErr](std::shared_ptr bv, std::string err, std::size_t nPts) { if (!bv) { onErr(std::string("Api3dRepository::loadVolume: ") + err); return; } double vmin = bv->vmin, vmax = bv->vmax; const std::vector stops = scale.stopValues(); if (stops.size() >= 2) { vmin = stops.front(); vmax = stops.back(); } qInfo().noquote() << "[volbuild] finalize pts=" << nPts << "grid" << bv->spec.nx << "x" << bv->spec.ny << "x" << bv->spec.nz << "origin" << bv->spec.ox << bv->spec.oy << bv->spec.oz << "spacing" << bv->spec.dx << bv->spec.dy << bv->spec.dz; VolumeGrid out{std::move(bv->vol), {{bv->spec.ox, bv->spec.oy, bv->spec.oz}}, {{bv->spec.dx, bv->spec.dy, bv->spec.dz}}, vmin, vmax}; auto it = volumes_.find(dsId); if (it != volumes_.end()) { // 缓存明细 + 色阶(下次命中即跳重算) it->second.cachedGrid = out; it->second.cachedScale = scale; it->second.pointCount = nPts; // 持久化聚合散点数(详情统计用) } onOk(std::move(out), scale); }; // 纯计算闭包:返回 (built|nullptr, err, nPts)。 auto compute = [pts, params]() { std::shared_ptr bv; std::string err; try { bv = std::make_shared(geopro::core::buildVolume( pts, params.cellXY, params.cellZ, params.power, params.maxDist)); } catch (const std::exception& e) { err = e.what(); } return std::make_tuple(bv, err, pts.v.size()); }; qInfo().noquote() << "[volbuild] start dsId=" << QString::fromStdString(dsId) << "pts=" << pts.v.size() << "async=" << (QCoreApplication::instance() != nullptr); if (!QCoreApplication::instance()) { // 无事件循环(headless/单测)→ 同步 auto res = compute(); deliver(std::get<0>(res), std::get<1>(res), std::get<2>(res)); return; } std::thread([compute, deliver, dsId]() mutable { const auto t0 = std::chrono::steady_clock::now(); auto res = compute(); auto bv = std::get<0>(res); // 具名变量(非结构化绑定)→ C++17 可被 lambda 捕获 auto err = std::get<1>(res); auto nPts = std::get<2>(res); const auto ms = std::chrono::duration_cast( std::chrono::steady_clock::now() - t0).count(); qInfo().noquote() << "[volbuild] computed dsId=" << QString::fromStdString(dsId) << "ms=" << ms << "ok=" << (bv != nullptr); // 回主线程交付(QueuedConnection;qApp 为主线程对象,存活于整个会话)。 QMetaObject::invokeMethod( qApp, [deliver, bv, err, nPts]() mutable { deliver(std::move(bv), std::move(err), nPts); }, Qt::QueuedConnection); }).detach(); } void Api3dRepository::loadVolume(const std::string& dsId, std::function onOk, OnError onErr) { auto it = volumes_.find(dsId); if (it == volumes_.end()) { onErr("Api3dRepository::loadVolume: 未知三维体 " + dsId); return; } StoredVolume& sv = it->second; if (sv.cachedGrid) { // 明细命中 → 直接渲染(不重算) onOk(*sv.cachedGrid, sv.cachedScale); return; } const VolumeBuildParams params = sv.params; // 拷贝:异步回调期间存储可能变动 if (params.sourceDatasetIds.empty()) { onErr("Api3dRepository::loadVolume: 三维体无源数据集"); return; } // 多源扇出:每个源走 loadSection(与帘面同一 inversion.grid 路径 → 同系对齐), // 主线程聚合(loadSection 回调在主线程)。任一源失败 → 整体失败(只回一次)。 struct Agg { int pending; bool failed = false; core::PointSet pts; std::vector scales; // 收集所有源色阶 → 取 vmax 中位者定值域(不依赖到达顺序) }; auto agg = std::make_shared(); agg->pending = static_cast(params.sourceDatasetIds.size()); for (const std::string& srcId : params.sourceDatasetIds) { loadSectionWithRetry( srcId, /*attemptsLeft=*/2, [this, dsId, srcId, params, agg, onOk, onErr](SectionData s) { if (agg->failed) return; const std::size_t before = agg->pts.v.size(); appendGridPoints(s.grid, agg->pts); qInfo().noquote() << "[volbuild] source" << QString::fromStdString(srcId) << "grid" << s.grid.nx() << "x" << s.grid.ny() << "-> +" << (agg->pts.v.size() - before) << "pts (total" << agg->pts.v.size() << ")"; agg->scales.push_back(s.scale); if (--agg->pending > 0) return; // 还有源未到齐 // 值域定法(修偶发"淡蓝/几乎不可见"根因):旧逻辑取「首个到达源」的色阶 → 多条线值域 // 不一(如多条 2168、一条 24550)时随异步到达顺序抖动;取到大值域那条会把数据全压到 // 色阶低端→全蓝近透明。改为取所有源色阶按 vmax 排序的中位者:确定性(去到达顺序依赖) // + 抗单条线值域离群 → 多数线的正常值域稳定胜出。 auto& ss = agg->scales; std::sort(ss.begin(), ss.end(), [](const core::ColorScale& a, const core::ColorScale& b) { const auto av = a.stopValues(), bv = b.stopValues(); return (av.empty() ? 0.0 : av.back()) < (bv.empty() ? 0.0 : bv.back()); }); const core::ColorScale chosen = ss.empty() ? core::ColorScale{} : ss[ss.size() / 2]; finalizeVolume(dsId, agg->pts, chosen, params, onOk, onErr); }, [agg, onErr](const std::string& m) { if (agg->failed) return; agg->failed = true; onErr("Api3dRepository::loadVolume 源加载失败: " + m); }); } } void Api3dRepository::loadTerrainPaths(std::function /*onOk*/, OnError onErr) { onErr(kNotReady); // 后端地形 DEM/影像端点未就绪 } // ── 切片 CRUD(后端无切片端点 → 内存 mock;端点就绪后换实现)──────────────── std::vector Api3dRepository::sliceRows() const { std::vector rows; rows.reserve(slices_.size()); for (const auto& [id, ss] : slices_) { DsRow r; r.id = id; r.dsName = ss.name; r.ddCode = "dd_slice"; r.typeName = "切片"; r.parentId = ss.spec.volumeDsId; // 树中挂在所属三维体下 r.createTime = ss.createTime; rows.push_back(std::move(r)); } return rows; } bool Api3dRepository::isSliceDataset(const std::string& dsId) const { return slices_.find(dsId) != slices_.end(); } bool Api3dRepository::isAnomalyDataset(const std::string& dsId) const { return anomalies_.find(dsId) != anomalies_.end(); } bool Api3dRepository::anomalyById(const std::string& anomalyId, geopro::core::Anomaly& out) const { const auto it = anomalies_.find(anomalyId); if (it == anomalies_.end()) return false; out = it->second.a; return true; } bool Api3dRepository::sliceSpec(const std::string& dsId, SliceSpec& out) const { auto it = slices_.find(dsId); if (it == slices_.end()) return false; out = it->second.spec; return true; } void Api3dRepository::createSlice(const SliceSpec& spec, const std::string& name, std::function onOk, OnError /*onErr*/) { const std::string id = "slice-" + std::to_string(++sliceCounter_); slices_[id] = StoredSlice{ spec, name, QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString()}; // 打印切片登记请求体(对齐 SliceGenerateRequest,端点 POST /business/dsObject/slice/generate)供后端联调。 // projectId 由真实请求层据当前项目填充(mock 仓储无项目上下文,此处留空)。 SliceGenerateRequest req; req.volumeDsId = spec.volumeDsId; req.name = name; req.axis = spec.axis; req.origin = spec.origin; req.point1 = spec.point1; req.point2 = spec.point2; req.colorScaleId = spec.colorScaleId; qInfo().noquote() << "[slicereq] registerSlice 请求体:" << QJsonDocument(req.toJson()).toJson(QJsonDocument::Compact); onOk(id); } void Api3dRepository::saveSlice(const std::string& dsId, const SliceSpec& spec, std::function onOk, OnError /*onErr*/) { auto it = slices_.find(dsId); if (it != slices_.end()) it->second.spec = spec; // 覆盖位姿 onOk(); } void Api3dRepository::deleteSlice(const std::string& dsId, std::function onOk, OnError /*onErr*/) { slices_.erase(dsId); onOk(); } void Api3dRepository::setSliceColorScale(const std::string& dsId, const geopro::core::ColorScale& cs) { auto it = slices_.find(dsId); if (it == slices_.end()) return; it->second.colorScale = cs; // 切片独立色阶(mock;真实后端走该切片 dsId 的 colorGradation) it->second.hasColorScale = true; } bool Api3dRepository::sliceColorScale(const std::string& dsId, geopro::core::ColorScale& out) const { auto it = slices_.find(dsId); if (it == slices_.end() || !it->second.hasColorScale) return false; out = it->second.colorScale; return true; } // ── 异常 / 异常体(后端真实端点存在,但异常挂三维体、三维体仍 mock → 异常暂内存 mock; // 挂载结构按"异常→三维体",整链端点就绪后切真实,见记忆 vtk-3d-persistence-structure)── void Api3dRepository::loadAnomalyTree(const std::string& remarkSourceId, std::function onOk, OnError /*onErr*/) { // 按归属实体(体/切片)过滤;按 consortiumId 分组(异常体),空 consortiumId → loose(未分组)。 AnomalyTree tree; std::map bodyIndex; // consortiumId → tree.bodies 下标 for (const auto& [id, sa] : anomalies_) { if (!remarkSourceId.empty() && sa.a.remarkSourceId != remarkSourceId) continue; if (sa.a.consortiumId.empty()) { tree.loose.push_back(sa.a); continue; } auto it = bodyIndex.find(sa.a.consortiumId); if (it == bodyIndex.end()) { it = bodyIndex.emplace(sa.a.consortiumId, tree.bodies.size()).first; AnomalyBody body; body.id = sa.a.consortiumId; body.name = sa.a.consortiumId; // mock:名同 id(真实异常体有独立 name/typeName) tree.bodies.push_back(std::move(body)); } tree.bodies[it->second].members.push_back(sa.a); } onOk(std::move(tree)); } void Api3dRepository::saveAnomaly(const geopro::core::Anomaly& a, const std::string& screenshotPngPath, std::function onOk, OnError /*onErr*/) { std::string id = a.id; if (id.empty()) id = "anomaly-" + std::to_string(++anomalyCounter_); // 新建 → 生成 id geopro::core::Anomaly stored = a; stored.id = id; if (stored.createTime.empty()) // mock:构建时未设创建时刻 → 补当前时间(副标题/列表显示) stored.createTime = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString(); anomalies_[id] = StoredAnomaly{std::move(stored), screenshotPngPath}; onOk(id); } void Api3dRepository::deleteAnomaly(const std::string& anomalyId, std::function onOk, OnError /*onErr*/) { anomalies_.erase(anomalyId); onOk(); } void Api3dRepository::deleteAnomalyGroup(const std::string& bodyId, std::function onOk, OnError /*onErr*/) { // 删除该异常体分组下所有异常(mock:consortiumId == bodyId 的全删)。 for (auto it = anomalies_.begin(); it != anomalies_.end();) { if (it->second.a.consortiumId == bodyId) it = anomalies_.erase(it); else ++it; } onOk(); } // ── 任务管理(load 回空列表避免 UI 崩)────────────────────────────────────── void Api3dRepository::loadTaskRecords(const std::string& /*dsId*/, std::function)> onOk, OnError /*onErr*/) { onOk({}); // 后端未就绪 → 空记录 } void Api3dRepository::loadUsableTasks(const std::string& /*ddCode*/, std::function)> onOk, OnError /*onErr*/) { onOk({}); // 后端未就绪 → 空列表 } } // namespace geopro::data