#include "api/Api3dRepository.hpp" #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 "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 序列,平铺进地图(spec §4.1/§4.2)。 if (c == "dd_trajectory_data" || c == "dd_transient_electromagnetic_trajectory_data" || c == "dd_radar_channel_trajectory" || c == "dd_radar_rtk_trajectory") { 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_); volumes_[id] = StoredVolume{std::move(params), name, std::nullopt}; 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; } 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; } 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 = "三维体"; 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::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; } try { geopro::core::BuiltVolume bv = geopro::core::buildVolume(pts, params.cellXY, params.cellZ, params.power, params.maxDist); // 值域:优先色阶分段值,否则 buildVolume 的数据实测范围。 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=" << pts.v.size() << "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 = pts.v.size(); // 持久化聚合散点数(详情统计用) } onOk(std::move(out), scale); } catch (const std::exception& e) { onErr(std::string("Api3dRepository::loadVolume: ") + e.what()); } } 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; core::ColorScale scale; // 取首个到达源的色阶定值域 bool haveScale = false; }; auto agg = std::make_shared(); agg->pending = static_cast(params.sourceDatasetIds.size()); for (const std::string& srcId : params.sourceDatasetIds) { loadSection( srcId, [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() << ")"; if (!agg->haveScale) { agg->scale = s.scale; agg->haveScale = true; } if (--agg->pending > 0) return; // 还有源未到齐 finalizeVolume(dsId, agg->pts, agg->scale, 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; // 树中挂在所属三维体下 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::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}; 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(); } // ── 异常 / 异常体(后端真实端点存在,但异常挂三维体、三维体仍 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; 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