```
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:
parent
3b894da2ec
commit
ad6699d48d
|
|
@ -90,6 +90,7 @@ add_executable(geopro_desktop WIN32
|
|||
ProjectListDialog.cpp
|
||||
ObjectFormDialog.cpp
|
||||
ImportDatasetDialog.cpp
|
||||
ImportLocalRadarDialog.cpp
|
||||
ExportDatasetDialog.cpp
|
||||
AnomalySaveDialog.cpp
|
||||
AnomalyPropertiesDialog.cpp
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
DetailLoad* ApiDatasetRepository::makeRadarInfo(const std::string& dsId) {
|
||||
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,
|
||||
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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue