655 lines
30 KiB
C++
655 lines
30 KiB
C++
#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→white,colorAt 才能给出连续灰阶。(反演等值面仍用稀疏 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 必须在主线程)。
|
||
// 兜底:无 QCoreApplication(headless/单测)时退化为同步,保证可测/可离屏。
|
||
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);
|
||
// 回主线程交付(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<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 级连续(见 radarGrayScale:3-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*/) {
|
||
// 删除该异常体分组下所有异常(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
|