#include "BatchValidation.h" #include "BatchSelectionDialog.h" #include #include #include #include #include #include #include #include #include #include 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 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 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 companionsByKey; for (const QString& path : companions) { companionsByKey[matchKey(path, options.companionSuffixAliases)].append(path); } QMap 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 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(index) / static_cast(total) : 0.0; return QStringLiteral("%1%").arg(percent, 0, 'f', 2); } QVector BatchValidation::CollectTextData(const QString& inputPath) { BatchValidationOptions options; options.inputNameFilters = QStringList() << QStringLiteral("*.txt"); options.requireCompanion = false; return Collect(inputPath, options); } BatchValidationSummary BatchValidation::ProcessSequentially( const QVector& 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 items = CollectTextData(inputPath); if (items.isEmpty()) { if (errorMessage) { *errorMessage = QObject::tr("所选文件或目录中没有找到可复检的 TXT 点云数据"); } return false; } if (started) { started(items.size()); } QPointer 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; }