GrabBag/AppUtils/AppCommon/Src/BatchValidation.cpp
2026-10-30 14:07:40 +08:00

407 lines
14 KiB
C++
Raw 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 "BatchValidation.h"
#include "BatchSelectionDialog.h"
#include <QDir>
#include <QDirIterator>
#include <QFileDialog>
#include <QFileInfo>
#include <QMap>
#include <QMetaObject>
#include <QPointer>
#include <algorithm>
#include <exception>
#include <thread>
namespace {
// 文件名(含扩展名)包含关键字即排除,用于过滤复检生成的全部图片和 txt。
bool hasExcludedKeyword(const QString& filePath, const QStringList& keywords)
{
const QString fileName = QFileInfo(filePath).fileName();
for (const QString& rawKeyword : keywords) {
const QString keyword = rawKeyword.trimmed();
if (!keyword.isEmpty() && fileName.contains(keyword, Qt::CaseInsensitive)) {
return true;
}
}
return false;
}
// 自然排序:按数字段大小比较,得到 1, 2, 3, ..., 10, 11, ..., 101,
// 而不是字典序的 1, 10, 101, ..., 2, 20。
bool naturalLessThan(const QString& lhs, const QString& rhs)
{
int left = 0;
int right = 0;
const int leftSize = lhs.size();
const int rightSize = rhs.size();
while (left < leftSize && right < rightSize) {
const QChar leftChar = lhs.at(left);
const QChar rightChar = rhs.at(right);
if (leftChar.isDigit() && rightChar.isDigit()) {
int leftEnd = left;
while (leftEnd < leftSize && lhs.at(leftEnd).isDigit()) {
++leftEnd;
}
int rightEnd = right;
while (rightEnd < rightSize && rhs.at(rightEnd).isDigit()) {
++rightEnd;
}
// 跳过前导零,先比位数再逐位比较,避免整数溢出。
int leftDigit = left;
while (leftDigit < leftEnd - 1 && lhs.at(leftDigit) == QLatin1Char('0')) {
++leftDigit;
}
int rightDigit = right;
while (rightDigit < rightEnd - 1 && rhs.at(rightDigit) == QLatin1Char('0')) {
++rightDigit;
}
const int leftCount = leftEnd - leftDigit;
const int rightCount = rightEnd - rightDigit;
if (leftCount != rightCount) {
return leftCount < rightCount;
}
for (int index = 0; index < leftCount; ++index) {
const QChar leftValue = lhs.at(leftDigit + index);
const QChar rightValue = rhs.at(rightDigit + index);
if (leftValue != rightValue) {
return leftValue < rightValue;
}
}
left = leftEnd;
right = rightEnd;
continue;
}
const QChar leftFolded = leftChar.toCaseFolded();
const QChar rightFolded = rightChar.toCaseFolded();
if (leftFolded != rightFolded) {
return leftFolded < rightFolded;
}
++left;
++right;
}
return (leftSize - left) < (rightSize - right);
}
QString matchKey(const QString& filePath, const QStringList& aliases)
{
const QFileInfo info(filePath);
QString directory = QDir::cleanPath(info.absolutePath());
QString stem = info.completeBaseName();
QStringList orderedAliases;
for (const QString& alias : aliases) {
if (!alias.trimmed().isEmpty()) {
orderedAliases.append(alias.trimmed());
}
}
std::sort(orderedAliases.begin(), orderedAliases.end(), [](const QString& lhs, const QString& rhs) {
return lhs.size() > rhs.size();
});
for (const QString& alias : orderedAliases) {
if (stem.endsWith(alias, Qt::CaseInsensitive)) {
stem.chop(alias.size());
break;
}
}
#ifdef Q_OS_WIN
directory = directory.toCaseFolded();
stem = stem.toCaseFolded();
#endif
return directory + QLatin1Char('/') + stem;
}
int inputRank(const QString& inputPath, const QString& companionPath,
const QStringList& aliases)
{
const QString inputStem = QFileInfo(inputPath).completeBaseName();
const QString companionStem = QFileInfo(companionPath).completeBaseName();
if (inputStem.compare(companionStem, Qt::CaseInsensitive) == 0) {
return 0;
}
for (int index = 0; index < aliases.size(); ++index) {
const QString alias = aliases.at(index).trimmed();
if (!alias.isEmpty() && inputStem.endsWith(alias, Qt::CaseInsensitive)) {
return index + 1;
}
}
return aliases.size() + 1;
}
QStringList collectFiles(const QString& rootPath,
const QStringList& nameFilters,
bool recursive,
const QStringList& excludedKeywords)
{
QStringList files;
if (nameFilters.isEmpty()) {
return files;
}
const QDirIterator::IteratorFlags flags = recursive
? QDirIterator::Subdirectories
: QDirIterator::NoIteratorFlags;
QDirIterator iterator(rootPath, QDir::Files, flags);
while (iterator.hasNext()) {
const QString path = iterator.next();
const QFileInfo info(path);
bool matchesFilter = false;
for (const QString& filter : nameFilters) {
const QString trimmedFilter = filter.trimmed();
if (QDir::match(trimmedFilter, info.fileName())
|| (trimmedFilter.startsWith(QStringLiteral("*."))
&& info.suffix().compare(trimmedFilter.mid(2), Qt::CaseInsensitive) == 0)) {
matchesFilter = true;
break;
}
}
if (matchesFilter && !hasExcludedKeyword(path, excludedKeywords)) {
files.append(QFileInfo(path).absoluteFilePath());
}
}
std::sort(files.begin(), files.end(), naturalLessThan);
return files;
}
bool matchesAnyFilter(const QString& filePath, const QStringList& nameFilters)
{
if (nameFilters.isEmpty()) {
return true;
}
const QFileInfo info(filePath);
for (const QString& rawFilter : nameFilters) {
const QString filter = rawFilter.trimmed();
if (QDir::match(filter, info.fileName())
|| (filter.startsWith(QStringLiteral("*."))
&& info.suffix().compare(filter.mid(2), Qt::CaseInsensitive) == 0)) {
return true;
}
}
return false;
}
} // namespace
QString BatchValidation::SelectFileOrDirectory(QWidget* parent,
const QString& title,
const QString& nameFilter,
const QString& initialPath)
{
// 系统原生对话框一次只能在“文件”和“目录”中选择一种,
// 因此先在 .ui 对话框中确认方式,再打开对应的系统对话框。
BatchSelectionDialog chooser(title, parent);
if (chooser.exec() != QDialog::Accepted) {
return QString();
}
if (chooser.selectedMode() == BatchSelectionDialog::Mode::File) {
return QFileDialog::getOpenFileName(parent, title, initialPath, nameFilter);
}
const QFileInfo info(initialPath);
const QString startDirectory = info.isFile() ? info.absolutePath() : initialPath;
return QFileDialog::getExistingDirectory(parent, title, startDirectory);
}
QVector<BatchValidationItem> BatchValidation::Collect(
const QString& inputPath, const BatchValidationOptions& options)
{
const QFileInfo selectedInfo(inputPath);
if (!selectedInfo.exists()) {
return {};
}
if (selectedInfo.isFile()) {
if (!matchesAnyFilter(selectedInfo.fileName(), options.inputNameFilters)
|| hasExcludedKeyword(selectedInfo.fileName(), options.excludedKeywords)) {
return {};
}
return {{selectedInfo.absoluteFilePath(), QString()}};
}
if (!selectedInfo.isDir()) {
return {};
}
const QStringList inputs = collectFiles(selectedInfo.absoluteFilePath(),
options.inputNameFilters,
options.recursive,
options.excludedKeywords);
if (!options.requireCompanion) {
QVector<BatchValidationItem> items;
items.reserve(inputs.size());
for (const QString& path : inputs) {
items.append({path, QString()});
}
return items;
}
const QStringList companions = collectFiles(selectedInfo.absoluteFilePath(),
options.companionNameFilters,
options.recursive,
options.excludedKeywords);
QMap<QString, QStringList> companionsByKey;
for (const QString& path : companions) {
companionsByKey[matchKey(path, options.companionSuffixAliases)].append(path);
}
QMap<QString, QStringList> inputsByKey;
for (const QString& inputPathEntry : inputs) {
inputsByKey[matchKey(inputPathEntry, options.inputSuffixAliases)].append(inputPathEntry);
}
// QMap 默认按键的字典序迭代(1、10、101、2…),这里显式按自然顺序排序,
// 保证复检顺序为 1、2、3、…、10、11、…、101。
QStringList orderedKeys = inputsByKey.keys();
std::sort(orderedKeys.begin(), orderedKeys.end(), naturalLessThan);
QVector<BatchValidationItem> items;
for (const QString& key : orderedKeys) {
const auto companionIt = companionsByKey.constFind(key);
if (companionIt == companionsByKey.cend() || companionIt.value().size() != 1) {
continue;
}
QStringList candidates = inputsByKey.value(key);
std::sort(candidates.begin(), candidates.end(), [&](const QString& lhs, const QString& rhs) {
const int lhsRank = inputRank(lhs, companionIt.value().front(), options.inputSuffixAliases);
const int rhsRank = inputRank(rhs, companionIt.value().front(), options.inputSuffixAliases);
return lhsRank == rhsRank
? naturalLessThan(lhs, rhs)
: lhsRank < rhsRank;
});
const int count = options.allowMultipleInputsPerCompanion ? candidates.size() : 1;
for (int index = 0; index < count; ++index) {
items.append({candidates.at(index), companionIt.value().front()});
}
}
return items;
}
QString BatchValidation::ProgressPercent(int index, int total)
{
const double percent = total > 0
? 100.0 * static_cast<double>(index) / static_cast<double>(total)
: 0.0;
return QStringLiteral("%1%").arg(percent, 0, 'f', 2);
}
QVector<BatchValidationItem> BatchValidation::CollectTextData(const QString& inputPath)
{
BatchValidationOptions options;
options.inputNameFilters = QStringList() << QStringLiteral("*.txt");
options.requireCompanion = false;
return Collect(inputPath, options);
}
BatchValidationSummary BatchValidation::ProcessSequentially(
const QVector<BatchValidationItem>& items,
const Processor& processor,
const ProgressCallback& progress)
{
BatchValidationSummary summary;
if (!processor) {
return summary;
}
const int total = items.size();
for (int index = 0; index < total; ++index) {
const BatchValidationItem& item = items.at(index);
if (progress) {
progress(item, index + 1, total);
}
++summary.attempted;
int result = -1;
try {
result = processor(item);
} catch (const std::exception&) {
result = -1;
} catch (...) {
result = -1;
}
if (result == 0) {
++summary.succeeded;
} else {
++summary.failed;
}
}
return summary;
}
bool BatchValidation::StartTextDataValidation(
QObject* context,
QWidget* dialogParent,
const QString& title,
const Processor& processor,
const ProgressCallback& progress,
const CompletionCallback& completion,
QString* errorMessage,
const StartedCallback& started)
{
if (errorMessage) {
errorMessage->clear();
}
if (!context || !processor) {
if (errorMessage) {
*errorMessage = QObject::tr("复检处理器未初始化");
}
return false;
}
const QString inputPath = SelectFileOrDirectory(
dialogParent, title, QObject::tr("点云数据文件 (*.txt);;所有文件 (*.*)"));
if (inputPath.isEmpty()) {
return false;
}
const QVector<BatchValidationItem> items = CollectTextData(inputPath);
if (items.isEmpty()) {
if (errorMessage) {
*errorMessage = QObject::tr("所选文件或目录中没有找到可复检的 TXT 点云数据");
}
return false;
}
if (started) {
started(items.size());
}
QPointer<QObject> contextGuard(context);
const Processor processorCopy = processor;
const ProgressCallback progressCopy = progress;
const CompletionCallback completionCopy = completion;
std::thread([contextGuard, items, processorCopy, progressCopy, completionCopy]() {
const BatchValidationSummary summary = ProcessSequentially(
items, processorCopy,
[contextGuard, progressCopy](const BatchValidationItem& item, int index, int total) {
if (!contextGuard || !progressCopy) {
return;
}
QMetaObject::invokeMethod(contextGuard, [contextGuard, progressCopy, item, index, total]() {
if (contextGuard) {
progressCopy(item, index, total);
}
}, Qt::QueuedConnection);
});
if (contextGuard && completionCopy) {
QMetaObject::invokeMethod(contextGuard, [contextGuard, completionCopy, summary]() {
if (contextGuard) {
completionCopy(summary);
}
}, Qt::QueuedConnection);
}
}).detach();
return true;
}