feat/vtk-merged-dataset-column #10

Merged
gaozheng merged 40 commits from feat/vtk-merged-dataset-column into main 2026-07-01 14:48:38 +08:00
10 changed files with 128 additions and 10 deletions
Showing only changes of commit c0b6b31a9a - Show all commits

View File

@ -16,6 +16,7 @@
#include "data/store/ChunkedVolumeStore.hpp"
#include "io/gpr/GprSurveyAssembler.hpp"
#include "io/gpr/IprHeader.hpp"
#include "io/gpr/LocalPath.hpp"
namespace geopro::data {
@ -49,7 +50,7 @@ std::string toHeaderPath(const std::string& iprbPath) {
}
std::string readFileText(const std::string& path) {
std::ifstream f(path, std::ios::binary);
std::ifstream f(geopro::io::gpr::localPath(path), std::ios::binary);
if (!f) throw std::runtime_error("StreamingVolumeBuilder: 无法打开 " + path);
std::ostringstream ss;
ss << f.rdbuf();
@ -68,7 +69,7 @@ std::int64_t totalTraces(const std::vector<std::string>& iprb, double& surveyDx)
throw std::runtime_error("StreamingVolumeBuilder: samples<=0");
if (c == 0) surveyDx = h.distanceInterval;
const std::int64_t bytes =
static_cast<std::int64_t>(fs::file_size(fs::path(iprb[c])));
static_cast<std::int64_t>(fs::file_size(geopro::io::gpr::localPath(iprb[c])));
const std::int64_t per = static_cast<std::int64_t>(h.samples) * 2;
if (per <= 0 || bytes % per != 0)
throw std::runtime_error("StreamingVolumeBuilder: .iprb 字节非整道");

View File

@ -1,6 +1,6 @@
add_library(geopro_io_gpr STATIC
IprHeader.cpp IprbReader.cpp GprGeometry.cpp GprSurveyAssembler.cpp
GpsTrack.cpp NormalizedRadarReader.cpp
GpsTrack.cpp NormalizedRadarReader.cpp LocalPath.cpp
RadarVolumeAssembler.cpp NormalizedRadarVolumeBridge.cpp)
target_include_directories(geopro_io_gpr PUBLIC ${CMAKE_SOURCE_DIR}/src)
target_compile_features(geopro_io_gpr PUBLIC cxx_std_17)

View File

@ -10,6 +10,7 @@
#include "io/gpr/GprGeometry.hpp"
#include "io/gpr/IprHeader.hpp"
#include "io/gpr/IprbReader.hpp"
#include "io/gpr/LocalPath.hpp"
namespace geopro::io::gpr {
namespace {
@ -27,7 +28,7 @@ std::string toHeaderPath(const std::string& iprbPath) {
}
std::string readFileText(const std::string& path) {
std::ifstream f(path, std::ios::binary);
std::ifstream f(localPath(path), std::ios::binary);
if (!f) {
throw std::runtime_error("无法打开文件: " + path);
}

View File

@ -7,6 +7,8 @@
#include <sstream>
#include <stdexcept>
#include "io/gpr/LocalPath.hpp"
namespace geopro::io::gpr {
namespace {
@ -26,7 +28,7 @@ bool parseDouble(const std::string& s, double& out) {
} // namespace
GpsTrack parseGps(const std::string& path) {
std::ifstream f(path);
std::ifstream f(localPath(path));
if (!f) throw std::runtime_error("parseGps: 打不开 " + path);
GpsTrack track;

View File

@ -4,10 +4,12 @@
#include <fstream>
#include <stdexcept>
#include "io/gpr/LocalPath.hpp"
namespace geopro::io::gpr {
BScan readIprb(const std::string& path, const IprHeader& h) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
std::ifstream f(localPath(path), std::ios::binary | std::ios::ate);
if (!f) {
throw std::runtime_error("readIprb: 无法打开文件: " + path);
}
@ -43,7 +45,7 @@ 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) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
std::ifstream f(localPath(path), std::ios::binary | std::ios::ate);
if (!f) {
throw std::runtime_error("readIprbRange: 无法打开文件: " + path);
}

24
src/io/gpr/LocalPath.cpp Normal file
View File

@ -0,0 +1,24 @@
#include "io/gpr/LocalPath.hpp"
#ifdef _WIN32
#include <windows.h>
#endif
namespace geopro::io::gpr {
std::filesystem::path localPath(const std::string& p) {
#ifdef _WIN32
if (p.empty()) return std::filesystem::path{};
const int wlen = ::MultiByteToWideChar(
CP_ACP, 0, p.data(), static_cast<int>(p.size()), nullptr, 0);
if (wlen <= 0) return std::filesystem::path(p); // 退化:原样(ASCII 安全)
std::wstring w(static_cast<std::size_t>(wlen), L'\0');
::MultiByteToWideChar(CP_ACP, 0, p.data(), static_cast<int>(p.size()),
w.data(), wlen);
return std::filesystem::path(w);
#else
return std::filesystem::path(p); // POSIX窄字节即 UTF-8 原生路径
#endif
}
} // namespace geopro::io::gpr

16
src/io/gpr/LocalPath.hpp Normal file
View File

@ -0,0 +1,16 @@
#pragma once
#include <filesystem>
#include <string>
namespace geopro::io::gpr {
// 把"本地 8 位编码"的窄字节路径转成 std::filesystem::path。
// Windows源串按当前 ANSI 代码页(简中=GBK/936即 QString::toLocal8Bit 产物)
// 解码为宽字符 path使 std::ifstream/ofstream 走宽字符打开 —— 否则
// 窄字符 ifstream 在默认 "C" locale 下无法解析含中文的路径open 失败。
// 其它平台:窄字节即原生 UTF-8 路径,直接构造。
// 退化保护:转换失败时原样返回(ASCII 路径不受影响)。
std::filesystem::path localPath(const std::string& p);
} // namespace geopro::io::gpr

View File

@ -1,4 +1,5 @@
#include "io/gpr/NormalizedRadarReader.hpp"
#include "io/gpr/LocalPath.hpp"
#include <cmath>
#include <cstdint>
#include <filesystem>
@ -76,11 +77,11 @@ std::vector<std::int16_t> readRadarDataCube(const std::string& dataPath,
const std::size_t n = static_cast<std::size_t>(h.lastTrace) * h.samples;
const std::uintmax_t expect = static_cast<std::uintmax_t>(n) * 2;
std::error_code ec;
const auto fsize = std::filesystem::file_size(dataPath, ec);
const auto fsize = std::filesystem::file_size(localPath(dataPath), ec);
if (ec || fsize != expect)
throw std::runtime_error("规范化 .data 大小不符: " + dataPath);
std::vector<std::int16_t> cube(n);
std::ifstream f(dataPath, std::ios::binary);
std::ifstream f(localPath(dataPath), std::ios::binary);
if (!f) throw std::runtime_error("打开 .data 失败: " + dataPath);
f.read(reinterpret_cast<char*>(cube.data()), static_cast<std::streamsize>(expect));
if (!f) throw std::runtime_error("读 .data 失败: " + dataPath);

View File

@ -7,6 +7,7 @@
#include <stdexcept>
#include <vector>
#include "io/gpr/LocalPath.hpp"
#include "io/gpr/NormalizedRadarReader.hpp"
#include "io/gpr/RadarVolumeAssembler.hpp"
@ -20,7 +21,7 @@ geopro::core::BuiltI16 buildLineVolumeFromNormalized(const std::string& lineDir,
std::string headText;
{
std::ifstream f(head);
std::ifstream f(localPath(head));
if (!f) throw std::runtime_error("打开 .head 失败: " + head);
std::stringstream ss;
ss << f.rdbuf();

View File

@ -1,11 +1,29 @@
#include <gtest/gtest.h>
#include <clocale>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string>
#include "core/algo/GprVolumeBuilder.hpp"
#include "io/gpr/NormalizedRadarVolumeBridge.hpp"
#ifdef _WIN32
#include <windows.h>
#endif
namespace fs = std::filesystem;
namespace {
// 在指定目录写出一组最小可解析的规范化 .head/.dataK=4 道 M=2 通道 N=3 采样)。
void writeMinimalLine(const fs::path& head, const fs::path& data) {
{ std::ofstream f(head);
f << "SAMPLES:3\nNUMBER_OF_CH:2\nLAST_TRACE:8\nBITS:16\nENDIAN_TYPE:1\n"
"DISTANCE_INTERVAL:0.1\nTIMEWINDOW:30\nDIELECTRIC:9\n"; }
{ std::ofstream f(data, std::ios::binary);
for (int t = 0; t < 4; ++t) for (int c = 0; c < 2; ++c) for (int s = 0; s < 3; ++s) {
std::int16_t v = static_cast<std::int16_t>(t * 10 + c * 100 + s);
f.write(reinterpret_cast<const char*>(&v), 2); } }
}
} // namespace
TEST(NormalizedRadarBridge, BuildsVolumeWithExpectedAxes) {
// K=4 道, M=2 通道, N=3 采样, 无通道偏移(不插值), coarse=1。
fs::path dir = fs::temp_directory_path() / "radar_bridge_test";
@ -26,3 +44,55 @@ TEST(NormalizedRadarBridge, BuildsVolumeWithExpectedAxes) {
EXPECT_GT(b.spacing[2], 0.0); // dz 由 timewindow/dielectric 求得 >0
EXPECT_NEAR(b.quant.toPhys(b.vol.at(3, 1, 2)), 132.0, b.quant.scale); // t3c1s2=30+100+2
}
// 回归中文目录路径必须能打开渲染。app 传入的是 QString::toLocal8Bit(),即当前
// ANSI 代码页(简中=GBK)窄字节。关键复现条件——GUI app 链接 QWebEngine(Chromium)/VTK
// 它们在启动时 setlocale(LC_ALL,"") 把 LC_CTYPE 提升为系统 UTF-8 locale此后窄字符
// ifstream 会把 GBK 路径字节当 UTF-8 解析 → open 失败(即"打开 .head 失败")。
// 故本测试显式置 UTF-8 locale 复现该失败面,走宽字符打开(见 io/gpr/LocalPath)守护回归。
// (纯 "C" locale 下 UCRT 用 CP_ACP=GBK 解窄路径,反而不失败,无法复现——须置 UTF-8。)
TEST(NormalizedRadarBridge, OpensCjkDirectoryPathUnderUtf8Locale) {
#ifdef _WIN32
const std::wstring wname = L"radar_cjk_南同大道";
const fs::path dir = fs::temp_directory_path() / wname;
fs::remove_all(dir);
fs::create_directories(dir);
writeMinimalLine(dir / L"南同大道_000.head", dir / L"南同大道_000.data");
// 模拟 app宽字符 → 当前 ANSI 代码页(GBK)窄字节,等价 QString::toLocal8Bit()。
auto toAcp = [](const std::wstring& w) {
const int n = ::WideCharToMultiByte(CP_ACP, 0, w.data(),
static_cast<int>(w.size()), nullptr, 0,
nullptr, nullptr);
std::string s(static_cast<std::size_t>(n), '\0');
::WideCharToMultiByte(CP_ACP, 0, w.data(), static_cast<int>(w.size()),
s.data(), n, nullptr, nullptr);
return s;
};
const std::string dirAcp = toAcp(dir.wstring());
const std::string prefixAcp = toAcp(L"南同大道_000");
// 复现 app 运行期的 UTF-8 C locale(QWebEngine/VTK 所置)——不修复则 narrow open 失败。
const char* prevC = std::setlocale(LC_ALL, nullptr);
const std::string savedC = prevC ? prevC : "C";
std::setlocale(LC_ALL, ".UTF-8");
geopro::core::BuiltI16 b;
try {
b = geopro::io::gpr::buildLineVolumeFromNormalized(
dirAcp, prefixAcp, /*coarse=*/1, /*targetDy=*/0.0);
} catch (...) {
std::setlocale(LC_ALL, savedC.c_str());
fs::remove_all(dir);
throw; // 未修复时会在此抛"打开 .head 失败"→ 测试红,正是回归守护
}
std::setlocale(LC_ALL, savedC.c_str());
EXPECT_EQ(b.vol.nx(), 4);
EXPECT_EQ(b.vol.ny(), 2);
EXPECT_EQ(b.vol.nz(), 3);
fs::remove_all(dir);
#else
GTEST_SKIP() << "中文窄路径打开问题仅 Windows 相关";
#endif
}