feat/3d-radar-volume-ingest #9
|
|
@ -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: 三维体无源数据集");
|
||||
|
|
|
|||
|
|
@ -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,列表副标题/详情用)
|
||||
// 雷达体 DS(registerRadarDataset 路径):只存元数据,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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue