geopro/external/gpr3dviewer/IprhParser.cpp

626 lines
24 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "IprhParser.h"
#include "PerformanceLogger.h"
#include "ImpulseMultiChannelConverter.h"
#include <QFile>
#include <QTextStream>
#include <QDataStream>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QRegularExpression>
#include <QStringList>
#include <QVector3D>
#include <QLocale>
#include <algorithm>
#include <cstring>
/**
* @brief 加载 Impulse 系列雷达 IPRH 数据iprh头文件 + iprb纯二进制波形
* @param iprhFilePath .iprh 文本配置头文件路径
* @param model 输出 GPR 全局数据模型
* @return true 加载解析成功false 失败
* @note 存储结构:.iprh 存储全部仪器/测线参数;.iprb 无文件头,从头到尾全是 short16 振幅采样
*/
bool IprhParser::loadFromIprh(const QString &iprhFilePath, GPRDataModel &model) {
SCOPED_PERF_TIMER("Parser", "IprhParser::loadFromIprh");
model.clear();
model.header = GPRDataModel::Header{};
// 第一步:解析 iprh 文本头
if (!parseIprhHeader(iprhFilePath, model.header)) {
qDebug() << "Error: Failed to parse .iprh header file:" << iprhFilePath;
return false;
}
// 自动匹配同目录同名二进制数据文件 .iprb
QFileInfo iprhInfo(iprhFilePath);
QString iprbPath = iprhInfo.absolutePath() + "/" + iprhInfo.completeBaseName() + ".iprb";
if (!QFile::exists(iprbPath)) {
qDebug() << "Error: Matching .iprb binary file not found at:" << iprbPath;
return false;
}
// 如果头文件没有 LAST TRACE从二进制文件大小推算
if (model.header.numTraces <= 0) {
QFile binaryFile(iprbPath);
if (binaryFile.open(QIODevice::ReadOnly)) {
qint64 fileSize = binaryFile.size();
qint64 traceBytes = static_cast<qint64>(model.header.samplesPerTrace) * sizeof(short);
if (traceBytes > 0) {
model.header.numTraces = static_cast<int>(fileSize / traceBytes);
qDebug() << "Inferred numTraces from binary size:" << model.header.numTraces;
}
binaryFile.close();
}
}
if (model.header.timeWindowNs <= 0.0 && model.header.timeIntervalNs > 0.0) {
model.header.timeWindowNs = model.header.samplesPerTrace * model.header.timeIntervalNs;
qDebug() << "Inferred timeWindowNs from samples * timeInterval:" << model.header.timeWindowNs;
}
if (model.header.numTraces <= 0) {
qDebug() << "Error: Unable to determine numTraces from header or binary file";
return false;
}
// 第二步:读取纯二进制波形
return loadIprbBinary(iprbPath, model);
}
/**
* @brief 解析 IPRH 文本头文件,提取雷达采集关键参数
* @param iprhFilePath iprh 文本文件路径
* @param header 待填充头部参数结构体
* @return 解析成功返回 true
*/
bool IprhParser::parseHeaderOnly(const QString &iprhFilePath, GPRDataModel::Header &header)
{
return parseIprhHeader(iprhFilePath, header);
}
bool IprhParser::parseIprhHeader(const QString &iprhFilePath, GPRDataModel::Header &header) {
SCOPED_PERF_TIMER("Parser", "IprhParser::parseIprhHeader");
QFile file(iprhFilePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "Error: Cannot open iprh file for read";
return false;
}
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (line.isEmpty()) continue;
int sepPos = line.indexOf(':');
if (sepPos == -1) continue;
QString key = line.left(sepPos).trimmed();
QString value = line.mid(sepPos + 1).trimmed();
header.rawParams[key] = value;
if (key == "SAMPLES") {
header.samplesPerTrace = extractInt(value);
} else if (key == "LAST TRACE") {
header.numTraces = extractInt(value);
} else if (key == "TIMEWINDOW") {
header.timeWindowNs = extractDouble(value);
} else if (key == "DISTANCE INTERVAL") {
header.distanceInc = extractDouble(value);
} else if (key == "TIME INTERVAL") {
header.timeIntervalNs = extractDouble(value);
} else if (key == "ANTENNAS") {
header.antennaFreq = extractDouble(value);
} else if (key == "ANTENNA") {
header.antennaType = value;
} else if (key == "DATE") {
header.date = value;
} else if (key == "START TIME" || key == "TIME") {
header.timeStr = value;
} else if (key == "CHANNELS" || key == "NUMBER_OF_CH") {
header.numberOfChannels = extractInt(value);
} else if (key == "CH_X_OFFSET" || key == "CH_OFFSET_X") {
// 单通道偏移量Impulse 单通道文件)
if (!value.isEmpty()) {
header.chXOffsets.append(value.toFloat());
}
} else if (key == "CH_Y_OFFSET" || key == "CH_OFFSET_Y") {
if (!value.isEmpty()) {
header.chYOffsets.append(value.toFloat());
}
} else if (key == "CH_X_OFFSETS") {
// 兼容 Mala Mira 风格多值空格分隔
QStringList offsets = value.split(' ', Qt::SkipEmptyParts);
for (const QString &offset : offsets) {
header.chXOffsets.append(offset.toFloat());
}
} else if (key == "CH_Y_OFFSETS") {
QStringList offsets = value.split(' ', Qt::SkipEmptyParts);
for (const QString &offset : offsets) {
header.chYOffsets.append(offset.toFloat());
}
} else if (key == "UNITS") {
header.units = value;
} else if (key == "START POSITION") {
header.startPosition = extractDouble(value);
} else if (key == "STOP POSITION") {
header.stopPosition = extractDouble(value);
}
}
file.close();
if (header.samplesPerTrace <= 0) {
qDebug() << "Error: Invalid SAMPLES value in iprh file";
return false;
}
header.waveVelocity = 0.1;
qDebug() << "==== IPRH Header Parse Complete ===="
<< "\nSamples per trace:" << header.samplesPerTrace
<< "\nTotal traces:" << header.numTraces
<< "\nTime window(ns):" << header.timeWindowNs
<< "\nChannel count:" << header.numberOfChannels
<< "\nX offset array size:" << header.chXOffsets.size()
<< "\nY offset array size:" << header.chYOffsets.size();
return true;
}
/**
* @brief 读取纯 IPRB 二进制数据(无任何文件头,全连续 short16 振幅)
* @param iprbFilePath iprb 波形文件路径
* @param model 绑定头部参数,填充完整 traces 波形数组
* @return 读取成功 true
*/
bool IprhParser::loadIprbBinary(const QString &iprbFilePath, GPRDataModel &model) {
SCOPED_PERF_TIMER("Parser", "IprhParser::loadIprbBinary");
QFile file(iprbFilePath);
if (!file.open(QIODevice::ReadOnly)) {
qDebug() << "Error: Open iprb binary failed:" << iprbFilePath;
return false;
}
const int samplesPerTrace = model.header.samplesPerTrace;
const int totalTraceCount = model.header.numTraces;
const qint64 dataStartOffset = 0;
file.seek(dataStartOffset);
const qint64 singleTraceByteSize = samplesPerTrace * sizeof(short);
const qint64 fullExpectedBytes = totalTraceCount * singleTraceByteSize;
const qint64 realFileBytes = file.size();
if (realFileBytes < fullExpectedBytes) {
qDebug() << "Warning: IPRB file size smaller than theoretical data size!"
<< "Expected:" << fullExpectedBytes << "Actual:" << realFileBytes;
}
model.traces.reserve(totalTraceCount);
QByteArray traceBuffer;
traceBuffer.resize(singleTraceByteSize);
const int channelCnt = model.header.numberOfChannels > 0 ? model.header.numberOfChannels : 1;
model.channels = channelCnt;
model.tracesPerChannel = totalTraceCount / channelCnt;
if (model.header.distanceInc > 1e-6) {
model.totalDistance = static_cast<float>(model.tracesPerChannel * model.header.distanceInc);
} else {
model.totalDistance = static_cast<float>(model.header.stopPosition - model.header.startPosition);
}
for (int traceGlobalIdx = 0; traceGlobalIdx < totalTraceCount; ++traceGlobalIdx) {
if (file.atEnd()) {
qDebug() << "Warning: File ended early at global trace index" << traceGlobalIdx;
break;
}
qint64 readBytes = file.read(traceBuffer.data(), traceBuffer.size());
if (readBytes != traceBuffer.size()) {
qDebug() << "Warning: Trace" << traceGlobalIdx << "incomplete byte read";
break;
}
RadarTrace oneTrace;
oneTrace.amplitudes.resize(samplesPerTrace);
const short* rawShortBuf = reinterpret_cast<const short*>(traceBuffer.constData());
for (int s = 0; s < samplesPerTrace; s++) {
oneTrace.amplitudes[s] = rawShortBuf[s];
}
int chNo = traceGlobalIdx % channelCnt;
int traceInChIdx = traceGlobalIdx / channelCnt;
oneTrace.channelNumber = chNo;
float xOff = 0.0f;
float yOff = 0.0f;
if (chNo < model.header.chXOffsets.size()) xOff = model.header.chXOffsets[chNo];
if (chNo < model.header.chYOffsets.size()) yOff = model.header.chYOffsets[chNo];
float lineDist = static_cast<float>(model.header.startPosition + traceInChIdx * model.header.distanceInc);
oneTrace.position = QVector3D(xOff, lineDist - yOff, 0.0f);
model.traces.append(std::move(oneTrace));
}
file.close();
if (model.traces.isEmpty()) {
qDebug() << "Error: No valid traces loaded from iprb";
return false;
}
qDebug() << "IPRB Binary Load OK, total valid traces:" << model.traces.size();
return true;
}
/**
* @brief 加载 Impulse 多通道数据(每个通道一个 .iprh + .iprb
* @param dirPath 数据所在目录
* @param baseName 测线基础名(如 "明星路_001"
* @param model 输出合并后的 GPR 数据模型
* @return true 加载合并成功
*
* 文件命名约定baseName_A01.iprh / .iprb ... baseName_A14.iprh / .iprb
* 合并后 traces 按 Mala Mira 格式交错trace0=ch0_pos0, trace1=ch1_pos0, ...
*/
bool IprhParser::convertImpulseMultiChannelToMala(const QString &dirPath,
const QString &baseName,
QString *radFilePath,
QString *errorMessage)
{
ImpulseMultiChannelConverter::ConversionPlan plan;
if (!ImpulseMultiChannelConverter::buildPlan(dirPath, baseName, plan, errorMessage)) {
if (errorMessage && errorMessage->isEmpty()) {
*errorMessage = QStringLiteral("多通道 Impulse 数据合并失败:%1").arg(baseName);
}
return false;
}
ImpulseMultiChannelConverter::Options options;
options.overwriteExisting = true;
options.reuseExistingIfValid = false;
if (!ImpulseMultiChannelConverter::convertStreaming(plan, options, radFilePath, errorMessage)) {
return false;
}
qDebug() << "Impulse multi-channel converted to Mala Mira files:" << plan.outputRadPath << plan.outputRd3Path;
return true;
}
/**
* @brief 加载 Impulse 多通道数据(每个通道一个 .iprh + .iprb
* @param dirPath 数据所在目录
* @param baseName 测线基础名(如 "明星路_001"
* @param model 输出合并后的 GPR 数据模型
* @return true 加载合并成功
*
* 文件命名约定baseName_A01.iprh / .iprb ... baseName_A14.iprh / .iprb
* 合并后 traces 按 Mala Mira 格式交错trace0=ch0_pos0, trace1=ch1_pos0, ...
*/
bool IprhParser::loadImpulseMultiChannel(const QString &dirPath,
const QString &baseName,
GPRDataModel &model)
{
SCOPED_PERF_TIMER("Parser", "IprhParser::loadImpulseMultiChannel");
model.clear();
model.header = GPRDataModel::Header{};
// 1. 发现所有通道文件并排序
QDir dir(dirPath);
dir.setFilter(QDir::Files | QDir::NoDotAndDotDot);
QRegularExpression reChannel(QStringLiteral("^%1_A(\\d+)\\.iprh$").arg(QRegularExpression::escape(baseName)));
QMap<int, QString> channelHeaders; // channelNum -> iprh path
for (const QFileInfo &fi : dir.entryInfoList()) {
QRegularExpressionMatch match = reChannel.match(fi.fileName());
if (match.hasMatch()) {
int chNum = match.captured(1).toInt();
QString iprhPath = fi.absoluteFilePath();
QString iprbPath = iprhPath;
iprbPath.replace(".iprh", ".iprb");
if (QFile::exists(iprbPath)) {
channelHeaders.insert(chNum, iprhPath);
} else {
qDebug() << "Missing .iprb for" << iprhPath;
}
}
}
if (channelHeaders.isEmpty()) {
qDebug() << "No multi-channel .iprh/.iprb found for baseName:" << baseName;
return false;
}
const int channelCount = channelHeaders.size();
qDebug() << "Impulse multi-channel: found" << channelCount << "channels for" << baseName;
// 2. 解析第一个通道的头文件作为 master
auto it = channelHeaders.begin();
GPRDataModel::Header masterHeader;
if (!parseIprhHeader(it.value(), masterHeader)) {
qDebug() << "Failed to parse master header:" << it.value();
return false;
}
QVector<float> xOffsets;
QVector<float> yOffsets;
xOffsets.reserve(channelCount);
yOffsets.reserve(channelCount);
// 3. 验证各通道一致性并收集偏移量
QVector<int> channelTracesPerChannel;
channelTracesPerChannel.reserve(channelCount);
double antennaSeparation = 0.0;
for (auto cit = channelHeaders.begin(); cit != channelHeaders.end(); ++cit) {
int chNum = cit.key();
QString iprhPath = cit.value();
GPRDataModel::Header chHeader;
if (!parseIprhHeader(iprhPath, chHeader)) {
qDebug() << "Failed to parse channel header:" << iprhPath;
return false;
}
// 验证关键参数一致性
if (chHeader.samplesPerTrace != masterHeader.samplesPerTrace) {
qDebug() << "Inconsistent SAMPLES across channels";
return false;
}
if (qFuzzyCompare(chHeader.timeIntervalNs, masterHeader.timeIntervalNs) == false &&
chHeader.timeIntervalNs > 0 && masterHeader.timeIntervalNs > 0) {
qDebug() << "Warning: Inconsistent TIME INTERVAL across channels";
}
if (qFuzzyCompare(chHeader.distanceInc, masterHeader.distanceInc) == false &&
chHeader.distanceInc > 0 && masterHeader.distanceInc > 0) {
qDebug() << "Warning: Inconsistent DISTANCE INTERVAL across channels";
}
// 收集单通道偏移量
float xOff = 0.0f, yOff = 0.0f;
if (chHeader.rawParams.contains("CH_X_OFFSET")) {
xOff = chHeader.rawParams.value("CH_X_OFFSET").toFloat();
} else if (chHeader.rawParams.contains("CH_OFFSET_X")) {
xOff = chHeader.rawParams.value("CH_OFFSET_X").toFloat();
} else if (!chHeader.chXOffsets.isEmpty()) {
xOff = chHeader.chXOffsets.first();
}
if (chHeader.rawParams.contains("CH_Y_OFFSET")) {
yOff = chHeader.rawParams.value("CH_Y_OFFSET").toFloat();
} else if (chHeader.rawParams.contains("CH_OFFSET_Y")) {
yOff = chHeader.rawParams.value("CH_OFFSET_Y").toFloat();
} else if (!chHeader.chYOffsets.isEmpty()) {
yOff = chHeader.chYOffsets.first();
}
xOffsets.append(xOff);
yOffsets.append(yOff);
if (chHeader.rawParams.contains("ANTENNA SEPARATION")) {
antennaSeparation = chHeader.rawParams.value("ANTENNA SEPARATION").toDouble();
}
// 从 .iprb 大小计算该通道的道数
QString iprbPath = iprhPath;
iprbPath.replace(".iprh", ".iprb");
QFile iprbFile(iprbPath);
int tracesInThisChannel = 0;
if (iprbFile.open(QIODevice::ReadOnly)) {
qint64 fileSize = iprbFile.size();
qint64 traceBytes = static_cast<qint64>(chHeader.samplesPerTrace) * sizeof(short);
if (traceBytes > 0) {
tracesInThisChannel = static_cast<int>(fileSize / traceBytes);
}
iprbFile.close();
}
channelTracesPerChannel.append(tracesInThisChannel);
}
// 4. 以最小道数为准截断读取,容忍各通道微小差异
int tracesPerChannel = channelTracesPerChannel.isEmpty() ? 0 : *std::min_element(channelTracesPerChannel.begin(), channelTracesPerChannel.end());
for (int tc : channelTracesPerChannel) {
if (tc != tracesPerChannel) {
qDebug() << "Warning: Inconsistent trace count across channels. Using minimum:" << tracesPerChannel << "channel has:" << tc;
}
}
if (tracesPerChannel <= 0) {
qDebug() << "Error: No valid traces in any channel";
return false;
}
// 5. 如果偏移量全部为零,基于 ANTENNA SEPARATION 计算对称分布
bool allZeroOffsets = true;
for (float v : xOffsets) { if (qFuzzyIsNull(v) == false) { allZeroOffsets = false; break; } }
if (allZeroOffsets && antennaSeparation > 1e-6 && channelCount > 1) {
double totalWidth = (channelCount - 1) * antennaSeparation;
double startX = -totalWidth / 2.0;
for (int i = 0; i < channelCount; ++i) {
xOffsets[i] = static_cast<float>(startX + i * antennaSeparation);
}
qDebug() << "Computed symmetric X offsets from antenna separation:" << antennaSeparation;
}
// 6. 组装合并后的 Header
model.header = masterHeader;
model.header.numberOfChannels = channelCount;
model.header.numTraces = tracesPerChannel * channelCount;
model.header.chXOffsets = xOffsets;
model.header.chYOffsets = yOffsets;
if (model.header.timeWindowNs <= 0.0 && model.header.timeIntervalNs > 0.0) {
model.header.timeWindowNs = model.header.samplesPerTrace * model.header.timeIntervalNs;
}
// 7. 预分配 traces
model.traces.reserve(model.header.numTraces);
// 8. 读取每个通道的 .iprb 到临时数组
QVector<QVector<RadarTrace>> channelTraceArrays;
channelTraceArrays.resize(channelCount);
int chIdx = 0;
for (auto cit = channelHeaders.begin(); cit != channelHeaders.end(); ++cit, ++chIdx) {
QString iprhPath = cit.value();
QString iprbPath = iprhPath;
iprbPath.replace(".iprh", ".iprb");
QFile file(iprbPath);
if (!file.open(QIODevice::ReadOnly)) {
qDebug() << "Error: Cannot open" << iprbPath;
return false;
}
const int samplesPerTrace = model.header.samplesPerTrace;
const qint64 singleTraceByteSize = samplesPerTrace * sizeof(short);
QByteArray traceBuffer;
traceBuffer.resize(singleTraceByteSize);
channelTraceArrays[chIdx].reserve(tracesPerChannel);
for (int t = 0; t < tracesPerChannel; ++t) {
if (file.atEnd()) break;
qint64 readBytes = file.read(traceBuffer.data(), traceBuffer.size());
if (readBytes != traceBuffer.size()) break;
RadarTrace oneTrace;
oneTrace.amplitudes.resize(samplesPerTrace);
const short* rawShortBuf = reinterpret_cast<const short*>(traceBuffer.constData());
for (int s = 0; s < samplesPerTrace; s++) {
oneTrace.amplitudes[s] = rawShortBuf[s];
}
channelTraceArrays[chIdx].append(std::move(oneTrace));
}
file.close();
if (channelTraceArrays[chIdx].size() != tracesPerChannel) {
qDebug() << "Warning: Channel" << cit.key() << "has fewer traces than expected";
}
}
// 9. 按 Mala Mira 格式交错合并
model.channels = channelCount;
model.tracesPerChannel = tracesPerChannel;
for (int pos = 0; pos < tracesPerChannel; ++pos) {
for (int ch = 0; ch < channelCount; ++ch) {
if (pos < channelTraceArrays[ch].size()) {
RadarTrace trace = std::move(channelTraceArrays[ch][pos]);
trace.channelNumber = ch;
float xOff = (ch < xOffsets.size()) ? xOffsets[ch] : 0.0f;
float yOff = (ch < yOffsets.size()) ? yOffsets[ch] : 0.0f;
float lineDist = static_cast<float>(model.header.startPosition + pos * model.header.distanceInc);
trace.position = QVector3D(xOff, lineDist - yOff, 0.0f);
model.traces.append(std::move(trace));
}
}
}
if (model.header.distanceInc > 1e-6) {
model.totalDistance = static_cast<float>(model.tracesPerChannel * model.header.distanceInc);
} else {
model.totalDistance = static_cast<float>(model.header.stopPosition - model.header.startPosition);
}
qDebug() << "Impulse multi-channel load OK. Total traces:" << model.traces.size()
<< "Channels:" << channelCount << "Traces/Channel:" << tracesPerChannel;
return !model.traces.isEmpty();
}
bool IprhParser::writeMalaFiles(const QString &radFilePath,
const QString &rd3FilePath,
const GPRDataModel &model,
const QString &sourceBaseName,
QString *errorMessage)
{
QFile rd3File(rd3FilePath);
if (!rd3File.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
if (errorMessage) {
*errorMessage = QStringLiteral("无法写入转换后的 RD3 文件:%1").arg(rd3FilePath);
}
return false;
}
QDataStream out(&rd3File);
out.setByteOrder(QDataStream::LittleEndian);
const int samplesPerTrace = model.header.samplesPerTrace;
for (const RadarTrace &trace : model.traces) {
for (int s = 0; s < samplesPerTrace; ++s) {
const qint16 sample = static_cast<qint16>(s < trace.amplitudes.size() ? trace.amplitudes[s] : 0);
out << sample;
}
}
rd3File.close();
QFile radFile(radFilePath);
if (!radFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
if (errorMessage) {
*errorMessage = QStringLiteral("无法写入转换后的 RAD 文件:%1").arg(radFilePath);
}
QFile::remove(rd3FilePath);
return false;
}
QTextStream rad(&radFile);
rad << "# Converted from Impulse multi-channel survey: " << sourceBaseName << '\n';
rad << "SAMPLES: " << model.header.samplesPerTrace << '\n';
rad << "LAST TRACE: " << model.traces.size() << '\n';
rad << "TIMEWINDOW: " << QLocale::c().toString(model.header.timeWindowNs, 'g', 12) << '\n';
rad << "TIME INTERVAL: " << QLocale::c().toString(model.header.timeIntervalNs, 'g', 12) << '\n';
rad << "DISTANCE INTERVAL: " << QLocale::c().toString(model.header.distanceInc, 'g', 12) << '\n';
rad << "ANTENNAS: " << QLocale::c().toString(model.header.antennaFreq, 'g', 12) << '\n';
if (!model.header.antennaType.isEmpty()) rad << "ANTENNA: " << model.header.antennaType << '\n';
if (!model.header.date.isEmpty()) rad << "DATE: " << model.header.date << '\n';
if (!model.header.timeStr.isEmpty()) rad << "TIME: " << model.header.timeStr << '\n';
rad << "NUMBER_OF_CH: " << qMax(1, model.header.numberOfChannels) << '\n';
rad << "CH_X_OFFSETS: " << formatFloatList(model.header.chXOffsets) << '\n';
rad << "CH_Y_OFFSETS: " << formatFloatList(model.header.chYOffsets) << '\n';
if (!model.header.units.isEmpty()) rad << "UNITS: " << model.header.units << '\n';
rad << "START POSITION: " << QLocale::c().toString(model.header.startPosition, 'g', 12) << '\n';
const double stopPosition = model.header.stopPosition > model.header.startPosition
? model.header.stopPosition
: model.header.startPosition + model.tracesPerChannel * model.header.distanceInc;
rad << "STOP POSITION: " << QLocale::c().toString(stopPosition, 'g', 12) << '\n';
radFile.close();
return true;
}
QString IprhParser::formatFloatList(const QVector<float> &values)
{
QStringList parts;
parts.reserve(values.size());
for (float value : values) {
parts.append(QLocale::c().toString(value, 'g', 10));
}
return parts.join(' ');
}
QString IprhParser::uniqueConvertedBasePath(const QString &dirPath, const QString &baseName)
{
return QDir(dirPath).absoluteFilePath(baseName + QStringLiteral("_mala_converted"));
}
double IprhParser::extractDouble(const QString &value) {
bool ok = false;
double res = value.toDouble(&ok);
return ok ? res : 0.0;
}
int IprhParser::extractInt(const QString &value) {
bool ok = false;
int res = value.toInt(&ok);
return ok ? res : 0;
}