geopro/src/data/api/Api3dRepository.cpp

444 lines
18 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.

#include "api/Api3dRepository.hpp"
#include <QDateTime>
#include <QDebug>
#include <QJsonDocument>
#include <QObject>
#include <QString>
#include <QVariant>
#include <cmath>
#include <cstddef>
#include <exception>
#include <memory>
#include <utility>
#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<core::GeoLocalFrame> 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<void(SectionData)> 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<core::ContourPayload>(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<void(MapLine)> 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<core::MapPayload>(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;
}
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<DsRow> Api3dRepository::volumeRows() const {
std::vector<DsRow> 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<DsRow> Api3dRepository::anomalyRows() const {
std::vector<DsRow> 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<std::size_t>(ny)) return;
// 与 CurtainActor::buildCurtain 同口径:有 lat/lon 用 frame.toLocal否则退化用 g.x/0。
const bool hasLatLon = g.lat.size() >= static_cast<std::size_t>(nx) &&
g.lon.size() >= static_cast<std::size_t>(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<std::size_t>(i)) ? g.x[i] : static_cast<double>(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<void(VolumeGrid, core::ColorScale)> 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<double> 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<void(VolumeGrid, core::ColorScale)> 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>();
agg->pending = static_cast<int>(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<void(TerrainPaths)> /*onOk*/, OnError onErr) {
onErr(kNotReady); // 后端地形 DEM/影像端点未就绪
}
// ── 切片 CRUD后端无切片端点 → 内存 mock端点就绪后换实现────────────────
std::vector<DsRow> Api3dRepository::sliceRows() const {
std::vector<DsRow> 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<void(std::string)> 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()};
onOk(id);
}
void Api3dRepository::saveSlice(const std::string& dsId, const SliceSpec& spec,
std::function<void()> 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<void()> onOk,
OnError /*onErr*/) {
slices_.erase(dsId);
onOk();
}
// ── 异常 / 异常体(后端真实端点存在,但异常挂三维体、三维体仍 mock → 异常暂内存 mock
// 挂载结构按"异常→三维体",整链端点就绪后切真实,见记忆 vtk-3d-persistence-structure──
void Api3dRepository::loadAnomalyTree(const std::string& remarkSourceId,
std::function<void(AnomalyTree)> onOk, OnError /*onErr*/) {
// 按归属实体(体/切片)过滤;按 consortiumId 分组(异常体),空 consortiumId → loose(未分组)。
AnomalyTree tree;
std::map<std::string, std::size_t> 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<void(std::string)> 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<void()> onOk,
OnError /*onErr*/) {
anomalies_.erase(anomalyId);
onOk();
}
void Api3dRepository::deleteAnomalyGroup(const std::string& bodyId, std::function<void()> onOk,
OnError /*onErr*/) {
// 删除该异常体分组下所有异常mockconsortiumId == 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<void(std::vector<TaskRecord>)> onOk,
OnError /*onErr*/) {
onOk({}); // 后端未就绪 → 空记录
}
void Api3dRepository::loadUsableTasks(const std::string& /*ddCode*/,
std::function<void(std::vector<UsableTask>)> onOk,
OnError /*onErr*/) {
onOk({}); // 后端未就绪 → 空列表
}
} // namespace geopro::data