From 9af4de2afc8d92b40cdd2091ead8b034d187257c 2017-12-18 13:52:49 From: perrinel Date: 2017-12-18 13:52:49 Subject: [PATCH] Merge pull request #392 from SciQLop-fork develop Develop --- diff --git a/app/src/Main.cpp b/app/src/Main.cpp index 9246498..bd42c5e 100644 --- a/app/src/Main.cpp +++ b/app/src/Main.cpp @@ -25,9 +25,9 @@ #include #include -#include #include #include +#include #include diff --git a/core/include/Catalogue/CatalogueController.h b/core/include/Catalogue/CatalogueController.h index 9d839af..717037d 100644 --- a/core/include/Catalogue/CatalogueController.h +++ b/core/include/Catalogue/CatalogueController.h @@ -32,27 +32,33 @@ public: virtual ~CatalogueController(); // DB - // QStringList getRepositories() const; + QStringList getRepositories() const; void addDB(const QString &dbPath); void saveDB(const QString &destinationPath, const QString &repository); // Event - // bool createEvent(const QString &name); + /// retrieveEvents with empty repository retrieve them from the default repository std::list > retrieveEvents(const QString &repository) const; std::list > retrieveAllEvents() const; std::list > retrieveEventsFromCatalogue(std::shared_ptr catalogue) const; - // void updateEvent(std::shared_ptr event); + void addEvent(std::shared_ptr event); + void updateEvent(std::shared_ptr event); + void removeEvent(std::shared_ptr event); // void trashEvent(std::shared_ptr event); - // void removeEvent(std::shared_ptr event); // void restore(QUuid eventId); - // void saveEvent(std::shared_ptr event); + void saveEvent(std::shared_ptr event); // Catalogue // bool createCatalogue(const QString &name, QVector eventList); - std::list > getCatalogues(const QString &repository) const; - // void removeEvent(QUuid catalogueId, const QString &repository); - // void saveCatalogue(std::shared_ptr event); + /// retrieveEvents with empty repository retrieve them from the default repository + std::list > retrieveCatalogues(const QString &repository + = QString()) const; + void updateCatalogue(std::shared_ptr catalogue); + void removeCatalogue(std::shared_ptr catalogue); + void saveCatalogue(std::shared_ptr catalogue); + + void saveAll(); public slots: /// Manage init/end of the controller diff --git a/core/src/Catalogue/CatalogueController.cpp b/core/src/Catalogue/CatalogueController.cpp index 9e2ad7c..be76fcf 100644 --- a/core/src/Catalogue/CatalogueController.cpp +++ b/core/src/Catalogue/CatalogueController.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -21,20 +22,28 @@ Q_LOGGING_CATEGORY(LOG_CatalogueController, "CatalogueController") namespace { -static QString REPOSITORY_WORK_SUFFIX = QString{"Work"}; - +static QString REPOSITORY_WORK_SUFFIX = QString{"work"}; +static QString REPOSITORY_TRASH_SUFFIX = QString{"trash"}; } class CatalogueController::CatalogueControllerPrivate { + public: + explicit CatalogueControllerPrivate(CatalogueController *parent) : m_Q{parent} {} + QMutex m_WorkingMutex; CatalogueDao m_CatalogueDao; - std::list m_RepositoryList; + QStringList m_RepositoryList; + CatalogueController *m_Q; + + void copyDBtoDB(const QString &dbFrom, const QString &dbTo); + QString toWorkRepository(QString repository); + QString toSyncRepository(QString repository); }; CatalogueController::CatalogueController(QObject *parent) - : impl{spimpl::make_unique_impl()} + : impl{spimpl::make_unique_impl(this)} { qCDebug(LOG_CatalogueController()) << tr("CatalogueController construction") << QThread::currentThread(); @@ -47,6 +56,11 @@ CatalogueController::~CatalogueController() this->waitForFinish(); } +QStringList CatalogueController::getRepositories() const +{ + return impl->m_RepositoryList; +} + void CatalogueController::addDB(const QString &dbPath) { QDir dbDir(dbPath); @@ -64,7 +78,8 @@ void CatalogueController::addDB(const QString &dbPath) << tr("Impossible to addDB %1 from %2 ").arg(dirName, dbPath); } else { - impl->m_RepositoryList.push_back(dirName); + impl->m_RepositoryList << dirName; + impl->copyDBtoDB(dirName, impl->toWorkRepository(dirName)); } } else { @@ -84,8 +99,10 @@ void CatalogueController::saveDB(const QString &destinationPath, const QString & std::list > CatalogueController::retrieveEvents(const QString &repository) const { + QString dbDireName = repository.isEmpty() ? REPOSITORY_DEFAULT : repository; + auto eventsShared = std::list >{}; - auto events = impl->m_CatalogueDao.getEvents(repository); + auto events = impl->m_CatalogueDao.getEvents(impl->toWorkRepository(dbDireName)); for (auto event : events) { eventsShared.push_back(std::make_shared(event)); } @@ -113,17 +130,92 @@ CatalogueController::retrieveEventsFromCatalogue(std::shared_ptr ca return eventsShared; } +void CatalogueController::updateEvent(std::shared_ptr event) +{ + event->setRepository(impl->toSyncRepository(event->getRepository())); + + impl->m_CatalogueDao.updateEvent(*event); +} + +void CatalogueController::removeEvent(std::shared_ptr event) +{ + // Remove it from both repository and repository_work + event->setRepository(impl->toWorkRepository(event->getRepository())); + impl->m_CatalogueDao.removeEvent(*event); + event->setRepository(impl->toSyncRepository(event->getRepository())); + impl->m_CatalogueDao.removeEvent(*event); +} + +void CatalogueController::addEvent(std::shared_ptr event) +{ + event->setRepository(impl->toWorkRepository(event->getRepository())); + + impl->m_CatalogueDao.addEvent(*event); + + // Call update is necessary at the creation of add Event if it has some tags or some event + // products + if (!event->getEventProducts().empty() || !event->getTags().empty()) { + impl->m_CatalogueDao.updateEvent(*event); + } +} + +void CatalogueController::saveEvent(std::shared_ptr event) +{ + impl->m_CatalogueDao.moveEvent(*event, impl->toSyncRepository(event->getRepository()), true); +} + std::list > -CatalogueController::getCatalogues(const QString &repository) const +CatalogueController::retrieveCatalogues(const QString &repository) const { + QString dbDireName = repository.isEmpty() ? REPOSITORY_DEFAULT : repository; + auto cataloguesShared = std::list >{}; - auto catalogues = impl->m_CatalogueDao.getCatalogues(repository); + auto catalogues = impl->m_CatalogueDao.getCatalogues(impl->toWorkRepository(dbDireName)); for (auto catalogue : catalogues) { cataloguesShared.push_back(std::make_shared(catalogue)); } return cataloguesShared; } +void CatalogueController::updateCatalogue(std::shared_ptr catalogue) +{ + catalogue->setRepository(impl->toSyncRepository(catalogue->getRepository())); + + impl->m_CatalogueDao.updateCatalogue(*catalogue); +} + +void CatalogueController::removeCatalogue(std::shared_ptr catalogue) +{ + // Remove it from both repository and repository_work + catalogue->setRepository(impl->toWorkRepository(catalogue->getRepository())); + impl->m_CatalogueDao.removeCatalogue(*catalogue); + catalogue->setRepository(impl->toSyncRepository(catalogue->getRepository())); + impl->m_CatalogueDao.removeCatalogue(*catalogue); +} + +void CatalogueController::saveCatalogue(std::shared_ptr catalogue) +{ + impl->m_CatalogueDao.moveCatalogue(*catalogue, + impl->toSyncRepository(catalogue->getRepository()), true); +} + +void CatalogueController::saveAll() +{ + for (auto repository : impl->m_RepositoryList) { + // Save Event + auto events = this->retrieveEvents(repository); + for (auto event : events) { + this->saveEvent(event); + } + + // Save Catalogue + auto catalogues = this->retrieveCatalogues(repository); + for (auto catalogue : catalogues) { + this->saveCatalogue(catalogue); + } + } +} + void CatalogueController::initialize() { qCDebug(LOG_CatalogueController()) << tr("CatalogueController init") @@ -137,8 +229,8 @@ void CatalogueController::initialize() if (defaultRepositoryLocationDir.mkpath(defaultRepositoryLocation)) { defaultRepositoryLocationDir.cd(defaultRepositoryLocation); auto defaultRepository = defaultRepositoryLocationDir.absoluteFilePath(REPOSITORY_DEFAULT); - qCInfo(LOG_CatalogueController()) - << tr("Persistant data loading from: ") << defaultRepository; + qCInfo(LOG_CatalogueController()) << tr("Persistant data loading from: ") + << defaultRepository; this->addDB(defaultRepository); } else { @@ -159,3 +251,46 @@ void CatalogueController::waitForFinish() { QMutexLocker locker{&impl->m_WorkingMutex}; } + +void CatalogueController::CatalogueControllerPrivate::copyDBtoDB(const QString &dbFrom, + const QString &dbTo) +{ + auto cataloguesShared = std::list >{}; + auto catalogues = m_CatalogueDao.getCatalogues(dbFrom); + for (auto catalogue : catalogues) { + cataloguesShared.push_back(std::make_shared(catalogue)); + } + + auto eventsShared = std::list >{}; + auto events = m_CatalogueDao.getEvents(dbFrom); + for (auto event : events) { + eventsShared.push_back(std::make_shared(event)); + } + + for (auto catalogue : cataloguesShared) { + m_CatalogueDao.copyCatalogue(*catalogue, dbTo, true); + } + + for (auto event : eventsShared) { + m_CatalogueDao.copyEvent(*event, dbTo, true); + } +} + +QString CatalogueController::CatalogueControllerPrivate::toWorkRepository(QString repository) +{ + auto syncRepository = toSyncRepository(repository); + + return QString("%1_%2").arg(syncRepository, REPOSITORY_WORK_SUFFIX); +} + +QString CatalogueController::CatalogueControllerPrivate::toSyncRepository(QString repository) +{ + auto syncRepository = repository; + if (repository.endsWith(REPOSITORY_WORK_SUFFIX)) { + syncRepository.remove(REPOSITORY_WORK_SUFFIX); + } + else if (repository.endsWith(REPOSITORY_TRASH_SUFFIX)) { + syncRepository.remove(REPOSITORY_TRASH_SUFFIX); + } + return syncRepository; +} diff --git a/core/src/Plugin/PluginManager.cpp b/core/src/Plugin/PluginManager.cpp index 2f07645..6579f63 100644 --- a/core/src/Plugin/PluginManager.cpp +++ b/core/src/Plugin/PluginManager.cpp @@ -97,8 +97,7 @@ struct PluginManager::PluginManagerPrivate { void loadStaticPlugins() { - for (QObject *plugin : QPluginLoader::staticInstances()) - { + for (QObject *plugin : QPluginLoader::staticInstances()) { qobject_cast(plugin)->initialize(); m_RegisteredPlugins.insert(plugin->metaObject()->className(), "StaticPlugin"); } diff --git a/core/tests/Data/TestDataSeriesUtils.cpp b/core/tests/Data/TestDataSeriesUtils.cpp index 1801022..2e5850f 100644 --- a/core/tests/Data/TestDataSeriesUtils.cpp +++ b/core/tests/Data/TestDataSeriesUtils.cpp @@ -176,8 +176,8 @@ void TestDataSeriesUtils::testThresholds_data() << false << 1. << 47.073; QTest::newRow("thresholds (empty data)") << std::vector{} << false << nan << nan; - QTest::newRow("thresholds (only nan values)") - << std::vector{nan, nan, nan, nan, nan} << false << nan << nan; + QTest::newRow("thresholds (only nan values)") << std::vector{nan, nan, nan, nan, nan} + << false << nan << nan; QTest::newRow("thresholds (from file with logarithmic scale)") << fromFile(inputFilePath("TestThresholds.txt")) << true << 832.005 << 17655064.730; diff --git a/gui/include/Catalogue/CatalogueActionManager.h b/gui/include/Catalogue/CatalogueActionManager.h new file mode 100644 index 0000000..5bd3347 --- /dev/null +++ b/gui/include/Catalogue/CatalogueActionManager.h @@ -0,0 +1,17 @@ +#ifndef SCIQLOP_CATALOGUEACTIONMANAGER_H +#define SCIQLOP_CATALOGUEACTIONMANAGER_H + +#include + +class CatalogueActionManager { +public: + CatalogueActionManager(); + + void installSelectionZoneActions(); + +private: + class CatalogueActionManagerPrivate; + spimpl::unique_impl_ptr impl; +}; + +#endif // SCIQLOP_CATALOGUEACTIONMANAGER_H diff --git a/gui/include/Catalogue/CatalogueEventsModel.h b/gui/include/Catalogue/CatalogueEventsModel.h index bc0bd70..e51ee47 100644 --- a/gui/include/Catalogue/CatalogueEventsModel.h +++ b/gui/include/Catalogue/CatalogueEventsModel.h @@ -3,17 +3,29 @@ #include #include +#include +#include class DBEvent; class DBEventProduct; +Q_DECLARE_LOGGING_CATEGORY(LOG_CatalogueEventsModel) + class CatalogueEventsModel : public QAbstractItemModel { + Q_OBJECT + +signals: + void modelSorted(); + public: CatalogueEventsModel(QObject *parent = nullptr); + enum class Column { Name, TStart, TEnd, Tags, Product, Validation, NbColumn }; + void setEvents(const QVector > &events); void addEvent(const std::shared_ptr &event); void removeEvent(const std::shared_ptr &event); + QVector > events() const; enum class ItemType { Root, Event, EventProduct }; ItemType itemTypeOf(const QModelIndex &index) const; @@ -21,8 +33,18 @@ public: std::shared_ptr getParentEvent(const QModelIndex &index) const; std::shared_ptr getEventProduct(const QModelIndex &index) const; + /// Refresh the data for the specified event void refreshEvent(const std::shared_ptr &event); + /// Returns a QModelIndex which represent the specified event + QModelIndex indexOf(const std::shared_ptr &event) const; + + /// Marks a change flag on the specified event to allow sorting on the validation column + void setEventHasChanges(const std::shared_ptr &event, bool hasChanges); + + /// Returns true if the specified event has unsaved changes + bool eventsHasChanges(const std::shared_ptr &event) const; + // Model QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; QModelIndex parent(const QModelIndex &index) const; diff --git a/gui/include/Catalogue/CatalogueEventsWidget.h b/gui/include/Catalogue/CatalogueEventsWidget.h index e7ce270..036b520 100644 --- a/gui/include/Catalogue/CatalogueEventsWidget.h +++ b/gui/include/Catalogue/CatalogueEventsWidget.h @@ -36,6 +36,7 @@ public: public slots: void populateWithCatalogues(const QVector > &catalogues); + void populateWithAllEvents(); private: Ui::CatalogueEventsWidget *ui; diff --git a/gui/include/Catalogue/CatalogueExplorerHelper.h b/gui/include/Catalogue/CatalogueExplorerHelper.h new file mode 100644 index 0000000..037901c --- /dev/null +++ b/gui/include/Catalogue/CatalogueExplorerHelper.h @@ -0,0 +1,13 @@ +#ifndef SCIQLOP_CATALOGUEEXPLORERHELPER_H +#define SCIQLOP_CATALOGUEEXPLORERHELPER_H + +#include + +#include + +struct CatalogueExplorerHelper { + static QWidget *buildValidationWidget(QWidget *parent, std::function save, + std::function discard); +}; + +#endif // SCIQLOP_CATALOGUEEXPLORERHELPER_H diff --git a/gui/include/Catalogue/CatalogueTreeWidgetItem.h b/gui/include/Catalogue/CatalogueTreeWidgetItem.h index a8b4f14..f9b0d65 100644 --- a/gui/include/Catalogue/CatalogueTreeWidgetItem.h +++ b/gui/include/Catalogue/CatalogueTreeWidgetItem.h @@ -22,6 +22,9 @@ public: /// changes void setHasChanges(bool value); + /// Returns true if the widget indicating the event has unsaved changes is displayed + bool hasChanges(); + /// Refreshes the data displayed by the item from the catalogue void refresh(); diff --git a/gui/include/Catalogue/CreateEventDialog.h b/gui/include/Catalogue/CreateEventDialog.h new file mode 100644 index 0000000..4cb4baa --- /dev/null +++ b/gui/include/Catalogue/CreateEventDialog.h @@ -0,0 +1,35 @@ +#ifndef SCIQLOP_CREATEEVENTDIALOG_H +#define SCIQLOP_CREATEEVENTDIALOG_H + +#include +#include +#include + +namespace Ui { +class CreateEventDialog; +} + +class DBCatalogue; + +class CreateEventDialog : public QDialog { + Q_OBJECT + +public: + explicit CreateEventDialog(QWidget *parent = 0); + virtual ~CreateEventDialog(); + + void hideCatalogueChoice(); + + QString eventName() const; + + std::shared_ptr selectedCatalogue() const; + QString catalogueName() const; + +private: + Ui::CreateEventDialog *ui; + + class CreateEventDialogPrivate; + spimpl::unique_impl_ptr impl; +}; + +#endif // SCIQLOP_CREATEEVENTDIALOG_H diff --git a/gui/meson.build b/gui/meson.build index 727fa0d..e37bc1c 100644 --- a/gui/meson.build +++ b/gui/meson.build @@ -1,4 +1,3 @@ - qxorm_dep = dependency('QxOrm', required : true, fallback:['QxOrm','qxorm_dep']) catalogueapi_dep = dependency('CatalogueAPI', required : true, fallback:['CatalogueAPI','CatalogueAPI_dep']) @@ -26,7 +25,9 @@ gui_moc_headers = [ 'include/Catalogue/CatalogueExplorer.h', 'include/Catalogue/CatalogueEventsWidget.h', 'include/Catalogue/CatalogueSideBarWidget.h', - 'include/Catalogue/CatalogueInspectorWidget.h' + 'include/Catalogue/CatalogueInspectorWidget.h', + 'include/Catalogue/CatalogueEventsModel.h', + 'include/Catalogue/CreateEventDialog.h' ] gui_ui_files = [ @@ -47,14 +48,24 @@ gui_ui_files = [ 'ui/Catalogue/CatalogueExplorer.ui', 'ui/Catalogue/CatalogueEventsWidget.ui', 'ui/Catalogue/CatalogueSideBarWidget.ui', - 'ui/Catalogue/CatalogueInspectorWidget.ui' + 'ui/Catalogue/CatalogueInspectorWidget.ui', + 'ui/Catalogue/CreateEventDialog.ui' ] gui_qresources = ['resources/sqpguiresources.qrc'] +rcc_gen = generator(rcc, + output : 'qrc_@BASENAME@.cpp', + arguments : [ + '--output', + '@OUTPUT@', + '@INPUT@', + '@EXTRA_ARGS@']) + +rcc_files = rcc_gen.process(gui_qresources, extra_args : ['-name', 'sqpguiresources']) + gui_moc_files = qt5.preprocess(moc_headers : gui_moc_headers, - ui_files : gui_ui_files, - qresources : gui_qresources) + ui_files : gui_ui_files) gui_sources = [ 'src/SqpApplication.cpp', @@ -108,7 +119,10 @@ gui_sources = [ 'src/Catalogue/CatalogueSideBarWidget.cpp', 'src/Catalogue/CatalogueInspectorWidget.cpp', 'src/Catalogue/CatalogueTreeWidgetItem.cpp', - 'src/Catalogue/CatalogueEventsModel.cpp' + 'src/Catalogue/CatalogueEventsModel.cpp', + 'src/Catalogue/CatalogueExplorerHelper.cpp', + 'src/Catalogue/CatalogueActionManager.cpp', + 'src/Catalogue/CreateEventDialog.cpp' ] gui_inc = include_directories(['include']) @@ -116,6 +130,7 @@ gui_inc = include_directories(['include']) sciqlop_gui_lib = library('sciqlopgui', gui_sources, gui_moc_files, + rcc_files, include_directories : [gui_inc], dependencies : [ qt5printsupport, qt5gui, qt5widgets, qt5svg, sciqlop_core, catalogueapi_dep], install : true @@ -125,3 +140,4 @@ sciqlop_gui = declare_dependency(link_with : sciqlop_gui_lib, include_directories : gui_inc, dependencies : [qt5printsupport, qt5gui, qt5widgets, qt5svg, sciqlop_core, catalogueapi_dep]) + diff --git a/gui/src/Catalogue/CatalogueActionManager.cpp b/gui/src/Catalogue/CatalogueActionManager.cpp new file mode 100644 index 0000000..258ac5d --- /dev/null +++ b/gui/src/Catalogue/CatalogueActionManager.cpp @@ -0,0 +1,107 @@ +#include "Catalogue/CatalogueActionManager.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +struct CatalogueActionManager::CatalogueActionManagerPrivate { + void createEventFromZones(const QString &eventName, + const QVector &zones, + const std::shared_ptr &catalogue = nullptr) + { + auto event = std::make_shared(); + event->setName(eventName); + + std::list productList; + for (auto zone : zones) { + auto graph = zone->parentGraphWidget(); + for (auto var : graph->variables()) { + auto eventProduct = std::make_shared(); + eventProduct->setEvent(*event); + + auto zoneRange = zone->range(); + eventProduct->setTStart(zoneRange.m_TStart); + eventProduct->setTEnd(zoneRange.m_TEnd); + + eventProduct->setProductId(var->metadata().value("id", "TODO").toString()); // todo + + productList.push_back(*eventProduct); + } + } + + event->setEventProducts(productList); + + sqpApp->catalogueController().addEvent(event); + + if (catalogue) { + // TODO + // catalogue->addEvent(event); + } + } +}; + +CatalogueActionManager::CatalogueActionManager() + : impl{spimpl::make_unique_impl()} +{ +} + +void CatalogueActionManager::installSelectionZoneActions() +{ + auto &actionController = sqpApp->actionsGuiController(); + + auto createEventEnableFuntion = [](auto zones) { + QSet usedGraphs; + for (auto zone : zones) { + auto graph = zone->parentGraphWidget(); + if (!usedGraphs.contains(graph)) { + usedGraphs.insert(graph); + } + else { + return false; + } + } + + return true; + }; + + auto createEventAction = actionController.addSectionZoneAction( + {QObject::tr("Catalogues")}, QObject::tr("New Event..."), [this](auto zones) { + CreateEventDialog dialog; + dialog.hideCatalogueChoice(); + if (dialog.exec() == QDialog::Accepted) { + impl->createEventFromZones(dialog.eventName(), zones); + } + }); + createEventAction->setEnableFunction(createEventEnableFuntion); + + auto createEventInCatalogueAction = actionController.addSectionZoneAction( + {QObject::tr("Catalogues")}, QObject::tr("New Event in Catalogue..."), [this](auto zones) { + CreateEventDialog dialog; + if (dialog.exec() == QDialog::Accepted) { + auto selectedCatalogue = dialog.selectedCatalogue(); + if (!selectedCatalogue) { + selectedCatalogue = std::make_shared(); + selectedCatalogue->setName(dialog.catalogueName()); + } + + impl->createEventFromZones(dialog.eventName(), zones, selectedCatalogue); + } + }); + createEventInCatalogueAction->setEnableFunction(createEventEnableFuntion); +} diff --git a/gui/src/Catalogue/CatalogueEventsModel.cpp b/gui/src/Catalogue/CatalogueEventsModel.cpp index ad78d41..de66396 100644 --- a/gui/src/Catalogue/CatalogueEventsModel.cpp +++ b/gui/src/Catalogue/CatalogueEventsModel.cpp @@ -15,38 +15,46 @@ #include #include +Q_LOGGING_CATEGORY(LOG_CatalogueEventsModel, "CatalogueEventsModel") + const auto EVENT_ITEM_TYPE = 1; const auto EVENT_PRODUCT_ITEM_TYPE = 2; struct CatalogueEventsModel::CatalogueEventsModelPrivate { QVector > m_Events; std::unordered_map > > m_EventProducts; + std::unordered_set > m_EventsWithChanges; - enum class Column { Name, TStart, TEnd, Tags, Product, NbColumn }; QStringList columnNames() { - return QStringList{tr("Event"), tr("TStart"), tr("TEnd"), tr("Tags"), tr("Product")}; + return QStringList{tr("Event"), tr("TStart"), tr("TEnd"), + tr("Tags"), tr("Product"), tr("")}; + } + + QVariant sortData(int col, const std::shared_ptr &event) const + { + if (col == (int)CatalogueEventsModel::Column::Validation) { + return m_EventsWithChanges.find(event) != m_EventsWithChanges.cend() ? true + : QVariant(); + } + + return eventData(col, event); } QVariant eventData(int col, const std::shared_ptr &event) const { switch (static_cast(col)) { - case Column::Name: + case CatalogueEventsModel::Column::Name: return event->getName(); - case Column::TStart: - return DateUtils::dateTime(event->getTStart()); - case Column::TEnd: - return DateUtils::dateTime(event->getTEnd()); - case Column::Product: { - auto eventProductsIt = m_EventProducts.find(event.get()); - if (eventProductsIt != m_EventProducts.cend()) { - return QString::number(m_EventProducts.at(event.get()).count()) + " product(s)"; - } - else { - return "0 product"; - } - } - case Column::Tags: { + case CatalogueEventsModel::Column::TStart: + return nbEventProducts(event) > 0 ? DateUtils::dateTime(event->getTStart()) + : QVariant{}; + case CatalogueEventsModel::Column::TEnd: + return nbEventProducts(event) > 0 ? DateUtils::dateTime(event->getTEnd()) + : QVariant{}; + case CatalogueEventsModel::Column::Product: + return QString::number(nbEventProducts(event)) + " product(s)"; + case CatalogueEventsModel::Column::Tags: { QString tagList; auto tags = event->getTags(); for (auto tag : tags) { @@ -56,6 +64,8 @@ struct CatalogueEventsModel::CatalogueEventsModelPrivate { return tagList; } + case CatalogueEventsModel::Column::Validation: + return QVariant(); default: break; } @@ -71,20 +81,32 @@ struct CatalogueEventsModel::CatalogueEventsModelPrivate { } } + int nbEventProducts(const std::shared_ptr &event) const + { + auto eventProductsIt = m_EventProducts.find(event.get()); + if (eventProductsIt != m_EventProducts.cend()) { + return m_EventProducts.at(event.get()).count(); + } + else { + return 0; + } + } + QVariant eventProductData(int col, const std::shared_ptr &eventProduct) const { switch (static_cast(col)) { - case Column::Name: + case CatalogueEventsModel::Column::Name: return eventProduct->getProductId(); - case Column::TStart: + case CatalogueEventsModel::Column::TStart: return DateUtils::dateTime(eventProduct->getTStart()); - case Column::TEnd: + case CatalogueEventsModel::Column::TEnd: return DateUtils::dateTime(eventProduct->getTEnd()); - case Column::Product: + case CatalogueEventsModel::Column::Product: return eventProduct->getProductId(); - case Column::Tags: { + case CatalogueEventsModel::Column::Tags: return QString(); - } + case CatalogueEventsModel::Column::Validation: + return QVariant(); default: break; } @@ -105,6 +127,7 @@ void CatalogueEventsModel::setEvents(const QVector > &e impl->m_Events = events; impl->m_EventProducts.clear(); + impl->m_EventsWithChanges.clear(); for (auto event : events) { impl->parseEventProduct(event); } @@ -159,21 +182,58 @@ void CatalogueEventsModel::removeEvent(const std::shared_ptr &event) beginRemoveRows(QModelIndex(), index, index); impl->m_Events.removeAt(index); impl->m_EventProducts.erase(event.get()); + impl->m_EventsWithChanges.erase(event); endRemoveRows(); } } +QVector > CatalogueEventsModel::events() const +{ + return impl->m_Events; +} + void CatalogueEventsModel::refreshEvent(const std::shared_ptr &event) { - auto i = impl->m_Events.indexOf(event); - if (i >= 0) { - auto eventIndex = index(i, 0); + auto eventIndex = indexOf(event); + if (eventIndex.isValid()) { + + // Refreshes the event line auto colCount = columnCount(); - emit dataChanged(eventIndex, index(i, colCount)); + emit dataChanged(eventIndex, index(eventIndex.row(), colCount)); + // Also refreshes its children event products auto childCount = rowCount(eventIndex); emit dataChanged(index(0, 0, eventIndex), index(childCount, colCount, eventIndex)); } + else { + qCWarning(LOG_CatalogueEventsModel()) << "refreshEvent: event not found."; + } +} + +QModelIndex CatalogueEventsModel::indexOf(const std::shared_ptr &event) const +{ + auto row = impl->m_Events.indexOf(event); + if (row >= 0) { + return index(row, 0); + } + + return QModelIndex(); +} + +void CatalogueEventsModel::setEventHasChanges(const std::shared_ptr &event, + bool hasChanges) +{ + if (hasChanges) { + impl->m_EventsWithChanges.insert(event); + } + else { + impl->m_EventsWithChanges.erase(event); + } +} + +bool CatalogueEventsModel::eventsHasChanges(const std::shared_ptr &event) const +{ + return impl->m_EventsWithChanges.find(event) != impl->m_EventsWithChanges.cend(); } QModelIndex CatalogueEventsModel::index(int row, int column, const QModelIndex &parent) const @@ -249,7 +309,7 @@ int CatalogueEventsModel::rowCount(const QModelIndex &parent) const int CatalogueEventsModel::columnCount(const QModelIndex &parent) const { - return static_cast(CatalogueEventsModelPrivate::Column::NbColumn); + return static_cast(CatalogueEventsModel::Column::NbColumn); } Qt::ItemFlags CatalogueEventsModel::flags(const QModelIndex &index) const @@ -296,8 +356,8 @@ void CatalogueEventsModel::sort(int column, Qt::SortOrder order) { std::sort(impl->m_Events.begin(), impl->m_Events.end(), [this, column, order](auto e1, auto e2) { - auto data1 = impl->eventData(column, e1); - auto data2 = impl->eventData(column, e2); + auto data1 = impl->sortData(column, e1); + auto data2 = impl->sortData(column, e2); auto result = data1.toString() < data2.toString(); @@ -305,6 +365,7 @@ void CatalogueEventsModel::sort(int column, Qt::SortOrder order) }); emit dataChanged(QModelIndex(), QModelIndex()); + emit modelSorted(); } Qt::DropActions CatalogueEventsModel::supportedDragActions() const diff --git a/gui/src/Catalogue/CatalogueEventsWidget.cpp b/gui/src/Catalogue/CatalogueEventsWidget.cpp index 410d933..de503c2 100644 --- a/gui/src/Catalogue/CatalogueEventsWidget.cpp +++ b/gui/src/Catalogue/CatalogueEventsWidget.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -16,8 +17,8 @@ Q_LOGGING_CATEGORY(LOG_CatalogueEventsWidget, "CatalogueEventsWidget") -/// Format of the dates appearing in the label of a cursor -const auto DATETIME_FORMAT = QStringLiteral("yyyy/MM/dd hh:mm:ss"); +/// Fixed size of the validation column +const auto VALIDATION_COLUMN_SIZE = 35; struct CatalogueEventsWidget::CatalogueEventsWidgetPrivate { @@ -277,8 +278,20 @@ CatalogueEventsWidget::CatalogueEventsWidget(QWidget *parent) }); ui->treeView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); - ui->treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->treeView->header()->setSectionResizeMode((int)CatalogueEventsModel::Column::Name, + QHeaderView::Stretch); + ui->treeView->header()->setSectionResizeMode((int)CatalogueEventsModel::Column::Validation, + QHeaderView::Fixed); + ui->treeView->header()->resizeSection((int)CatalogueEventsModel::Column::Validation, + VALIDATION_COLUMN_SIZE); ui->treeView->header()->setSortIndicatorShown(true); + + connect(impl->m_Model, &CatalogueEventsModel::modelSorted, [this]() { + auto allEvents = impl->m_Model->events(); + for (auto event : allEvents) { + setEventChanges(event, impl->m_Model->eventsHasChanges(event)); + } + }); } CatalogueEventsWidget::~CatalogueEventsWidget() @@ -294,6 +307,25 @@ void CatalogueEventsWidget::setVisualizationWidget(VisualizationWidget *visualiz void CatalogueEventsWidget::setEventChanges(const std::shared_ptr &event, bool hasChanges) { impl->m_Model->refreshEvent(event); + + auto eventIndex = impl->m_Model->indexOf(event); + auto validationIndex + = eventIndex.sibling(eventIndex.row(), (int)CatalogueEventsModel::Column::Validation); + + if (hasChanges) { + if (ui->treeView->indexWidget(validationIndex) == nullptr) { + auto widget = CatalogueExplorerHelper::buildValidationWidget( + ui->treeView, [this, event]() { setEventChanges(event, false); }, + [this, event]() { setEventChanges(event, false); }); + ui->treeView->setIndexWidget(validationIndex, widget); + } + } + else { + // Note: the widget is destroyed + ui->treeView->setIndexWidget(validationIndex, nullptr); + } + + impl->m_Model->setEventHasChanges(event, hasChanges); } void CatalogueEventsWidget::populateWithCatalogues( @@ -314,3 +346,15 @@ void CatalogueEventsWidget::populateWithCatalogues( impl->setEvents(events, ui->treeView); } + +void CatalogueEventsWidget::populateWithAllEvents() +{ + auto allEvents = sqpApp->catalogueController().retrieveAllEvents(); + + QVector > events; + for (auto event : allEvents) { + events << event; + } + + impl->setEvents(events, ui->treeView); +} diff --git a/gui/src/Catalogue/CatalogueExplorer.cpp b/gui/src/Catalogue/CatalogueExplorer.cpp index b0d99d7..e409089 100644 --- a/gui/src/Catalogue/CatalogueExplorer.cpp +++ b/gui/src/Catalogue/CatalogueExplorer.cpp @@ -1,12 +1,16 @@ #include "Catalogue/CatalogueExplorer.h" #include "ui_CatalogueExplorer.h" +#include +#include +#include #include #include #include struct CatalogueExplorer::CatalogueExplorerPrivate { + CatalogueActionManager m_ActionManager; }; CatalogueExplorer::CatalogueExplorer(QWidget *parent) @@ -16,6 +20,8 @@ CatalogueExplorer::CatalogueExplorer(QWidget *parent) { ui->setupUi(this); + impl->m_ActionManager.installSelectionZoneActions(); + connect(ui->catalogues, &CatalogueSideBarWidget::catalogueSelected, [this](auto catalogues) { if (catalogues.count() == 1) { ui->inspector->setCatalogue(catalogues.first()); @@ -34,8 +40,10 @@ CatalogueExplorer::CatalogueExplorer(QWidget *parent) connect(ui->catalogues, &CatalogueSideBarWidget::trashSelected, [this]() { ui->inspector->showPage(CatalogueInspectorWidget::Page::Empty); }); - connect(ui->catalogues, &CatalogueSideBarWidget::allEventsSelected, - [this]() { ui->inspector->showPage(CatalogueInspectorWidget::Page::Empty); }); + connect(ui->catalogues, &CatalogueSideBarWidget::allEventsSelected, [this]() { + ui->inspector->showPage(CatalogueInspectorWidget::Page::Empty); + ui->events->populateWithAllEvents(); + }); connect(ui->catalogues, &CatalogueSideBarWidget::selectionCleared, [this]() { ui->inspector->showPage(CatalogueInspectorWidget::Page::Empty); }); @@ -62,11 +70,15 @@ CatalogueExplorer::CatalogueExplorer(QWidget *parent) connect(ui->events, &CatalogueEventsWidget::selectionCleared, [this]() { ui->inspector->showPage(CatalogueInspectorWidget::Page::Empty); }); - connect(ui->inspector, &CatalogueInspectorWidget::catalogueUpdated, - [this](auto catalogue) { ui->catalogues->setCatalogueChanges(catalogue, true); }); + connect(ui->inspector, &CatalogueInspectorWidget::catalogueUpdated, [this](auto catalogue) { + sqpApp->catalogueController().updateCatalogue(catalogue); + ui->catalogues->setCatalogueChanges(catalogue, true); + }); - connect(ui->inspector, &CatalogueInspectorWidget::eventUpdated, - [this](auto event) { ui->events->setEventChanges(event, true); }); + connect(ui->inspector, &CatalogueInspectorWidget::eventUpdated, [this](auto event) { + sqpApp->catalogueController().updateEvent(event); + ui->events->setEventChanges(event, true); + }); connect(ui->inspector, &CatalogueInspectorWidget::eventProductUpdated, [this](auto event, auto eventProduct) { ui->events->setEventChanges(event, true); }); diff --git a/gui/src/Catalogue/CatalogueExplorerHelper.cpp b/gui/src/Catalogue/CatalogueExplorerHelper.cpp new file mode 100644 index 0000000..1fb7767 --- /dev/null +++ b/gui/src/Catalogue/CatalogueExplorerHelper.cpp @@ -0,0 +1,32 @@ +#include "Catalogue/CatalogueExplorerHelper.h" + +#include +#include + +const auto VALIDATION_BUTTON_ICON_SIZE = 12; + +QWidget *CatalogueExplorerHelper::buildValidationWidget(QWidget *parent, std::function save, + std::function discard) +{ + auto widget = new QWidget{parent}; + + auto layout = new QHBoxLayout{widget}; + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + auto btnValid = new QToolButton{widget}; + btnValid->setIcon(QIcon{":/icones/save"}); + btnValid->setIconSize(QSize{VALIDATION_BUTTON_ICON_SIZE, VALIDATION_BUTTON_ICON_SIZE}); + btnValid->setAutoRaise(true); + QObject::connect(btnValid, &QToolButton::clicked, save); + layout->addWidget(btnValid); + + auto btnDiscard = new QToolButton{widget}; + btnDiscard->setIcon(QIcon{":/icones/discard"}); + btnDiscard->setIconSize(QSize{VALIDATION_BUTTON_ICON_SIZE, VALIDATION_BUTTON_ICON_SIZE}); + btnDiscard->setAutoRaise(true); + QObject::connect(btnDiscard, &QToolButton::clicked, discard); + layout->addWidget(btnDiscard); + + return widget; +} diff --git a/gui/src/Catalogue/CatalogueSideBarWidget.cpp b/gui/src/Catalogue/CatalogueSideBarWidget.cpp index 97850e5..e9f5d4e 100644 --- a/gui/src/Catalogue/CatalogueSideBarWidget.cpp +++ b/gui/src/Catalogue/CatalogueSideBarWidget.cpp @@ -176,11 +176,14 @@ void CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate::configureTreeWidget( treeWidget->addTopLevelItem(separatorItem); treeWidget->setItemWidget(separatorItem, 0, separator); - auto db = addDatabaseItem("Default", treeWidget); + auto repositories = sqpApp->catalogueController().getRepositories(); + for (auto dbname : repositories) { + auto db = addDatabaseItem(dbname, treeWidget); - auto catalogues = sqpApp->catalogueController().getCatalogues("Default"); - for (auto catalogue : catalogues) { - addCatalogueItem(catalogue, db); + auto catalogues = sqpApp->catalogueController().retrieveCatalogues(dbname); + for (auto catalogue : catalogues) { + addCatalogueItem(catalogue, db); + } } treeWidget->expandAll(); diff --git a/gui/src/Catalogue/CatalogueTreeWidgetItem.cpp b/gui/src/Catalogue/CatalogueTreeWidgetItem.cpp index afa3cef..92de0ff 100644 --- a/gui/src/Catalogue/CatalogueTreeWidgetItem.cpp +++ b/gui/src/Catalogue/CatalogueTreeWidgetItem.cpp @@ -1,12 +1,12 @@ #include "Catalogue/CatalogueTreeWidgetItem.h" +#include + +#include +#include #include #include -#include -#include - -const auto VALIDATION_BUTTON_ICON_SIZE = 12; /// Column in the tree widget where the apply and cancel buttons must appear const auto APPLY_CANCEL_BUTTONS_COLUMN = 1; @@ -50,6 +50,7 @@ void CatalogueTreeWidgetItem::setData(int column, int role, const QVariant &valu if (newName != impl->m_Catalogue->getName()) { setText(0, newName); impl->m_Catalogue->setName(newName); + sqpApp->catalogueController().updateCatalogue(impl->m_Catalogue); setHasChanges(true); } } @@ -66,29 +67,11 @@ std::shared_ptr CatalogueTreeWidgetItem::catalogue() const void CatalogueTreeWidgetItem::setHasChanges(bool value) { if (value) { - if (treeWidget()->itemWidget(this, APPLY_CANCEL_BUTTONS_COLUMN) == nullptr) { - auto widet = new QWidget{treeWidget()}; - - auto layout = new QHBoxLayout{widet}; - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - - auto btnValid = new QToolButton{widet}; - btnValid->setIcon(QIcon{":/icones/save"}); - btnValid->setIconSize(QSize{VALIDATION_BUTTON_ICON_SIZE, VALIDATION_BUTTON_ICON_SIZE}); - btnValid->setAutoRaise(true); - QObject::connect(btnValid, &QToolButton::clicked, [this]() { setHasChanges(false); }); - layout->addWidget(btnValid); - - auto btnDiscard = new QToolButton{widet}; - btnDiscard->setIcon(QIcon{":/icones/discard"}); - btnDiscard->setIconSize( - QSize{VALIDATION_BUTTON_ICON_SIZE, VALIDATION_BUTTON_ICON_SIZE}); - btnDiscard->setAutoRaise(true); - QObject::connect(btnDiscard, &QToolButton::clicked, [this]() { setHasChanges(false); }); - layout->addWidget(btnDiscard); - - treeWidget()->setItemWidget(this, APPLY_CANCEL_BUTTONS_COLUMN, {widet}); + if (!hasChanges()) { + auto widget = CatalogueExplorerHelper::buildValidationWidget( + treeWidget(), [this]() { setHasChanges(false); }, + [this]() { setHasChanges(false); }); + treeWidget()->setItemWidget(this, APPLY_CANCEL_BUTTONS_COLUMN, widget); } } else { @@ -97,6 +80,11 @@ void CatalogueTreeWidgetItem::setHasChanges(bool value) } } +bool CatalogueTreeWidgetItem::hasChanges() +{ + return treeWidget()->itemWidget(this, APPLY_CANCEL_BUTTONS_COLUMN) != nullptr; +} + void CatalogueTreeWidgetItem::refresh() { emitDataChanged(); diff --git a/gui/src/Catalogue/CreateEventDialog.cpp b/gui/src/Catalogue/CreateEventDialog.cpp new file mode 100644 index 0000000..c3e90c9 --- /dev/null +++ b/gui/src/Catalogue/CreateEventDialog.cpp @@ -0,0 +1,59 @@ +#include "Catalogue/CreateEventDialog.h" +#include "ui_CreateEventDialog.h" + +#include +#include + +#include + +struct CreateEventDialog::CreateEventDialogPrivate { + QVector > m_DisplayedCatalogues; +}; + +CreateEventDialog::CreateEventDialog(QWidget *parent) + : QDialog(parent), + ui(new Ui::CreateEventDialog), + impl{spimpl::make_unique_impl()} +{ + ui->setupUi(this); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + auto catalogues = sqpApp->catalogueController().retrieveCatalogues(); + for (auto cat : catalogues) { + ui->cbCatalogue->addItem(cat->getName()); + impl->m_DisplayedCatalogues << cat; + } +} + +CreateEventDialog::~CreateEventDialog() +{ + delete ui; +} + +void CreateEventDialog::hideCatalogueChoice() +{ + ui->cbCatalogue->hide(); + ui->lblCatalogue->hide(); +} + +QString CreateEventDialog::eventName() const +{ + return ui->leEvent->text(); +} + +std::shared_ptr CreateEventDialog::selectedCatalogue() const +{ + auto catalogue = impl->m_DisplayedCatalogues.value(ui->cbCatalogue->currentIndex()); + if (!catalogue || catalogue->getName() != catalogueName()) { + return nullptr; + } + + return catalogue; +} + +QString CreateEventDialog::catalogueName() const +{ + return ui->cbCatalogue->currentText(); +} diff --git a/gui/src/Visualization/VisualizationActionManager.cpp b/gui/src/Visualization/VisualizationActionManager.cpp index cb84119..0de20a6 100644 --- a/gui/src/Visualization/VisualizationActionManager.cpp +++ b/gui/src/Visualization/VisualizationActionManager.cpp @@ -5,7 +5,9 @@ #include #include -VisualizationActionManager::VisualizationActionManager() {} +VisualizationActionManager::VisualizationActionManager() +{ +} void VisualizationActionManager::installSelectionZoneActions() { diff --git a/gui/src/Visualization/VisualizationGraphWidget.cpp b/gui/src/Visualization/VisualizationGraphWidget.cpp index 52484ae..b1e2048 100644 --- a/gui/src/Visualization/VisualizationGraphWidget.cpp +++ b/gui/src/Visualization/VisualizationGraphWidget.cpp @@ -241,10 +241,9 @@ VisualizationGraphWidget::VisualizationGraphWidget(const QString &name, QWidget connect(ui->widget, &QCustomPlot::mouseWheel, this, &VisualizationGraphWidget::onMouseWheel); connect(ui->widget, &QCustomPlot::mouseDoubleClick, this, &VisualizationGraphWidget::onMouseDoubleClick); - connect( - ui->widget->xAxis, - static_cast(&QCPAxis::rangeChanged), - this, &VisualizationGraphWidget::onRangeChanged, Qt::DirectConnection); + connect(ui->widget->xAxis, static_cast( + &QCPAxis::rangeChanged), + this, &VisualizationGraphWidget::onRangeChanged, Qt::DirectConnection); // Activates menu when right clicking on the graph ui->widget->setContextMenuPolicy(Qt::CustomContextMenu); @@ -695,9 +694,9 @@ void VisualizationGraphWidget::onGraphMenuRequested(const QPoint &pos) noexcept void VisualizationGraphWidget::onRangeChanged(const QCPRange &t1, const QCPRange &t2) { - qCDebug(LOG_VisualizationGraphWidget()) - << tr("TORM: VisualizationGraphWidget::onRangeChanged") - << QThread::currentThread()->objectName() << "DoAcqui" << impl->m_DoAcquisition; + qCDebug(LOG_VisualizationGraphWidget()) << tr("TORM: VisualizationGraphWidget::onRangeChanged") + << QThread::currentThread()->objectName() << "DoAcqui" + << impl->m_DoAcquisition; auto graphRange = SqpRange{t1.lower, t1.upper}; auto oldGraphRange = SqpRange{t2.lower, t2.upper}; diff --git a/gui/src/Visualization/VisualizationSelectionZoneItem.cpp b/gui/src/Visualization/VisualizationSelectionZoneItem.cpp index 4d06164..2e5c5b2 100644 --- a/gui/src/Visualization/VisualizationSelectionZoneItem.cpp +++ b/gui/src/Visualization/VisualizationSelectionZoneItem.cpp @@ -142,7 +142,9 @@ VisualizationSelectionZoneItem::VisualizationSelectionZoneItem(QCustomPlot *plot setColor(QColor(DEFAULT_COLOR)); } -VisualizationSelectionZoneItem::~VisualizationSelectionZoneItem() {} +VisualizationSelectionZoneItem::~VisualizationSelectionZoneItem() +{ +} VisualizationGraphWidget *VisualizationSelectionZoneItem::parentGraphWidget() const noexcept { diff --git a/gui/ui/Catalogue/CreateEventDialog.ui b/gui/ui/Catalogue/CreateEventDialog.ui new file mode 100644 index 0000000..b5352fe --- /dev/null +++ b/gui/ui/Catalogue/CreateEventDialog.ui @@ -0,0 +1,55 @@ + + + CreateEventDialog + + + + 0 + 0 + 324 + 93 + + + + New Event + + + + + + Event Name + + + + + + + + + + Catalogue + + + + + + + true + + + QComboBox::NoInsert + + + + + + + QDialogButtonBox::Ok + + + + + + + + diff --git a/plugins/amda/tests/TestAmdaResultParser.cpp b/plugins/amda/tests/TestAmdaResultParser.cpp index ca76006..78d8bb3 100644 --- a/plugins/amda/tests/TestAmdaResultParser.cpp +++ b/plugins/amda/tests/TestAmdaResultParser.cpp @@ -365,8 +365,8 @@ void TestAmdaResultParser::testReadSpectrogramTxt_data() {8946.475, 18133.158, 10875.621, 24051.619, 19283.221}, {20907.664, 32076.725, 13008.381, 13142.759, 23226.998}}); - QTest::newRow("Valid file (four bands)") - << QStringLiteral("spectro/ValidSpectrogram2.txt") << fourBandsResult; + QTest::newRow("Valid file (four bands)") << QStringLiteral("spectro/ValidSpectrogram2.txt") + << fourBandsResult; QTest::newRow("Valid file (four unsorted bands)") << QStringLiteral("spectro/ValidSpectrogram3.txt") << fourBandsResult; // Bands and values are sorted