GrabBag/AppUtils/AppCommon/Src/BatchValidation.cpp

407 lines
14 KiB
C++
Raw Normal View History

#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;
}