feat(app): 添加本地雷达数据导入功能并完善B-Scan剖面显示

- 新增ImportLocalRadarDialog对话框,支持选择文件夹扫描雷达测线数据
- 在ObjectTreePanel中添加"导入本地雷达数据"菜单选项
- 实现LocalRadarDatasetStore和LocalRadarScanner数据存储和扫描组件
- 扩展DatasetDetailController支持fileUrl参数传递,用于本地文件加载
- 更新ApiDatasetRepository支持雷达数据文件(.data/.iprb/.head/.iprh)解析
- 完善BScanProfileView视图,实现灰度B-Scan图像渲染和通道切换功能
- 添加多通道交错数据读取支持,兼容单文件多通道和多文件单通道模式
- 在数据集列表中存储fileUrl信息,用于本地雷达数据集识别和加载

BREAKING CHANGE: DatasetDetailController.openDataset方法增加fileUrl参数
```
This commit is contained in:
徐星 2026-07-01 09:00:50 +08:00
parent 3b894da2ec
commit ad6699d48d
23 changed files with 872 additions and 49 deletions

View File

@ -90,6 +90,7 @@ add_executable(geopro_desktop WIN32
ProjectListDialog.cpp
ObjectFormDialog.cpp
ImportDatasetDialog.cpp
ImportLocalRadarDialog.cpp
ExportDatasetDialog.cpp
AnomalySaveDialog.cpp
AnomalyPropertiesDialog.cpp

View File

@ -0,0 +1,160 @@
#include "ImportLocalRadarDialog.hpp"
#include <QDateTime>
#include <QDir>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QStandardPaths>
#include <QTreeWidget>
#include <QVBoxLayout>
#include "data/LocalRadarDatasetStore.hpp"
#include "data/LocalRadarScanner.hpp"
namespace geopro::app {
ImportLocalRadarDialog::ImportLocalRadarDialog(const QString& tmObjectId,
QWidget* parent)
: QDialog(parent), tmObjectId_(tmObjectId) {
setWindowTitle(QStringLiteral("导入本地雷达数据"));
setMinimumSize(560, 380);
auto* lay = new QVBoxLayout(this);
lay->setSpacing(12);
// 路径选择行
auto* pathLay = new QHBoxLayout();
pathLay->addWidget(new QLabel(QStringLiteral("雷达数据文件夹:"), this));
pathEdit_ = new QLineEdit(this);
pathEdit_->setReadOnly(true);
pathEdit_->setPlaceholderText(QStringLiteral("请选择包含 .data/.head 的文件夹"));
pathLay->addWidget(pathEdit_, 1);
auto* browseBtn = new QPushButton(QStringLiteral("浏览…"), this);
pathLay->addWidget(browseBtn);
lay->addLayout(pathLay);
// 扫描按钮
scanBtn_ = new QPushButton(QStringLiteral("扫描测线"), this);
scanBtn_->setEnabled(false);
lay->addWidget(scanBtn_);
// 扫描结果树
resultTree_ = new QTreeWidget(this);
resultTree_->setHeaderLabels(
QStringList() << QStringLiteral("测线名称") << QStringLiteral("类型")
<< QStringLiteral("通道数") << QStringLiteral("采样点数")
<< QStringLiteral("道数") << QStringLiteral("频率(MHz)"));
resultTree_->header()->setStretchLastSection(false);
resultTree_->setColumnWidth(0, 180);
resultTree_->setColumnWidth(1, 70);
resultTree_->setColumnWidth(2, 60);
resultTree_->setColumnWidth(3, 80);
resultTree_->setColumnWidth(4, 60);
resultTree_->setColumnWidth(5, 80);
lay->addWidget(resultTree_, 1);
// 状态标签
statusLabel_ = new QLabel(this);
statusLabel_->setWordWrap(true);
lay->addWidget(statusLabel_);
// 底部按钮
auto* btnLay = new QHBoxLayout();
btnLay->addStretch();
okBtn_ = new QPushButton(QStringLiteral("导入"), this);
okBtn_->setEnabled(false);
okBtn_->setDefault(true);
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
btnLay->addWidget(okBtn_);
btnLay->addWidget(cancelBtn);
lay->addLayout(btnLay);
QObject::connect(browseBtn, &QPushButton::clicked, this,
&ImportLocalRadarDialog::chooseFolder);
QObject::connect(scanBtn_, &QPushButton::clicked, this,
&ImportLocalRadarDialog::performScan);
QObject::connect(okBtn_, &QPushButton::clicked, this,
&ImportLocalRadarDialog::onConfirm);
QObject::connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
}
void ImportLocalRadarDialog::chooseFolder() {
const QString dir = QFileDialog::getExistingDirectory(
this, QStringLiteral("选择雷达数据文件夹"), QString(),
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (!dir.isEmpty()) {
sourceDir_ = dir;
pathEdit_->setText(dir);
scanBtn_->setEnabled(true);
okBtn_->setEnabled(false);
resultTree_->clear();
statusLabel_->clear();
}
}
void ImportLocalRadarDialog::performScan() {
if (sourceDir_.isEmpty()) return;
resultTree_->clear();
scanResults_ = geopro::data::LocalRadarScanner::scan(sourceDir_);
for (const auto& s : scanResults_) {
auto* item = new QTreeWidgetItem(resultTree_);
item->setText(0, s.surveyName);
item->setText(1, s.channelCount == 1 ? QStringLiteral("2D") : QStringLiteral("3D"));
item->setText(2, QString::number(s.channelCount));
item->setText(3, QString::number(s.meta.samples));
item->setText(4, QString::number(s.meta.lastTrace));
item->setText(5, QString::number(s.meta.frequency));
}
statusLabel_->setText(
QStringLiteral("发现 %1 条测线").arg(scanResults_.size()));
okBtn_->setEnabled(!scanResults_.empty());
}
void ImportLocalRadarDialog::copyDirectory(const QString& src, const QString& dst) {
QDir srcDir(src);
if (!srcDir.exists()) return;
QDir dstDir(dst);
if (!dstDir.exists()) dstDir.mkpath(QStringLiteral("."));
for (const QString& entry : srcDir.entryList(QDir::Files)) {
QFile::copy(srcDir.filePath(entry), dstDir.filePath(entry));
}
for (const QString& subdir : srcDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
copyDirectory(srcDir.filePath(subdir), dstDir.filePath(subdir));
}
}
void ImportLocalRadarDialog::onConfirm() {
if (scanResults_.empty()) return;
// 目标目录:<AppData>/geopro/local_radar/<timestamp>/
const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
+ QStringLiteral("/local_radar/");
const QString ts = QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_hhmmss"));
const QString targetDir = baseDir + ts + QStringLiteral("/");
// 复制源文件夹到目标目录
copyDirectory(sourceDir_, targetDir);
// 更新扫描结果的 folderPath 为复制后的路径
for (auto& s : scanResults_) {
s.folderPath = targetDir;
}
// 生成 DsRow
auto rows = geopro::data::LocalRadarScanner::toDsRows(scanResults_);
for (auto& row : rows) {
row.structParentId = tmObjectId_.toStdString();
}
emit imported(tmObjectId_, rows);
accept();
}
} // namespace geopro::app

View File

@ -0,0 +1,42 @@
#pragma once
#include <QDialog>
#include <QString>
#include <vector>
#include "data/LocalRadarScanner.hpp"
#include "repo/RepoTypes.hpp"
class QLineEdit;
class QPushButton;
class QTreeWidget;
class QLabel;
namespace geopro::app {
// 本地雷达数据导入对话框
class ImportLocalRadarDialog : public QDialog {
Q_OBJECT
public:
explicit ImportLocalRadarDialog(const QString& tmObjectId,
QWidget* parent = nullptr);
signals:
void imported(const QString& tmObjectId, const std::vector<geopro::data::DsRow>& rows);
private:
void chooseFolder();
void performScan();
void onConfirm();
void copyDirectory(const QString& src, const QString& dst);
QString tmObjectId_;
QString sourceDir_;
std::vector<geopro::data::RadarSurveyInfo> scanResults_;
QLineEdit* pathEdit_ = nullptr;
QPushButton* scanBtn_ = nullptr;
QPushButton* okBtn_ = nullptr;
QTreeWidget* resultTree_ = nullptr;
QLabel* statusLabel_ = nullptr;
};
} // namespace geopro::app

View File

@ -92,6 +92,9 @@
#include "model/ColorScale.hpp"
#include "model/Field.hpp"
#include "repo/LocalSampleRepository.hpp"
#include "data/LocalRadarDatasetStore.hpp"
#include "data/LocalRadarScanner.hpp"
#include "ImportLocalRadarDialog.hpp"
#include "ApiClient.hpp"
#include "AuthService.hpp"
@ -312,6 +315,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
geopro::controller::DatasetDetailController& detailCtrl,
const QString& sessionToken)
{
// 本地雷达数据集存储(测试项目用)。
geopro::data::LocalRadarDatasetStore localRadarStore;
// ── 世界系:启动取一次 grid1 的 lat/lon用中位数作 GeoLocalFrame 原点 ──
// 全项目共享shared_ptr 持有):所有帘面用同一 frame 投影,保证多条测线空间配准。
const auto baseGrid = repo.loadGrid("grid1");
@ -1662,7 +1668,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// tmObjectId白化 structParentId从行读出透传使白化模板列表非空。
const QString tmObjectId =
item->data(0, geopro::app::kDsTmObjectIdRole).toString();
if (!dsId.isEmpty()) detailCtrl.openDataset(dsId, ddCode, dsName, tmObjectId);
const QString fileUrl =
item->data(0, geopro::app::kDsFileUrlRole).toString();
if (!dsId.isEmpty()) detailCtrl.openDataset(dsId, ddCode, dsName, tmObjectId, fileUrl);
});
// ── 控制器信号 → 详情面板tab 引擎):开页 / 页签就绪 / 加载中 / 聚焦 ──
@ -2010,6 +2018,18 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
nav.switchProject(nav.currentProjectId());
});
dlg->open();
} else if (action == QStringLiteral("importLocalRadar")) {
// 导入本地雷达数据:选择文件夹 → 扫描测线 → 复制到项目目录 → 混入数据集列表。
auto* dlg = new geopro::app::ImportLocalRadarDialog(id, &window);
dlg->setAttribute(Qt::WA_DeleteOnClose);
QObject::connect(dlg, &geopro::app::ImportLocalRadarDialog::imported, &window,
[&nav, toast, &localRadarStore](const QString& tmId,
const std::vector<geopro::data::DsRow>& rows) {
localRadarStore.addDatasets(tmId, rows);
toast(QStringLiteral("雷达数据导入成功"));
nav.switchProject(nav.currentProjectId());
});
dlg->open();
} else if (action == QStringLiteral("showHide") || action == QStringLiteral("locate")) {
toast(QStringLiteral("「%1」需要二维/三维视图,开发中").arg(name));
} else {
@ -2120,10 +2140,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
const QString dsName = item->data(0, geopro::app::kDsNameRole).toString();
// tmObjectId白化 structParentId从行读出透传使白化模板列表非空。
const QString tmObjectId = item->data(0, geopro::app::kDsTmObjectIdRole).toString();
const QString fileUrl = item->data(0, geopro::app::kDsFileUrlRole).toString();
QMenu menu(datasetList);
menu.addAction(QStringLiteral("数据集详情"), datasetList,
[&detailCtrl, dsId, ddCode, dsName, tmObjectId]() {
detailCtrl.openDataset(dsId, ddCode, dsName, tmObjectId);
[&detailCtrl, dsId, ddCode, dsName, tmObjectId, fileUrl]() {
detailCtrl.openDataset(dsId, ddCode, dsName, tmObjectId, fileUrl);
});
menu.addAction(QStringLiteral("属性"), datasetList, [&nav, dsId]() {
nav.selectDataset(dsId); // 只读元字段
@ -2288,16 +2309,22 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
datasetTabs->setTabText(1, QStringLiteral("文件"));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::datasetsLoaded, datasetList,
[removeTreeLoadMore, addTreeLoadMore, datasetList, datasetTitle, datasetTabs](
[removeTreeLoadMore, addTreeLoadMore, datasetList, datasetTitle, datasetTabs,
&localRadarStore](
const QString& tmObjectId, const std::vector<geopro::data::DsRow>& rows,
int total, bool append) {
removeTreeLoadMore(datasetList);
// tmObjectId本批所属 TM 对象 id存入每项 → 白化对话框透传用structParentId
geopro::app::populateDatasetList(datasetList, rows, append, tmObjectId);
const int loaded = addTreeLoadMore(datasetList, total);
// 混入本地雷达数据集
auto localRows = localRadarStore.datasetsFor(tmObjectId);
if (!localRows.empty()) {
geopro::app::populateDatasetList(datasetList, localRows, true, tmObjectId);
}
const int loaded = addTreeLoadMore(datasetList, total + static_cast<int>(localRows.size()));
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集"));
datasetTabs->setTabText(
0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total)
0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total + static_cast<int>(localRows.size()))
: QStringLiteral("数据"));
});
QObject::connect(&nav, &geopro::controller::WorkbenchNavController::filesLoaded, fileList,

View File

@ -201,6 +201,7 @@ QTreeWidgetItem* makeDatasetItem(const geopro::data::DsRow& d, const QString& tm
item->setData(0, kDsTypeNameRole, QString::fromStdString(d.typeName));
item->setData(0, kDsCreateTimeRole, QString::fromStdString(d.createTime));
item->setData(0, kDsTmObjectIdRole, tmObjectId); // 所属 TM 对象 id白化 structParentId
item->setData(0, kDsFileUrlRole, QString::fromStdString(d.fileUrl)); // 文件 url雷达等本地加载用
// 单击 tip显示数据集主要属性名称 / 类型 / 创建时间对齐菜单文档「tip显示ds的主要属性」。
QString tip = QStringLiteral("名称:%1").arg(QString::fromStdString(d.dsName));
if (!d.typeName.empty()) tip += QStringLiteral("\n类型:%1").arg(QString::fromStdString(d.typeName));

View File

@ -272,6 +272,7 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
if (isTm) {
// TM 节点:不提供任何「新建」(测线下不能新增对象)——仅「导入数据集」。
add(QStringLiteral("导入数据集…"), QStringLiteral("importDs"));
add(QStringLiteral("导入本地雷达数据…"), QStringLiteral("importLocalRadar"));
}
menu.addSeparator();
add(QStringLiteral("删除"), QStringLiteral("delete"));

View File

@ -1,7 +1,10 @@
#include "panels/chart/BScanProfileView.hpp"
#include <QComboBox>
#include <QHBoxLayout>
#include <QImage>
#include <QPixmap>
#include <QVBoxLayout>
#include <QLabel>
#include <QVariant>
#include "model/detail/DetailPayloads.hpp"
@ -15,16 +18,119 @@ BScanProfileView::~BScanProfileView() = default;
void BScanProfileView::setupUi() {
auto* lay = new QVBoxLayout(this);
lay->addWidget(new QLabel(QStringLiteral("B-Scan Profile (placeholder)"), this));
lay->addStretch();
lay->setContentsMargins(8, 8, 8, 8);
// 通道切换工具条
auto* toolLay = new QHBoxLayout();
toolLay->addStretch();
channelCombo_ = new QComboBox(this);
channelCombo_->setVisible(false);
toolLay->addWidget(channelCombo_);
lay->addLayout(toolLay);
infoLabel_ = new QLabel(this);
infoLabel_->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
lay->addWidget(infoLabel_);
imageLabel_ = new QLabel(this);
imageLabel_->setAlignment(Qt::AlignCenter);
imageLabel_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
imageLabel_->setMinimumSize(200, 150);
lay->addWidget(imageLabel_, 1);
QObject::connect(channelCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this](int idx) {
if (idx < 0 || idx >= static_cast<int>(payload_.channels.size())) return;
payload_.currentChannel = idx;
updateImage();
});
}
void BScanProfileView::setPayload(const QVariant& payload) {
if (!payload.canConvert<geopro::core::GprProfilePayload>()) return;
const auto p = payload.value<geopro::core::GprProfilePayload>();
payload_ = payload.value<geopro::core::GprProfilePayload>();
hasData_ = true;
// Phase 2 壳子仅记录数据Phase 3/4 补渲染。
Q_UNUSED(p)
// 填充通道下拉框
channelCombo_->clear();
if (payload_.channels.size() > 1) {
for (const auto& ch : payload_.channels) {
channelCombo_->addItem(ch.channelName);
}
channelCombo_->setCurrentIndex(
(payload_.currentChannel >= 0 &&
payload_.currentChannel < static_cast<int>(payload_.channels.size()))
? payload_.currentChannel
: 0);
channelCombo_->setVisible(true);
} else {
channelCombo_->setVisible(false);
}
updateImage();
}
void BScanProfileView::updateImage() {
const auto& chs = payload_.channels;
if (chs.empty() || payload_.meta.ntraces <= 0 || payload_.meta.samples <= 0) {
infoLabel_->setText(QStringLiteral("暂无数据"));
imageLabel_->clear();
return;
}
const int ch = payload_.currentChannel;
const int useCh = (ch >= 0 && ch < static_cast<int>(chs.size())) ? ch : 0;
const auto& cd = chs[static_cast<size_t>(useCh)];
const int ntraces = payload_.meta.ntraces;
const int samples = payload_.meta.samples;
const std::vector<float>& data = cd.data;
if (static_cast<int>(data.size()) < ntraces * samples) {
infoLabel_->setText(QStringLiteral("数据大小不匹配"));
imageLabel_->clear();
return;
}
// 元信息
QString info = QStringLiteral("通道:%1 | 道数:%2 | 采样点数:%3 | 天线:%4 %5MHz")
.arg(cd.channelName)
.arg(ntraces)
.arg(samples)
.arg(payload_.antennaModel)
.arg(payload_.antennaFreqMHz);
if (!payload_.date.isEmpty()) info += QStringLiteral(" | 日期:%1").arg(payload_.date);
infoLabel_->setText(info);
// 计算值域
float vmin = data[0], vmax = data[0];
for (float v : data) {
if (v < vmin) vmin = v;
if (v > vmax) vmax = v;
}
const float range = vmax - vmin;
// 生成 B-scan 图像:宽度=ntraces高度=samples(t,s)=data[t*samples+s]
QImage img(ntraces, samples, QImage::Format_ARGB32);
for (int t = 0; t < ntraces; ++t) {
const int base = t * samples;
for (int s = 0; s < samples; ++s) {
float v = data[base + s];
int g = 128;
if (range > 0.0f) {
g = static_cast<int>(((v - vmin) / range) * 255.0f + 0.5f);
if (g < 0) g = 0;
if (g > 255) g = 255;
}
img.setPixelColor(t, s, QColor(g, g, g));
}
}
QPixmap pix = QPixmap::fromImage(img);
// 若图像较小,适度放大以提升可视性;最大不超过标签尺寸(保持纵横比)。
const QSize labelSize = imageLabel_->size();
if (pix.width() < labelSize.width() && pix.height() < labelSize.height()) {
pix = pix.scaled(labelSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
imageLabel_->setPixmap(pix);
}
} // namespace geopro::app

View File

@ -1,12 +1,15 @@
#pragma once
#include <QLabel>
#include <QWidget>
#include "model/detail/DetailPayloads.hpp"
#include "panels/chart/IDetailView.hpp"
class QComboBox;
namespace geopro::app {
// 雷达 B-Scan 剖面详情视图IDetailView 实现)。
// Phase 2 先放壳子:实现接口、解包 GprProfilePayload、显示通道数和基本信息。
// Phase 3/4 补自绘 Canvas + A-Scan + 交互。
// Phase 3本地文件路径打通后按真实 GprProfilePayload 渲染灰度 B-scan 图像。
class BScanProfileView : public QWidget, public IDetailView {
Q_OBJECT
public:
@ -18,8 +21,13 @@ public:
private:
void setupUi();
void updateImage();
bool hasData_ = false;
geopro::core::GprProfilePayload payload_;
QLabel* infoLabel_ = nullptr;
QLabel* imageLabel_ = nullptr;
QComboBox* channelCombo_ = nullptr;
};
} // namespace geopro::app

View File

@ -19,7 +19,8 @@ DatasetDetailController::~DatasetDetailController() {
}
void DatasetDetailController::openDataset(const QString& dsId, const QString& ddCode,
const QString& dsName, const QString& tmObjectId) {
const QString& dsName, const QString& tmObjectId,
const QString& fileUrl) {
qInfo("[detail] openDataset id=%s ddCode=%s name=%s tm=%s", qUtf8Printable(dsId),
qUtf8Printable(ddCode), qUtf8Printable(dsName), qUtf8Printable(tmObjectId));
auto* s = registry_.find(ddCode.toStdString());
@ -28,6 +29,7 @@ void DatasetDetailController::openDataset(const QString& dsId, const QString& dd
emit loadFailed(dsId, QStringLiteral("暂不支持该数据类型的预览"));
return;
}
if (!fileUrl.isEmpty()) fileUrlByDsId_[dsId] = fileUrl;
const std::vector<controller::TabSpec> tabs = s->tabs();
emit datasetOpened(dsId, ddCode, dsName, tmObjectId, tabs);
for (int i = 0; i < static_cast<int>(tabs.size()); ++i)
@ -53,9 +55,11 @@ void DatasetDetailController::loadTabImpl(const QString& dsId, const QString& dd
// loadAsync 对未知 loaderKey 抛 std::runtime_error若逃逸槽函数会被 GuardedApplication
// 吞掉、遮罩永久悬挂(文档化的崩溃/挂起类)。就地兜底为 loadFailed且不留半注册的在飞句柄。
const QString fileUrl = fileUrlByDsId_.value(dsId);
data::DetailLoad* load = nullptr;
try {
load = repo_.loadAsync(spec.loaderKey.toStdString(), dsId.toStdString(), pageNo, pageSize);
load = repo_.loadAsync(spec.loaderKey.toStdString(), dsId.toStdString(), pageNo, pageSize,
fileUrl);
} catch (const std::exception& e) {
qWarning("[detail] loadAsync 失败 id=%s tab=%d key=%s: %s", qUtf8Printable(dsId), tabIndex,
qUtf8Printable(spec.loaderKey), e.what());

View File

@ -23,7 +23,7 @@ public slots:
// 打开数据集:查策略 → datasetOpened(页签集) → 对每个非 lazy 页签发起 loadTab。
// tmObjectId数据集所属 TM 对象 id=白化 structParentId透传给详情页给白化对话框用可空。
void openDataset(const QString& dsId, const QString& ddCode, const QString& dsName = QString(),
const QString& tmObjectId = QString());
const QString& tmObjectId = QString(), const QString& fileUrl = QString());
// 加载某页签lazy 页签首次激活时由壳触发;非 lazy 由 openDataset 自动触发)。
// 分页型页签(如 dd_grid 列表首载用默认页pageNo=1/pageSize=0 → 仓储解析默认每页条数)。
void loadTab(const QString& dsId, const QString& ddCode, int tabIndex);
@ -46,5 +46,6 @@ private:
data::IAsyncDatasetRepository& repo_;
ChartStrategyRegistry& registry_;
QMap<int, QPointer<data::DetailLoad>> inflight_; // 按页签槽位的在飞句柄§5.0 身份比对)
QMap<QString, QString> fileUrlByDsId_; // dsId → fileUrl雷达等本地加载用
};
} // namespace geopro::controller

View File

@ -19,7 +19,9 @@ add_library(geopro_data STATIC
api/Api3dRepository.cpp
api/DatasetLoadHandles.cpp
api/NavRequest.cpp
GprVolumeRepository.cpp)
GprVolumeRepository.cpp
LocalRadarDatasetStore.cpp
LocalRadarScanner.cpp)
target_include_directories(geopro_data PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
# geopro_gpr3dv_bridge线 GPR (BuiltI16)GprVolumeRepository VolumeGrid
target_link_libraries(geopro_data PUBLIC geopro_core geopro_net Qt6::Core geopro_gpr3dv_bridge PRIVATE nlohmann_json::nlohmann_json)

View File

@ -0,0 +1,25 @@
#include "LocalRadarDatasetStore.hpp"
namespace geopro::data {
void LocalRadarDatasetStore::addDatasets(const QString& tmObjectId,
const std::vector<DsRow>& rows) {
auto& vec = byTm_[tmObjectId];
vec.insert(vec.end(), rows.begin(), rows.end());
}
std::vector<DsRow> LocalRadarDatasetStore::datasetsFor(const QString& tmObjectId) const {
auto it = byTm_.find(tmObjectId);
if (it == byTm_.end()) return {};
return it.value();
}
void LocalRadarDatasetStore::clear() {
byTm_.clear();
}
void LocalRadarDatasetStore::clearTm(const QString& tmObjectId) {
byTm_.remove(tmObjectId);
}
} // namespace geopro::data

View File

@ -0,0 +1,22 @@
#pragma once
#include <vector>
#include <QMap>
#include <QString>
#include "repo/RepoTypes.hpp"
namespace geopro::data {
// 本地雷达数据集存储:按所属 TM 分组管理已导入的本地雷达测线。
// 生命周期随应用运行关闭后不持久化M1 测试定位)。
class LocalRadarDatasetStore {
public:
void addDatasets(const QString& tmObjectId, const std::vector<DsRow>& rows);
std::vector<DsRow> datasetsFor(const QString& tmObjectId) const;
void clear();
void clearTm(const QString& tmObjectId);
private:
QMap<QString, std::vector<DsRow>> byTm_;
};
} // namespace geopro::data

View File

@ -0,0 +1,130 @@
#include "LocalRadarScanner.hpp"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QMap>
#include <QRegularExpression>
#include <algorithm>
#include <stdexcept>
#include "io/gpr/IprHeader.hpp"
namespace geopro::data {
QString LocalRadarScanner::surveyKeyFromFileName(const QString& baseName) {
const int lastUnderscore = baseName.lastIndexOf(QLatin1Char('_'));
if (lastUnderscore > 0) {
return baseName.left(lastUnderscore);
}
return baseName;
}
std::vector<RadarSurveyInfo> LocalRadarScanner::scan(const QString& dirPath) {
std::vector<RadarSurveyInfo> out;
QDir dir(dirPath);
if (!dir.exists()) return out;
// 收集所有 .data 文件
QStringList dataFiles = dir.entryList(QStringList() << QStringLiteral("*.data"),
QDir::Files | QDir::NoDotAndDotDot,
QDir::Name);
// 先按 NUMBER_OF_CH 分类:多通道单文件 vs 单通道多文件
// key = surveyKey, value = list of data file base names
QMap<QString, QStringList> modeBSurveys; // 模式 B单通道需要分组
std::vector<RadarSurveyInfo> modeASurveys; // 模式 A单文件多通道
for (const QString& dataFile : dataFiles) {
QFileInfo fi(dir.filePath(dataFile));
const QString baseName = fi.completeBaseName();
const QString headPath = dir.filePath(baseName + QStringLiteral(".head"));
if (!QFile::exists(headPath)) continue;
try {
QFile headFile(headPath);
if (!headFile.open(QIODevice::ReadOnly | QIODevice::Text)) continue;
const std::string headText = headFile.readAll().toStdString();
const io::gpr::IprHeader h = io::gpr::parseIprHeader(headText);
if (h.numberOfCh > 1) {
// 模式 A单文件多通道
RadarSurveyInfo info;
info.surveyName = baseName;
info.ddCode = QStringLiteral("dd_radar_3d");
info.channelCount = h.numberOfCh;
info.folderPath = dirPath;
info.dataFiles << fi.absoluteFilePath();
info.meta = h;
modeASurveys.push_back(std::move(info));
} else {
// 模式 B单通道先收集待分组
const QString key = surveyKeyFromFileName(baseName);
modeBSurveys[key] << baseName;
}
} catch (...) {
// 解析失败,跳过
continue;
}
}
// 处理模式 A
out.insert(out.end(), std::make_move_iterator(modeASurveys.begin()),
std::make_move_iterator(modeASurveys.end()));
// 处理模式 B按 key 分组,同 key 的多个 .data 属于同一条测线
for (auto it = modeBSurveys.begin(); it != modeBSurveys.end(); ++it) {
const QString& key = it.key();
const QStringList& baseNames = it.value();
if (baseNames.isEmpty()) continue;
RadarSurveyInfo info;
info.surveyName = key;
info.folderPath = dirPath;
info.channelCount = baseNames.size();
info.ddCode = (baseNames.size() == 1)
? QStringLiteral("dd_radar_2d")
: QStringLiteral("dd_radar_3d");
// 读取第一个 .head 作为元信息
const QString firstHeadPath = dir.filePath(baseNames.first() + QStringLiteral(".head"));
try {
QFile headFile(firstHeadPath);
if (headFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
info.meta = io::gpr::parseIprHeader(headFile.readAll().toStdString());
}
} catch (...) {
}
// 收集所有 .data 路径(按文件名排序)
for (const QString& bn : baseNames) {
info.dataFiles << dir.filePath(bn + QStringLiteral(".data"));
}
std::sort(info.dataFiles.begin(), info.dataFiles.end());
out.push_back(std::move(info));
}
return out;
}
std::vector<DsRow> LocalRadarScanner::toDsRows(const std::vector<RadarSurveyInfo>& surveys) {
std::vector<DsRow> out;
out.reserve(surveys.size());
for (const auto& s : surveys) {
DsRow row;
row.id = QStringLiteral("local-radar-%1")
.arg(QString::number(qHash(s.surveyName), 16).toStdString())
.toStdString();
row.dsName = s.surveyName.toStdString();
row.ddCode = s.ddCode.toStdString();
row.fileUrl = s.folderPath.toStdString();
row.typeName = (s.channelCount == 1) ? "雷达2D" : "雷达3D";
row.createTime = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss")).toStdString();
row.parentId.clear();
// structParentId 由调用方注入
out.push_back(std::move(row));
}
return out;
}
} // namespace geopro::data

View File

@ -0,0 +1,38 @@
#pragma once
#include <vector>
#include <QString>
#include <QStringList>
#include "repo/RepoTypes.hpp"
#include "io/gpr/IprHeader.hpp"
namespace geopro::data {
// 扫描发现的雷达测线信息
struct RadarSurveyInfo {
QString surveyName; // 测线名称(测线键)
QString ddCode; // "dd_radar_2d" 或 "dd_radar_3d"
int channelCount = 1; // 通道数
QString folderPath; // 测线所在文件夹绝对路径
QStringList dataFiles; // 该测线包含的 .data 文件路径列表
io::gpr::IprHeader meta; // 从头文件解析的元信息
};
// 本地雷达文件夹扫描器
class LocalRadarScanner {
public:
// 扫描 dirPath 下所有雷达测线。
// 规则:
// 1. 扫描所有 .data 文件,为每个 .data 找同名 .head。
// 2. 读取 .head 获取 NUMBER_OF_CH。
// 3. 若 NUMBER_OF_CH > 1该 .data 自身就是一条多通道测线(模式 A
// 4. 若 NUMBER_OF_CH <= 1按去掉最后一段 _xxx 后缀分组(模式 B
static std::vector<RadarSurveyInfo> scan(const QString& dirPath);
// 将扫描结果转换为 DsRow导入时由调用方注入 structParentId
static std::vector<DsRow> toDsRows(const std::vector<RadarSurveyInfo>& surveys);
private:
static QString surveyKeyFromFileName(const QString& baseName);
};
} // namespace geopro::data

View File

@ -1,6 +1,8 @@
#include "api/ApiDatasetRepository.hpp"
#include <stdexcept>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
@ -161,7 +163,7 @@ net::ApiBatch* gridRowsBatch(net::ApiClient& api, const std::string& dsId, int p
ApiDatasetRepository::ApiDatasetRepository(net::ApiClient& api) : api_(api) {}
DetailLoad* ApiDatasetRepository::loadAsync(const std::string& loaderKey, const std::string& dsId,
int pageNo, int pageSize) {
int pageNo, int pageSize, const QString& fileUrl) {
if (loaderKey == "inversion.scatter") return makeInversionScatter(dsId);
if (loaderKey == "inversion.grid") return makeInversionGrid(dsId);
if (loaderKey == "ert_measurement.scatter") return makeMeasurementScatter(dsId);
@ -172,9 +174,9 @@ DetailLoad* ApiDatasetRepository::loadAsync(const std::string& loaderKey, const
if (loaderKey == "traj.elev") return makeTrajectoryElevation(dsId);
if (loaderKey == "traj.map") return makeTrajectoryMap(dsId);
if (loaderKey == "grid.rows") return makeGridRows(dsId, pageNo, pageSize);
if (loaderKey == "radar.profile") return makeRadarProfile(dsId);
if (loaderKey == "radar.info") return makeRadarInfo(dsId);
if (loaderKey == "radar.anomalies") return makeRadarAnomalies(dsId);
if (loaderKey == "radar.profile") return makeRadarProfile(dsId, fileUrl);
if (loaderKey == "radar.info") return makeRadarInfo(dsId, fileUrl);
if (loaderKey == "radar.anomalies") return makeRadarAnomalies(dsId, fileUrl);
throw std::runtime_error("unknown loaderKey: " + loaderKey);
}
@ -253,11 +255,12 @@ DetailLoad* ApiDatasetRepository::makeGridRows(const std::string& dsId, int page
}
// ── 雷达数据集:本地文件加载(零网络请求)。 ──
// M1 阶段:路径未注册时返回空 payload视图层显示占位提示。
// 后续 app 层注入 radarPaths_ 映射后自动读取。
namespace {
using geopro::io::gpr::IprHeader;
using geopro::io::gpr::BScan;
// 读取文本文件全部内容UTF-8
std::string readTextFile(const QString& path) {
QFile f(path);
@ -267,26 +270,25 @@ std::string readTextFile(const QString& path) {
return f.readAll().toStdString();
}
// 从 .iprh + .iprb 组装 GprProfilePayload单通道 2d)。
geopro::core::GprProfilePayload buildRadar2dPayload(const QString& iprhPath,
const QString& iprbPath) {
// 从单个 .head + .data 组装单通道 GprProfilePayload兼容旧 .iprh/.iprb)。
geopro::core::GprProfilePayload buildRadar2dPayload(const QString& headPath,
const QString& dataPath) {
using namespace geopro::io::gpr;
const std::string headerText = readTextFile(iprhPath);
const std::string headerText = readTextFile(headPath);
const IprHeader h = parseIprHeader(headerText);
const BScan bscan = readIprb(iprbPath.toStdString(), h);
const BScan bscan = readIprb(dataPath.toStdString(), h);
geopro::core::GprProfilePayload payload;
payload.meta.ntraces = static_cast<int>(bscan.traces);
payload.meta.samples = bscan.samples;
payload.meta.dx = h.distanceInterval;
payload.meta.dz = h.timeWindowNs / (h.samples > 1 ? h.samples - 1 : 1);
payload.meta.velocityMPerNs = h.soilVelocity / 1e9; // m/s → m/ns
payload.meta.velocityMPerNs = h.soilVelocity / 1e9;
payload.date = QString::fromStdString(h.date);
payload.time = QString::fromStdString(h.startTime);
payload.antennaModel = QString::fromStdString(h.antennas);
payload.antennaFreqMHz = h.frequency;
// int16 → float行主序。
geopro::core::GprChannelData ch;
ch.channelName = QStringLiteral("CH01");
ch.data.reserve(bscan.data.size());
@ -296,27 +298,212 @@ geopro::core::GprProfilePayload buildRadar2dPayload(const QString& iprhPath,
return payload;
}
// 扫描文件夹,返回所有 .data 文件路径(同时兼容 .iprb
QStringList findDataFilesInFolder(const QString& folderPath) {
QDir dir(folderPath);
QStringList filters;
filters << QStringLiteral("*.data") << QStringLiteral("*.iprb");
return dir.entryList(filters, QDir::Files | QDir::NoDotAndDotDot, QDir::Name);
}
// 为 .data 找同名 .head或 .iprh
QString findHeadPath(const QString& dataPath) {
QFileInfo fi(dataPath);
const QString base = fi.path() + QStringLiteral("/") + fi.completeBaseName();
QString headPath = base + QStringLiteral(".head");
if (QFile::exists(headPath)) return headPath;
headPath = base + QStringLiteral(".iprh");
if (QFile::exists(headPath)) return headPath;
return QString();
}
// 从测线文件夹组装 GprProfilePayload支持模式 A 单文件多通道 + 模式 B 多文件单通道)。
geopro::core::GprProfilePayload buildRadarPayloadFromFolder(const QString& folderPath) {
using namespace geopro::io::gpr;
geopro::core::GprProfilePayload payload;
const QStringList dataFiles = findDataFilesInFolder(folderPath);
if (dataFiles.isEmpty()) {
throw std::runtime_error("测线文件夹内无 .data 或 .iprb 文件: " + folderPath.toStdString());
}
// 收集所有 .data 及其 .head
struct ChannelFile {
QString dataPath;
QString headPath;
IprHeader header;
};
std::vector<ChannelFile> files;
for (const QString& name : dataFiles) {
const QString dataPath = folderPath + QStringLiteral("/") + name;
const QString headPath = findHeadPath(dataPath);
if (headPath.isEmpty()) continue;
try {
IprHeader h = parseIprHeader(readTextFile(headPath));
files.push_back({dataPath, headPath, h});
} catch (...) {
continue;
}
}
if (files.empty()) {
throw std::runtime_error("测线文件夹内无有效头文件: " + folderPath.toStdString());
}
// 取第一个头作为公共元信息
const IprHeader& firstH = files.front().header;
payload.meta.samples = firstH.samples;
payload.meta.dx = firstH.distanceInterval;
payload.meta.dz = firstH.timeWindowNs / (firstH.samples > 1 ? firstH.samples - 1 : 1);
payload.meta.velocityMPerNs = firstH.soilVelocity / 1e9;
payload.date = QString::fromStdString(firstH.date);
payload.time = QString::fromStdString(firstH.startTime);
payload.antennaModel = QString::fromStdString(firstH.antennas);
payload.antennaFreqMHz = firstH.frequency;
// 模式 A第一个 .data 的 NUMBER_OF_CH > 1单文件多通道交错
if (firstH.numberOfCh > 1) {
std::vector<BScan> bscans = readMultiChannelData(files.front().dataPath.toStdString(), firstH);
payload.meta.ntraces = static_cast<int>(bscans.front().traces);
for (std::size_t c = 0; c < bscans.size(); ++c) {
geopro::core::GprChannelData ch;
ch.channelName = QStringLiteral("CH%1").arg(c + 1, 2, 10, QLatin1Char('0'));
ch.data.reserve(bscans[c].data.size());
for (int16_t v : bscans[c].data) ch.data.push_back(static_cast<float>(v));
payload.channels.push_back(std::move(ch));
}
return payload;
}
// 模式 B多文件单通道
payload.meta.ntraces = static_cast<int>(files.front().header.lastTrace);
for (std::size_t c = 0; c < files.size(); ++c) {
const BScan bscan = readIprb(files[c].dataPath.toStdString(), files[c].header);
geopro::core::GprChannelData ch;
ch.channelName = QStringLiteral("CH%1").arg(c + 1, 2, 10, QLatin1Char('0'));
ch.data.reserve(bscan.data.size());
for (int16_t v : bscan.data) ch.data.push_back(static_cast<float>(v));
payload.channels.push_back(std::move(ch));
}
return payload;
}
// 从 IprHeader 构建采集参数 TablePayload
geopro::core::TablePayload buildRadarInfoPayload(const IprHeader& h) {
geopro::core::TablePayload payload;
payload.columns = {
{QStringLiteral("field"), QStringLiteral("参数名"), 120, 0, geopro::core::TableColumnKind::Text},
{QStringLiteral("value"), QStringLiteral(""), 200, 0, geopro::core::TableColumnKind::Text},
};
auto addRow = [&](const QString& name, const QString& value) {
payload.rows.push_back({name, value});
};
addRow(QStringLiteral("采样点数"), QString::number(h.samples));
addRow(QStringLiteral("道数"), QString::number(h.lastTrace));
addRow(QStringLiteral("通道数"), QString::number(h.numberOfCh));
addRow(QStringLiteral("时窗(ns)"), QString::number(h.timeWindowNs));
addRow(QStringLiteral("波速(m/s)"), QString::number(h.soilVelocity));
addRow(QStringLiteral("道间距(m)"), QString::number(h.distanceInterval));
addRow(QStringLiteral("日期"), QString::fromStdString(h.date));
addRow(QStringLiteral("开始时间"), QString::fromStdString(h.startTime));
addRow(QStringLiteral("结束时间"), QString::fromStdString(h.stopTime));
addRow(QStringLiteral("频率(MHz)"), QString::number(h.frequency));
addRow(QStringLiteral("叠加次数"), QString::number(h.stacks));
addRow(QStringLiteral("天线型号"), QString::fromStdString(h.antennas));
addRow(QStringLiteral("采集模式"), QString::fromStdString(h.mode));
addRow(QStringLiteral("深度(m)"), QString::number(h.depth));
addRow(QStringLiteral("介电常数"), QString::number(h.dielectric));
addRow(QStringLiteral("土壤类型"), QString::fromStdString(h.soilType));
addRow(QStringLiteral("单位"), QString::fromStdString(h.units));
addRow(QStringLiteral("Bits"), QString::number(h.bits));
return payload;
}
} // namespace
DetailLoad* ApiDatasetRepository::makeRadarProfile(const std::string& dsId) {
// M1路径未配置时返回空 payload视图显示占位
// TODO: 从 app 层注入的 radarPaths_ 映射读取真实路径。
DetailLoad* ApiDatasetRepository::makeRadarProfile(const std::string& dsId,
const QString& fileUrl) {
Q_UNUSED(dsId)
if (fileUrl.isEmpty()) {
geopro::core::GprProfilePayload payload;
return new LocalDetailLoad(QVariant::fromValue(payload));
}
QString path = fileUrl;
if (path.startsWith(QStringLiteral("file://"))) path = path.mid(7);
if (path.startsWith(QStringLiteral("http://")) ||
path.startsWith(QStringLiteral("https://"))) {
qWarning("[radar] 远程文件URL暂不支持直接加载: %s", qUtf8Printable(fileUrl));
geopro::core::GprProfilePayload payload;
return new LocalDetailLoad(QVariant::fromValue(payload));
}
QFileInfo fi(path);
try {
geopro::core::GprProfilePayload payload;
if (fi.isDir()) {
// 测线文件夹模式
payload = buildRadarPayloadFromFolder(path);
} else if (fi.suffix().compare(QStringLiteral("iprb"), Qt::CaseInsensitive) == 0 ||
fi.suffix().compare(QStringLiteral("data"), Qt::CaseInsensitive) == 0) {
// 单文件模式:尝试找同名头文件
QString headPath = findHeadPath(path);
if (!headPath.isEmpty()) {
payload = buildRadar2dPayload(headPath, path);
} else {
qWarning("[radar] 找不到头文件: %s", qUtf8Printable(path));
}
} else {
// 默认尝试按文件夹处理fileUrl 可能是文件夹路径但 QFileInfo 判断不准)
payload = buildRadarPayloadFromFolder(path);
}
return new LocalDetailLoad(QVariant::fromValue(payload));
} catch (const std::exception& e) {
qWarning("[radar] 加载失败: %s", e.what());
geopro::core::GprProfilePayload payload;
return new LocalDetailLoad(QVariant::fromValue(payload));
}
}
DetailLoad* ApiDatasetRepository::makeRadarInfo(const std::string& dsId) {
DetailLoad* ApiDatasetRepository::makeRadarInfo(const std::string& dsId,
const QString& fileUrl) {
Q_UNUSED(dsId)
// 采集参数页签TablePayload从 .iprh 解析的字段。
// M1 先返回空表格占位。
geopro::core::TablePayload payload;
if (fileUrl.isEmpty()) {
return new LocalDetailLoad(QVariant::fromValue(payload));
}
QString path = fileUrl;
if (path.startsWith(QStringLiteral("file://"))) path = path.mid(7);
try {
QFileInfo fi(path);
QString headPath;
if (fi.isDir()) {
// 找文件夹内第一个 .head 或 .iprh
QDir dir(path);
QStringList heads = dir.entryList(QStringList() << QStringLiteral("*.head")
<< QStringLiteral("*.iprh"),
QDir::Files);
if (!heads.isEmpty()) headPath = dir.filePath(heads.first());
} else {
headPath = findHeadPath(path);
}
if (!headPath.isEmpty()) {
IprHeader h = geopro::io::gpr::parseIprHeader(readTextFile(headPath));
payload = buildRadarInfoPayload(h);
}
} catch (const std::exception& e) {
qWarning("[radar] 采集参数加载失败: %s", e.what());
}
return new LocalDetailLoad(QVariant::fromValue(payload));
}
DetailLoad* ApiDatasetRepository::makeRadarAnomalies(const std::string& dsId) {
DetailLoad* ApiDatasetRepository::makeRadarAnomalies(const std::string& dsId,
const QString& fileUrl) {
Q_UNUSED(dsId)
// 异常列表页签:空列表占位。
Q_UNUSED(fileUrl)
// 异常列表页签TODO 解析 .mrk 文件
geopro::core::TablePayload payload;
return new LocalDetailLoad(QVariant::fromValue(payload));
}

View File

@ -1,5 +1,6 @@
#pragma once
#include "repo/IAsyncDatasetRepository.hpp"
#include <QString>
namespace geopro::net { class ApiClient; }
namespace geopro::data {
@ -8,7 +9,8 @@ class ApiDatasetRepository : public IAsyncDatasetRepository {
public:
explicit ApiDatasetRepository(net::ApiClient& api);
DetailLoad* loadAsync(const std::string& loaderKey, const std::string& dsId,
int pageNo = 1, int pageSize = 0) override;
int pageNo = 1, int pageSize = 0,
const QString& fileUrl = QString()) override;
private:
DetailLoad* makeInversionScatter(const std::string& dsId);
DetailLoad* makeInversionGrid(const std::string& dsId);
@ -22,9 +24,9 @@ private:
DetailLoad* makeGridRows(const std::string& dsId, int pageNo, int pageSize);
// 雷达数据集:本地文件加载(零网络)。
DetailLoad* makeRadarProfile(const std::string& dsId);
DetailLoad* makeRadarInfo(const std::string& dsId);
DetailLoad* makeRadarAnomalies(const std::string& dsId);
DetailLoad* makeRadarProfile(const std::string& dsId, const QString& fileUrl);
DetailLoad* makeRadarInfo(const std::string& dsId, const QString& fileUrl);
DetailLoad* makeRadarAnomalies(const std::string& dsId, const QString& fileUrl);
net::ApiClient& api_;
};

View File

@ -1,5 +1,6 @@
#pragma once
#include <string>
#include <QString>
namespace geopro::data {
@ -13,7 +14,8 @@ public:
// pageNo/pageSize 仅分页型 loaderKey如 grid.rows使用其余 loaderKey 忽略。
// 默认 pageNo=1/pageSize=0 → 非分页加载(分页 loaderKey 自行解析默认每页条数)。
virtual DetailLoad* loadAsync(const std::string& loaderKey, const std::string& dsId,
int pageNo = 1, int pageSize = 0) = 0;
int pageNo = 1, int pageSize = 0,
const QString& fileUrl = QString()) = 0;
};
} // namespace geopro::data

View File

@ -89,4 +89,61 @@ BScan readIprbRange(const std::string& path, const IprHeader& h,
return b;
}
std::vector<BScan> readMultiChannelData(const std::string& path, const IprHeader& h) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) {
throw std::runtime_error("readMultiChannelData: 无法打开文件: " + path);
}
const std::int64_t fileBytes = static_cast<std::int64_t>(f.tellg());
const std::int64_t samples = static_cast<std::int64_t>(h.samples);
const int channels = h.numberOfCh > 1 ? h.numberOfCh : 1;
if (samples <= 0) {
throw std::runtime_error("readMultiChannelData: samples 非法(<=0): " + path);
}
const std::int64_t bytesPerBlock = samples * 2; // int16
if (fileBytes % bytesPerBlock != 0) {
throw std::runtime_error(
"readMultiChannelData: 文件大小不是 samples*2 的整数倍: " + path);
}
const std::int64_t totalBlocks = fileBytes / bytesPerBlock;
if (totalBlocks % channels != 0) {
throw std::runtime_error(
"readMultiChannelData: 总块数不能被通道数整除: " + path);
}
const std::int64_t tracesPerChannel = totalBlocks / channels;
// 预分配所有通道
std::vector<BScan> out(channels);
for (int c = 0; c < channels; ++c) {
out[c].samples = h.samples;
out[c].traces = tracesPerChannel;
out[c].data.resize(static_cast<std::size_t>(samples * tracesPerChannel));
}
// 读取全部数据
std::vector<int16_t> buffer(static_cast<std::size_t>(totalBlocks * samples));
f.seekg(0, std::ios::beg);
f.read(reinterpret_cast<char*>(buffer.data()),
static_cast<std::streamsize>(fileBytes));
if (!f) {
throw std::runtime_error("readMultiChannelData: 读取数据失败: " + path);
}
// 按交错布局分发到各通道
// globalBlockIdx = t * channels + c 对应 通道 c 的 trace t
for (std::int64_t t = 0; t < tracesPerChannel; ++t) {
for (int c = 0; c < channels; ++c) {
const std::int64_t globalBlockIdx = t * channels + c;
const std::size_t srcOffset = static_cast<std::size_t>(globalBlockIdx * samples);
const std::size_t dstOffset = static_cast<std::size_t>(t * samples);
std::copy(buffer.begin() + srcOffset,
buffer.begin() + srcOffset + static_cast<std::size_t>(samples),
out[c].data.begin() + dstOffset);
}
}
return out;
}
} // namespace geopro::io::gpr

View File

@ -29,6 +29,12 @@ BScan readIprb(const std::string& path, const IprHeader& h);
BScan readIprbRange(const std::string& path, const IprHeader& h,
std::int64_t t0, std::int64_t t1);
// 读取多通道交错 .data 文件。
// 文件布局:[ch0_t0采样][ch1_t0采样]...[chM_t0采样][ch0_t1采样]...
// totalBlocks = fileBytes / (samples*2); tracesPerChannel = totalBlocks / channels;
// 返回 std::vector<BScan>,每个元素对应一个通道。
std::vector<BScan> readMultiChannelData(const std::string& path, const IprHeader& h);
} // namespace geopro::io::gpr
#endif // GEOPRO_IO_GPR_IPRBREADER_HPP

View File

@ -48,7 +48,7 @@ struct StubAsyncRepo : data::IAsyncDatasetRepository {
StubDetailLoad* last = nullptr;
int lastPageNo = 0, lastPageSize = 0; // 记录最近一次分页参数(验证 loadTabPaged 透传)
data::DetailLoad* loadAsync(const std::string&, const std::string&, int pageNo,
int pageSize) override {
int pageSize, const QString&) override {
lastPageNo = pageNo;
lastPageSize = pageSize;
last = new StubDetailLoad;

View File

@ -110,7 +110,8 @@ TEST(LocalSample3dRepo, LoadTerrainPathsCallsBack) {
namespace {
// 极简桩volumeInfo/createVolume 不触碰 dsRepo_loadAsync 直接回空。
struct StubAsyncRepo : IAsyncDatasetRepository {
DetailLoad* loadAsync(const std::string&, const std::string&, int, int) override {
DetailLoad* loadAsync(const std::string&, const std::string&, int, int,
const QString&) override {
return nullptr;
}
};

View File

@ -85,7 +85,7 @@ TEST(IprHeaderExtended, missingOptionalFieldsAllowed) {
const IprHeader h = parseIprHeader(text);
EXPECT_EQ(h.samples, 100);
EXPECT_TRUE(h.date.isEmpty());
EXPECT_TRUE(h.date.empty());
EXPECT_DOUBLE_EQ(h.frequency, 0.0);
}