444 lines
18 KiB
C++
444 lines
18 KiB
C++
#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*/) {
|
||
// 删除该异常体分组下所有异常(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<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
|