From a31323a266b3273cb9b69bcafb5149ef431e6031 Mon Sep 17 00:00:00 2001 From: Rainer Kuemmerle Date: Sat, 27 Jul 2024 13:33:19 +0200 Subject: [PATCH] Implement saving/loading different formats in g2o_viewer --- g2o/apps/g2o_viewer/main_window.cpp | 87 +++++++++++++++++++++++++---- g2o/apps/g2o_viewer/main_window.h | 7 ++- g2o/core/abstract_graph.cpp | 21 ++----- g2o/core/io/io_format.cpp | 35 ++++++++++++ g2o/core/io/io_format.h | 31 +++++++++- unit_test/general/graph_io.cpp | 18 ++++++ 6 files changed, 168 insertions(+), 31 deletions(-) diff --git a/g2o/apps/g2o_viewer/main_window.cpp b/g2o/apps/g2o_viewer/main_window.cpp index e003d3629..6f62c1c37 100644 --- a/g2o/apps/g2o_viewer/main_window.cpp +++ b/g2o/apps/g2o_viewer/main_window.cpp @@ -22,9 +22,11 @@ #include #include #include +#include #include #include #include +#include #include #include "g2o/core/estimate_propagator.h" @@ -40,10 +42,56 @@ #include "properties_widget.h" #include "viewer_properties_widget.h" +namespace { +/** + * @brief Join into a string using a delimeter + * + * @tparam Iterator + * @tparam std::iterator_traits::value_type + * @param b begin of the range for output + * @param e end of the range for output + * @param delimiter will be inserted in between elements + * @return std::string joined string + */ +template ::value_type> +std::string strJoin(Iterator b, Iterator e, const std::string& delimiter) { + std::ostringstream os; + if (b != e) { + std::copy(b, std::prev(e), + std::ostream_iterator(os, delimiter.c_str())); + b = std::prev(e); + } + if (b != e) { + os << *b; + } + return os.str(); +} + +QString prepareFilter(const std::vector& filters) { + std::vector filter_patterns; + filter_patterns.reserve(filters.size()); + std::transform(filters.begin(), filters.end(), + std::back_inserter(filter_patterns), + [](const g2o::io::FileFilter& f) { return f.filter; }); + return QString::fromStdString( + strJoin(filter_patterns.begin(), filter_patterns.end(), ";;")); +} + +g2o::io::Format extractFileFormat( + const std::vector& filters, + const std::string& file_pattern) { + for (const auto& f : filters) { + if (f.filter == file_pattern) return f.format; + } + return g2o::io::Format::kUndefined; +} +} // namespace + MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { setupUi(this); leKernelWidth->setValidator( - new QDoubleValidator(-std::numeric_limits::max(), + new QDoubleValidator(std::numeric_limits::lowest(), std::numeric_limits::max(), 7, this)); plainTextEdit->setMaximumBlockCount(1000); btnForceStop->hide(); @@ -52,19 +100,34 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { } void MainWindow::on_actionLoad_triggered(bool) { + const std::vector file_filters = + g2o::io::getFileFilter(true /*open*/); + QString selectedFilter; const QString filename = QFileDialog::getOpenFileName( - this, "Load g2o file", "", "g2o files (*.g2o);;All Files (*)"); + this, "Load g2o file", "", prepareFilter(file_filters), &selectedFilter); if (!filename.isNull()) { - loadFromFile(filename); + const g2o::io::Format file_format = + extractFileFormat(file_filters, selectedFilter.toStdString()); + G2O_DEBUG("Selected filter {} with format {}", selectedFilter.toStdString(), + g2o::io::to_string(file_format)); + loadFromFile(filename, file_format); } } void MainWindow::on_actionSave_triggered(bool) { + const std::vector file_filters = + g2o::io::getFileFilter(false /*open*/); + QString selectedFilter; + const QString filename = QFileDialog::getSaveFileName( - this, "Save g2o file", "", "g2o files (*.g2o)"); + this, "Save g2o file", "", prepareFilter(file_filters), &selectedFilter); if (!filename.isNull()) { + const g2o::io::Format file_format = + extractFileFormat(file_filters, selectedFilter.toStdString()); + G2O_DEBUG("Selected filter {} with format {}", selectedFilter.toStdString(), + g2o::io::to_string(file_format)); std::ofstream fout(filename.toStdString().c_str()); - viewer->graph->save(fout); + viewer->graph->save(fout, file_format); if (fout.good()) std::cerr << "Saved " << filename.toStdString() << '\n'; else @@ -217,18 +280,20 @@ void MainWindow::updateDisplayedSolvers() { } } -bool MainWindow::load(const QString& filename) { +bool MainWindow::load(const QString& filename, g2o::io::Format format) { viewer->graph->clear(); bool loadStatus = false; if (filename == "-") { std::cerr << "reading stdin\n"; - loadStatus = viewer->graph->load(std::cin); + loadStatus = viewer->graph->load(std::cin, format); } else { const std::string filename_as_std = filename.toStdString(); const std::string filename_extension = g2o::getFileExtension(filename_as_std); - std::optional file_format = - g2o::io::formatForFileExtension(filename_extension); + const std::optional file_format = + format != g2o::io::Format::kUndefined + ? format + : g2o::io::formatForFileExtension(filename_extension); std::ifstream ifs(filename_as_std); if (!ifs) return false; loadStatus = file_format.has_value() @@ -346,9 +411,9 @@ void MainWindow::setRobustKernel() { void MainWindow::on_btnForceStop_clicked() { forceStopFlag_ = true; } -bool MainWindow::loadFromFile(const QString& filename) { +bool MainWindow::loadFromFile(const QString& filename, g2o::io::Format format) { viewer->graph->clear(); - bool loadStatus = load(filename); + bool loadStatus = load(filename, format); if (loadStatus) { filename_ = filename.toStdString(); } diff --git a/g2o/apps/g2o_viewer/main_window.h b/g2o/apps/g2o_viewer/main_window.h index e16561ae6..18d28bf7a 100644 --- a/g2o/apps/g2o_viewer/main_window.h +++ b/g2o/apps/g2o_viewer/main_window.h @@ -21,6 +21,7 @@ #include +#include "g2o/core/io/io_format.h" #include "g2o/core/optimization_algorithm_property.h" #include "g2o_viewer_api.h" #include "ui_base_main_window.h" @@ -56,7 +57,8 @@ class G2O_VIEWER_API MainWindow : public QMainWindow, /** * load a graph on which we will operate from a file */ - bool loadFromFile(const QString& filename); + bool loadFromFile(const QString& filename, + g2o::io::Format format = g2o::io::Format::kUndefined); public slots: // NOLINT void on_actionLoad_triggered(bool); @@ -80,7 +82,8 @@ class G2O_VIEWER_API MainWindow : public QMainWindow, bool allocateSolver(bool& allocatedNewSolver); bool prepare(); void setRobustKernel(); - bool load(const QString& filename); + bool load(const QString& filename, + g2o::io::Format format = g2o::io::Format::kUndefined); std::vector knownSolvers_; int lastSolver_ = -1; diff --git a/g2o/core/abstract_graph.cpp b/g2o/core/abstract_graph.cpp index e78a95d2c..013eb09b3 100644 --- a/g2o/core/abstract_graph.cpp +++ b/g2o/core/abstract_graph.cpp @@ -37,22 +37,6 @@ #include "io/io_g2o.h" namespace { -#ifdef G2O_HAVE_LOGGING -std::string_view to_string(g2o::io::Format format) { - switch (format) { - case g2o::io::Format::kG2O: - return "G2O"; - case g2o::io::Format::kBinary: - return "Binary"; - case g2o::io::Format::kJson: - return "JSON"; - case g2o::io::Format::kXML: - return "XML"; - } - return ""; -} -#endif - /** * @brief Allocate an g2o::IoInterface to load/save data of the graph. * @@ -61,6 +45,9 @@ std::string_view to_string(g2o::io::Format format) { */ std::unique_ptr allocate(g2o::io::Format format) { switch (format) { + case g2o::io::Format::kUndefined: + G2O_WARN("Cannot allocate IO interface for undefined format"); + return nullptr; case g2o::io::Format::kG2O: return std::make_unique(); case g2o::io::Format::kBinary: @@ -71,7 +58,7 @@ std::unique_ptr allocate(g2o::io::Format format) { return std::make_unique(); } G2O_CRITICAL("Failed to create graph IO interface for format {}", - to_string(format)); + g2o::io::to_string(format)); return nullptr; } diff --git a/g2o/core/io/io_format.cpp b/g2o/core/io/io_format.cpp index d585b65a3..b0a5f8121 100644 --- a/g2o/core/io/io_format.cpp +++ b/g2o/core/io/io_format.cpp @@ -31,6 +31,22 @@ namespace g2o::io { +std::string_view to_string(g2o::io::Format format) { + switch (format) { + case g2o::io::Format::kUndefined: + return "Undefined"; + case g2o::io::Format::kG2O: + return "G2O"; + case g2o::io::Format::kBinary: + return "Binary"; + case g2o::io::Format::kJson: + return "JSON"; + case g2o::io::Format::kXML: + return "XML"; + } + return ""; +} + std::optional formatForFileExtension(std::string_view extension) { if (extension == "g2o" || extension == "G2O") return Format::kG2O; if (extension == "json" || extension == "JSON") return Format::kJson; @@ -39,4 +55,23 @@ std::optional formatForFileExtension(std::string_view extension) { return std::nullopt; } +FileFilter::FileFilter(std::string filter, Format format) + : filter(std::move(filter)), format(format) {} + +bool FileFilter::operator==(const FileFilter& other) const { + return filter == other.filter && format == other.format; +} + +std::vector getFileFilter(bool open) { + std::vector result; + result.emplace_back("g2o Ascii files (*.g2o)", Format::kG2O); + result.emplace_back("g2o Json files (*.json)", Format::kJson); + result.emplace_back("g2o XML files (*.xml)", Format::kXML); + result.emplace_back("g2o BIN files (*.bin)", Format::kBinary); + if (open) { + result.emplace_back("All Files (*)", Format::kUndefined); + } + return result; +} + } // namespace g2o::io diff --git a/g2o/core/io/io_format.h b/g2o/core/io/io_format.h index 3323bf706..fdefb6e6f 100644 --- a/g2o/core/io/io_format.h +++ b/g2o/core/io/io_format.h @@ -28,12 +28,33 @@ #define G2O_CORE_IO_FORMAT_H #include +#include #include +#include #include "g2o/core/g2o_core_api.h" namespace g2o::io { -enum class G2O_CORE_API Format { kG2O = 0, kBinary = 1, kJson = 2, kXML = 3 }; +enum class G2O_CORE_API Format { + kUndefined = -1, + kG2O = 0, + kBinary = 1, + kJson = 2, + kXML = 3 +}; + +G2O_CORE_API std::string_view to_string(g2o::io::Format format); + +/** + * @brief FileFilter information for Open/Save Dialog + * + */ +struct G2O_CORE_API FileFilter { + FileFilter(std::string filter, Format format); + std::string filter; ///< filter string + Format format; ///< IO format + bool operator==(const FileFilter& other) const; +}; /** * @brief Maps a file extension to a format value @@ -45,6 +66,14 @@ enum class G2O_CORE_API Format { kG2O = 0, kBinary = 1, kJson = 2, kXML = 3 }; G2O_CORE_API std::optional formatForFileExtension( std::string_view extension); +/** + * @brief Get the filters for file open dialogs. + * + * @param open true, if opening files, false for save + * @return std::vector Filters + */ +G2O_CORE_API std::vector getFileFilter(bool open); + } // namespace g2o::io #endif diff --git a/unit_test/general/graph_io.cpp b/unit_test/general/graph_io.cpp index 6917dd742..d68fb67d3 100644 --- a/unit_test/general/graph_io.cpp +++ b/unit_test/general/graph_io.cpp @@ -316,3 +316,21 @@ const auto kFileformatsToTest = Values( INSTANTIATE_TEST_SUITE_P(AbstractGraph, AbstractGraphIO, kFileformatsToTest); INSTANTIATE_TEST_SUITE_P(OptimizableGraphGraph, OptimizableGraphIO, kFileformatsToTest); + +TEST(OptimizableGraphIO, FileFilter) { + EXPECT_THAT(g2o::io::getFileFilter(false), Not(IsEmpty())); + EXPECT_THAT(g2o::io::getFileFilter(true), Not(IsEmpty())); + EXPECT_THAT(g2o::io::getFileFilter(false), + IsSubsetOf(g2o::io::getFileFilter(true))); +} + +TEST(OptimizableGraphIO, FormatToString) { + static constexpr g2o::io::Format kAllFormats[] = { + g2o::io::Format::kUndefined, g2o::io::Format::kG2O, + g2o::io::Format::kBinary, g2o::io::Format::kJson, g2o::io::Format::kXML}; + for (const auto& f : kAllFormats) + EXPECT_THAT(g2o::io::to_string(f), Not(IsEmpty())); + + EXPECT_THAT(g2o::io::to_string(static_cast(1 << 31)), + IsEmpty()); +}