feat(radar): registerRadarDataset 登记 dd_radar_3d DS + loadVolume 懒加载后台建体

This commit is contained in:
gaozheng 2026-06-29 13:05:02 +08:00
parent a865264cd1
commit 25bdd5cc71
3 changed files with 166 additions and 2 deletions

View File

@ -148,6 +148,25 @@ std::string Api3dRepository::createGprVolume(const std::string& lineDir,
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;
}
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;
@ -167,9 +186,10 @@ std::vector<DsRow> Api3dRepository::volumeRows() const {
DsRow r;
r.id = id;
r.dsName = sv.name;
r.ddCode = "dd_voxel";
r.ddCode = sv.ddCode; // 雷达体="dd_radar_3d",其余 dd_voxel
r.typeName = "三维体";
r.structParentId = sv.request ? sv.request->structParentId : std::string(); // 结构归属(生成位置)
// 结构归属(生成位置)mock 请求体路径取 request->structParentId雷达体路径取 sv.structParentId。
r.structParentId = sv.request ? sv.request->structParentId : sv.structParentId;
r.createTime = sv.createTime;
rows.push_back(std::move(r));
}
@ -348,6 +368,53 @@ void Api3dRepository::loadVolume(const std::string& dsId,
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;
auto deliver = [this, dsId, onOk, onErr](std::shared_ptr<VolumeGrid> g, std::string err) {
if (!g) {
onErr("Api3dRepository::loadVolume(radar): " + err);
return;
}
core::ColorScale scale; // 简易灰度色阶(负→暗、零→灰、正→亮),使体素渲染可见。
const double mid = 0.5 * (g->vmin + g->vmax);
scale.addStop(g->vmin, core::Rgba{20, 24, 40, 255});
scale.addStop(mid, core::Rgba{140, 140, 150, 255});
scale.addStop(g->vmax, core::Rgba{235, 232, 220, 255});
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]() {
std::shared_ptr<VolumeGrid> g;
std::string err;
try {
g = std::make_shared<VolumeGrid>(
geopro::data::createRadarVolumeGrid(lineDir, linePrefix, coarse));
} 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: 三维体无源数据集");

View File

@ -48,6 +48,12 @@ public:
// 返回新 dsId失败抛 std::runtime_error加载/立方体空,由 io::gpr 链透传)。
std::string createGprVolume(const std::string& lineDir, const std::string& linePrefix,
const std::string& name, int coarse = 8);
// 登记一条规范化测线为 dd_radar_3d 体 DS只存元数据(lineDir/prefix/coarse),【不立即建体】。
// 首次 loadVolume 时在后台线程惰性建体并缓存(仿 finalizeVolume避免阻塞 UI
// id="radar-N"structParentId=结构归属(生成位置)。返回新 dsId。
std::string registerRadarDataset(const std::string& lineDir, const std::string& linePrefix,
const std::string& name, const std::string& structParentId,
int coarse = 4);
// 取回某三维体组装的请求体(测试/联调);非本 repo 创建或无 request 时返回 nullptr。
const VoxelGenerateRequest* lastVoxelRequest(const std::string& dsId) const;
// 清空内存态三维体/切片/异常(切换项目时调;否则上个项目的体/切片/异常残留在新项目列表)。
@ -144,6 +150,11 @@ private:
std::optional<std::size_t> pointCount; // 聚合散点数finalizeVolume 时持久化,详情统计用)
std::optional<VoxelGenerateRequest> request; // 组装的真实请求体createVolume(req) 路径填充)
std::string createTime; // 创建时刻mock列表副标题/详情用)
// 雷达体 DSregisterRadarDataset 路径只存元数据loadVolume 懒建。
std::string lineDir, linePrefix; // 规范化测线 .head/.data 所在目录 + 前缀
std::string ddCode = "dd_voxel"; // 数据字典码(雷达体="dd_radar_3d",其余默认 dd_voxel
std::string structParentId; // 结构归属(生成位置);雷达体无 request 时由此提供
int coarse = 4; // 沿测线下采样因子(控内存)
};
std::map<std::string, StoredVolume> volumes_; // dsId → 体
int volumeCounter_ = 0;

View File

@ -1,7 +1,14 @@
#include <gtest/gtest.h>
#include <QCoreApplication>
#include <QEventLoop>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <memory>
#include <string>
#include <system_error>
#include "api/Api3dRepository.hpp"
#include "geo/GeoLocalFrame.hpp"
@ -190,3 +197,82 @@ TEST(Api3dRepo, AnomalyRowsCarryMountAsParent) {
EXPECT_EQ(rs->parentId, "slice-9"); // 挂切片
EXPECT_EQ(rs->typeName, "异常"); // typeName 空 → 回退"异常"
}
namespace {
// 写一条规范化测线合成数据(.head + .data同 Task 6 createRadarVolumeGrid 用例口径:
// SAMPLES=3 / NUMBER_OF_CH=2 / LAST_TRACE=8 → X=道(4 段)、Y=通道(2)、Z=采样(3)。
void writeSyntheticRadarLine(const std::filesystem::path& dir) {
namespace fs = std::filesystem;
fs::create_directories(dir);
{
std::ofstream f(dir / "L.head");
f << "SAMPLES:3\nNUMBER_OF_CH:2\nLAST_TRACE:8\nBITS:16\nENDIAN_TYPE:1\n"
"DISTANCE_INTERVAL:0.1\nTIMEWINDOW:30\nDIELECTRIC:9\n";
}
{
std::ofstream f(dir / "L.data", std::ios::binary);
for (int t = 0; t < 4; ++t)
for (int c = 0; c < 2; ++c)
for (int s = 0; s < 3; ++s) {
std::int16_t v = static_cast<std::int16_t>(t * 10 + c * 100 + s);
f.write(reinterpret_cast<const char*>(&v), 2);
}
}
}
} // namespace
// registerRadarDataset登记为 dd_radar_3d 体 DS只存元数据、不建体→ 仍被认作三维体,
// volumeRows 输出 ddCode="dd_radar_3d" + structParentId。
TEST(Api3dRepo, RegisterRadarDatasetRoutesAsDdRadar3d) {
const auto dir = std::filesystem::temp_directory_path() / "api3d_radar_register";
std::error_code ec;
std::filesystem::remove_all(dir, ec);
writeSyntheticRadarLine(dir);
StubAsyncRepo dsRepo;
auto frame = std::make_shared<geopro::core::GeoLocalFrame>(22.0, 114.0);
Api3dRepository repo(dsRepo, frame);
const std::string id = repo.registerRadarDataset(dir.string(), "L", "测线L",
/*structParentId=*/"tm-1", /*coarse=*/1);
EXPECT_FALSE(id.empty());
EXPECT_TRUE(repo.isVolumeDataset(id)); // 运行期按 volumes_ 成员判体 → 真(即便未建体)
const auto rows = repo.volumeRows();
ASSERT_FALSE(rows.empty());
EXPECT_EQ(rows.back().ddCode, "dd_radar_3d"); // 不是 dd_voxel
EXPECT_EQ(rows.back().structParentId, "tm-1");
std::filesystem::remove_all(dir, ec);
}
// loadVolume首次勾选时懒建雷达体无 QCoreApplication → 同步交付;全量测试中若其它用例已建
// QCoreApplication 单例则走异步processEvents 排空队列交付)。回调收到有效 VolumeGrid。
TEST(Api3dRepo, LoadVolumeBuildsRadarLazily) {
const auto dir = std::filesystem::temp_directory_path() / "api3d_radar_lazy";
std::error_code ec;
std::filesystem::remove_all(dir, ec);
writeSyntheticRadarLine(dir);
StubAsyncRepo dsRepo;
auto frame = std::make_shared<geopro::core::GeoLocalFrame>(22.0, 114.0);
Api3dRepository repo(dsRepo, frame);
const std::string id = repo.registerRadarDataset(dir.string(), "L", "测线L", "", 1);
bool got = false;
repo.loadVolume(
id,
[&](VolumeGrid g, geopro::core::ColorScale) {
got = true;
EXPECT_GT(g.vol.nx(), 0);
EXPECT_GT(g.vol.ny(), 0);
EXPECT_GT(g.vol.nz(), 0);
},
[&](const std::string& e) { FAIL() << e; });
// 全量测试单进程中其它用例(test_async_repo_dispatch/test_auth)会建持久 QCoreApplication 单例 →
// radar 分支走异步(std::thread + queued 交付),需驱动事件循环排空;无 app 时同步交付 got 已真。
for (int i = 0; i < 200 && !got && QCoreApplication::instance(); ++i)
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
EXPECT_TRUE(got);
std::filesystem::remove_all(dir, ec);
}