geopro/src/data/api/Api3dRepository.cpp

655 lines
30 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 <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QJsonDocument>
#include <QObject>
#include <QString>
#include <QVariant>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <exception>
#include <memory>
#include <thread>
#include <tuple>
#include <utility>
#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 = "后端三维端点未就绪";
// 雷达中性灰度色阶。⚠ core::ColorScale 是【阶梯/分段常数】(colorAt 取下界 stop不插值)
// 只放 3 个 stop(黑/灰/白) → 整个 [mid,vmax) 正振幅全塌成一级灰、[vmin,mid) 全黑,连续 GPR
// 数据被压成 3 级(深部 DC 渲成恒灰、丢层理)——实测确诊的真 bug。故铺 256 级平滑斜坡
// black→whitecolorAt 才能给出连续灰阶。(反演等值面仍用稀疏 stop 的阶梯,故意离散,不动。)
core::ColorScale radarGrayScale(double vmin, double vmax) {
core::ColorScale cs;
constexpr int kLevels = 256;
for (int i = 0; i < kLevels; ++i) {
const double f = static_cast<double>(i) / (kLevels - 1); // 0..1
const double val = vmin + (vmax - vmin) * f;
const auto g = static_cast<unsigned char>(std::lround(f * 255.0));
cs.addStop(val, core::Rgba{g, g, g, 255});
}
return cs;
}
} // 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" || c == "dd_radar_3d") {
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<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;
}
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);
// 中性灰度(GPR 标准 B-scan)256 级连续——3-stop 会被 colorAt 阶梯压成 3 级(见 radarGrayScale)。
const core::ColorScale scale = radarGrayScale(grid.vmin, grid.vmax);
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;
}
std::string Api3dRepository::registerRadarDataset(const std::string& lineDir,
const std::string& linePrefix,
const std::string& name,
const std::string& structParentId, int coarse) {
// 只存元数据、不建体(懒建在首次 loadVolume 后台线程做并缓存)→ DS 优先、勾选才付出建体成本。
const std::string id = "radar-" + std::to_string(++volumeCounter_);
StoredVolume sv;
sv.name = name;
sv.ddCode = "dd_radar_3d";
sv.lineDir = lineDir;
sv.linePrefix = linePrefix;
sv.coarse = coarse;
sv.structParentId = structParentId;
sv.createTime =
QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString();
volumes_[id] = std::move(sv); // 不预填 cachedGrid → 懒建
return id;
}
bool Api3dRepository::setRadarGainMode(const std::string& dsId, RadarGainMode mode) {
auto it = volumes_.find(dsId);
if (it == volumes_.end() || it->second.ddCode != "dd_radar_3d") return false;
if (it->second.gainMode == mode) return true; // 未变化也算成功(调用方可跳过重渲)
it->second.gainMode = mode;
it->second.cachedGrid.reset(); // 失效缓存体 → 下次 loadVolume 用新增益模式重建
return true;
}
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<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 = sv.ddCode; // 雷达体="dd_radar_3d",其余 dd_voxel
r.typeName = "三维体";
// 结构归属(生成位置)mock 请求体路径取 request->structParentId雷达体路径取 sv.structParentId。
r.structParentId = sv.request ? sv.request->structParentId : sv.structParentId;
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::loadSectionWithRetry(const std::string& dsId, int attemptsLeft,
std::function<void(SectionData)> 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<void(VolumeGrid, core::ColorScale)> onOk,
OnError onErr) {
if (pts.v.empty()) {
onErr("Api3dRepository::loadVolume: 配准后无有效散点(源数据为空或全无数据)");
return;
}
// 重 IDW 建体放后台线程,避免阻塞 UI用户要求渲染必须异步。纯计算无 Qt/VTK算完
// 经事件循环回主线程做缓存 + 交付(缓存写 volumes_ / onOk 触碰 VTK 必须在主线程)。
// 兜底:无 QCoreApplicationheadless/单测)时退化为同步,保证可测/可离屏。
auto deliver = [this, dsId, scale, onOk, onErr](std::shared_ptr<geopro::core::BuiltVolume> 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<double> 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<geopro::core::BuiltVolume> bv;
std::string err;
try {
bv = std::make_shared<geopro::core::BuiltVolume>(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::milliseconds>(
std::chrono::steady_clock::now() - t0).count();
qInfo().noquote() << "[volbuild] computed dsId=" << QString::fromStdString(dsId)
<< "ms=" << ms << "ok=" << (bv != nullptr);
// 回主线程交付QueuedConnectionqApp 为主线程对象,存活于整个会话)。
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<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;
}
if (!sv.linePrefix.empty()) { // 雷达体 DS后台建体避免阻塞 UI与 finalizeVolume 同范式)
const std::string lineDir = sv.lineDir, linePrefix = sv.linePrefix;
const int coarse = sv.coarse;
const RadarGainMode gainMode = sv.gainMode; // 显示增益模式(右键可切,切时清缓存重建)
auto deliver = [this, dsId, onOk, onErr](std::shared_ptr<VolumeGrid> g, std::string err) {
if (!g) {
onErr("Api3dRepository::loadVolume(radar): " + err);
return;
}
// 中性灰度256 级连续(见 radarGrayScale3-stop 会被 colorAt 阶梯压成 3 级)。
const core::ColorScale scale = radarGrayScale(g->vmin, g->vmax);
if (auto it2 = volumes_.find(dsId); it2 != volumes_.end()) {
it2->second.cachedGrid = *g; // 缓存 → 下次命中直渲
it2->second.cachedScale = scale;
}
onOk(*g, scale);
};
auto compute = [lineDir, linePrefix, coarse, gainMode]() {
std::shared_ptr<VolumeGrid> g;
std::string err;
try {
g = std::make_shared<VolumeGrid>(geopro::data::createRadarVolumeGrid(
lineDir, linePrefix, coarse, /*targetDy=*/0.025, gainMode));
} catch (const std::exception& e) {
err = e.what();
}
return std::make_tuple(g, err);
};
if (!QCoreApplication::instance()) { // headless/单测 → 同步交付
auto r = compute();
deliver(std::get<0>(r), std::get<1>(r));
return;
}
std::thread([compute, deliver]() mutable {
auto r = compute();
auto g = std::get<0>(r); // 具名变量(非结构化绑定)→ C++17 可被 lambda 捕获
auto err = std::get<1>(r);
QMetaObject::invokeMethod(
qApp, [deliver, g, err]() mutable { deliver(std::move(g), std::move(err)); },
Qt::QueuedConnection);
}).detach();
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<core::ColorScale> scales; // 收集所有源色阶 → 取 vmax 中位者定值域(不依赖到达顺序)
};
auto agg = std::make_shared<Agg>();
agg->pending = static_cast<int>(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<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()};
// 打印切片登记请求体(对齐 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<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();
}
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<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;
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<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