diff --git a/include/petrack.h b/include/petrack.h index ba1932079de41dcce4439701a350e991b58720cc..8a6363cb0ce54e3f5df609af8282b11cca4e1eb6 100644 --- a/include/petrack.h +++ b/include/petrack.h @@ -26,6 +26,7 @@ #include <QMainWindow> #include <QMouseEvent> #include <opencv2/opencv.hpp> +#include <optional> #ifdef STEREO #include "calibStereoFilter.h" @@ -488,4 +489,10 @@ private: std::vector<std::string> mAuthors; }; +namespace util +{ +std::optional<QSet<int>> splitStringToInt(const QString &input); +} + + #endif diff --git a/src/petrack.cpp b/src/petrack.cpp index 23b6f68414550b5fa166577b9cfbfc5d4a722a90..fc65b5dacc0d84156735bb3f03adf5f7964b776d 100644 --- a/src/petrack.cpp +++ b/src/petrack.cpp @@ -3977,51 +3977,89 @@ QSet<int> Petrack::getPedestrianUserSelection() if(mControlWidget->trackShowOnly->checkState() == Qt::Checked) { QSet<int> onlyVisible; + // subtraction needed as in UI ID start at 1 and internally at 0 onlyVisible.insert(mControlWidget->trackShowOnlyNr->value() - 1); return onlyVisible; } if(mControlWidget->trackShowOnlyList->checkState() == Qt::Checked) { - QStringList list = mControlWidget->trackShowOnlyNrList->text().split(",", Qt::SkipEmptyParts); - QSet<int> onlyVisible; - foreach(QString s, list) + auto enteredIDs = util::splitStringToInt(mControlWidget->trackShowOnlyNrList->text()); + if(enteredIDs.has_value()) { - bool ok = false; - int nr = s.toInt(&ok); - if(ok /* && nr <= maxPed && nr > 0*/) // einzelne ID + QSet<int> selectedIDs; + for(auto id : enteredIDs.value()) { - onlyVisible.insert(nr - 1); + // subtraction needed as in UI ID start at 1 and internally at 0 + selectedIDs.insert(id - 1); } - else // error or IDs range (e.g. 1-3, 6-10, etc.) - { - QStringList range = s.split("-", Qt::SkipEmptyParts); - int last, first = range[0].toInt(&ok); + mControlWidget->trackShowOnlyNrList->setStyleSheet(""); + return selectedIDs; + } + else + { + mControlWidget->trackShowOnlyNrList->setStyleSheet("border: 1px solid red"); + } + } + return QSet<int>(); +} - if(ok /* && first <= maxPed && nr > 0*/) +/** + * @brief Splits the given text to get a set of integers. + * + * The given text will be split on ',' and then each element will be checked if it is a range. Ranges are marked with + * '-' as divider. Only positive integer values are allowed. + * + * Examples: + * '1,5,6' -> (1, 5, 6) + * '1-5' -> (1, 2, 3, 4, 5) + * + * @param input given text + * @return Set of int in the given text + */ +std::optional<QSet<int>> util::splitStringToInt(const QString &input) +{ + QSet<int> ids; + + for(const auto &id : input.split(",", Qt::SkipEmptyParts)) + { + bool ok = false; + int enteredID = id.toInt(&ok); + if(ok && enteredID >= 0) // parse single values + { + ids.insert(enteredID); + } + else // error or IDs range (e.g. 1-3, 6-10, etc.) + { + if(id.startsWith("-")) + { + ok = false; + } + auto range = id.split("-"); + int first = range[0].toInt(&ok); + ok = ok && range.size() == 2 && !range[1].isEmpty(); + if(ok) + { + int last = range[1].toInt(&ok); + if(ok) { - last = range[1].toInt(&ok); - if(ok /* && last <= maxPed && nr > 0*/) + if(first > last) { - if(first > last) - { - std::swap(first, last); - } + std::swap(first, last); + } - for(int i = first; i <= last; i++) - { - onlyVisible.insert(i - 1); - } + for(int i = first; i <= last; i++) + { + ids.insert(i); } } } - if(!ok) - { - debout << "Warning: error while reading showOnlyVisible list from input line!" << std::endl; - } } - return onlyVisible; // in anzeige wird ab 1 gezaehlt, in datenstruktur ab 0 + if(!ok) + { + return std::nullopt; + } } - return QSet<int>(); + return ids; } /** diff --git a/tests/unit_test/CMakeLists.txt b/tests/unit_test/CMakeLists.txt index 8b9088c44cb7bad704f5b1b06852408f90072d09..558dec9661301f10cfd5eb60c4cc716d515e6c3f 100644 --- a/tests/unit_test/CMakeLists.txt +++ b/tests/unit_test/CMakeLists.txt @@ -9,6 +9,5 @@ target_sources(petrack_tests PRIVATE tst_moCapController.cpp tst_recognition.cpp tst_codeMarkerWidget.cpp + tst_petrack.cpp ) - - diff --git a/tests/unit_test/tst_control.cpp b/tests/unit_test/tst_control.cpp index c370b38617bb156f964768b2f123b937b1ee472c..b48ab0a46d031ccb1d9b3dad7f2478f127e3c833 100644 --- a/tests/unit_test/tst_control.cpp +++ b/tests/unit_test/tst_control.cpp @@ -297,3 +297,95 @@ TEST_CASE("Loading from and saving to XML node", "[config]") } } } + +SCENARIO("Change the show only people list", "[ui][config][tracking][path]") +{ + Petrack pet{}; + QPointer<Control> control = pet.getControlWidget(); + control->trackShowOnlyList->setCheckState(Qt::Checked); + + GIVEN("No filter given") + { + REQUIRE(control->trackShowOnlyList->isChecked()); + REQUIRE(control->trackShowOnlyNrList->text().isEmpty()); + + WHEN("Enter valid filter (single values)") + { + QSet<int> enteredIDs{1, 4, 6, 7}; + QSet<int> expectedIDs; + for(auto id : enteredIDs) + { + expectedIDs.insert(id - 1); + } + + std::stringstream input; + std::copy(enteredIDs.begin(), enteredIDs.end(), std::ostream_iterator<int>(input, ",")); + + QTest::keyClicks(control->trackShowOnlyNrList, input.str().c_str(), Qt::NoModifier, 50); + auto receivedIDs = pet.getPedestrianUserSelection(); + + THEN("the entered ids should be returned") + { + REQUIRE(expectedIDs == receivedIDs); + REQUIRE(control->trackShowOnlyNrList->styleSheet().isEmpty()); + } + } + + WHEN("Enter valid filter (range)") + { + QSet<int> enteredIDs{1, 2, 3, 4}; + QSet<int> expectedIDs; + for(auto id : enteredIDs) + { + expectedIDs.insert(id - 1); + } + + QString input("1-4"); + + QSet<int> receivedIDs; + QTest::keyClicks(control->trackShowOnlyNrList, input, Qt::NoModifier, 50); + receivedIDs = pet.getPedestrianUserSelection(); + + THEN("the entered ids should be returned") + { + REQUIRE(expectedIDs == receivedIDs); + REQUIRE(control->trackShowOnlyNrList->styleSheet().isEmpty()); + } + } + + WHEN("Enter invalid filter") + { + QString input("1-"); + + THEN("the border should be red") + { + QTest::keyClicks(control->trackShowOnlyNrList, input, Qt::NoModifier, 50); + auto receivedIDs = pet.getPedestrianUserSelection(); + REQUIRE(receivedIDs.isEmpty()); + REQUIRE(control->trackShowOnlyNrList->styleSheet().contains("border: 1px solid red")); + } + } + + WHEN("Enter filter (element-wise)") + { + QString input("1, 3,4,10, 20-40, 50-45, 100"); + THEN("no exception should be thrown") + { + for(auto character : input.toStdString()) + { + QTest::keyClick(control->trackShowOnlyNrList, character, Qt::NoModifier, 10); + + QSet<int> receivedIDs = pet.getPedestrianUserSelection(); + if(receivedIDs.isEmpty()) + { + REQUIRE(control->trackShowOnlyNrList->styleSheet().contains("border: 1px solid red")); + } + else + { + REQUIRE(control->trackShowOnlyNrList->styleSheet().isEmpty()); + } + } + } + } + } +} diff --git a/tests/unit_test/tst_petrack.cpp b/tests/unit_test/tst_petrack.cpp new file mode 100644 index 0000000000000000000000000000000000000000..62dd57d4ffd441129d2ee962081be00b7f3f9144 --- /dev/null +++ b/tests/unit_test/tst_petrack.cpp @@ -0,0 +1,162 @@ +/* + * PeTrack - Software for tracking pedestrians movement in videos + * Copyright (C) 2010-2022 Forschungszentrum Jülich GmbH, + * Maik Boltes, Juliane Adrian, Ricardo Martin Brualla, Arne Graf, Paul Häger, Daniel Hillebrand, + * Deniz Kilic, Paul Lieberenz, Daniel Salden, Tobias Schrödter, Ann Katrin Seemann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "petrack.h" + +#include <catch2/catch.hpp> + +SCENARIO("Getting the IDs of the pedestrian from user input", "[petrack][util]") +{ + WHEN("Enter valid pedestrian ID filter") + { + AND_WHEN("single values") + { + QSet<int> expectedIDs{1, 4, 6, 7}; + std::stringstream input; + std::copy(expectedIDs.begin(), expectedIDs.end(), std::ostream_iterator<int>(input, ",")); + + auto receivedIDs = util::splitStringToInt(QString(input.str().c_str())); + + THEN("the entered ids should be returned") + { + REQUIRE(receivedIDs.has_value()); + REQUIRE(expectedIDs == receivedIDs.value()); + } + } + AND_WHEN("range") + { + QSet<int> expectedIDs{1, 2, 3, 4}; + QString input("1-4"); + + auto receivedIDs = util::splitStringToInt(input); + THEN("the entered ids should be returned") + { + REQUIRE(receivedIDs.has_value()); + REQUIRE(expectedIDs == receivedIDs.value()); + } + } + AND_WHEN("range (reverse order)") + { + QSet<int> expectedIDs{1, 2, 3, 4}; + QString input("4-1"); + + auto receivedIDs = util::splitStringToInt(input); + THEN("the entered ids should be returned") + { + REQUIRE(receivedIDs.has_value()); + REQUIRE(expectedIDs == receivedIDs.value()); + } + } + AND_WHEN("range + single values") + { + QSet<int> expectedIDs{1, 2, 3, 4, 5, 8, 10}; + QString input("1-5,8,10"); + + auto receivedIDs = util::splitStringToInt(input); + THEN("the entered ids should be returned") + { + REQUIRE(receivedIDs.has_value()); + REQUIRE(expectedIDs == receivedIDs.value()); + } + } + AND_WHEN("single values + range") + { + QSet<int> expectedIDs{1, 3, 5, 8, 9, 10, 11}; + QString input("1,3,5,8-11"); + + auto receivedIDs = util::splitStringToInt(input); + THEN("the entered ids should be returned") + { + REQUIRE(receivedIDs.has_value()); + REQUIRE(expectedIDs == receivedIDs.value()); + } + } + AND_WHEN("single values + range (with spaces") + { + QSet<int> expectedIDs{1, 3, 5, 8, 9, 10, 11}; + QString input("1, 3 ,5, 8 -11"); + + auto receivedIDs = util::splitStringToInt(input); + THEN("the entered ids should be returned") + { + REQUIRE(receivedIDs.has_value()); + REQUIRE(expectedIDs == receivedIDs.value()); + } + } + AND_WHEN("duplicate values") + { + QSet<int> expectedIDs{1, 2, 3, 4}; + QString input("1-4,1"); + + auto receivedIDs = util::splitStringToInt(input); + + THEN("the entered ids should be returned") + { + REQUIRE(receivedIDs.has_value()); + REQUIRE(expectedIDs == receivedIDs.value()); + } + } + AND_WHEN("range same start and end") + { + QSet<int> expectedIDs{1}; + QString input("1-1"); + + auto receivedIDs = util::splitStringToInt(input); + + THEN("the entered ids should be returned") + { + REQUIRE(receivedIDs.has_value()); + REQUIRE(expectedIDs == receivedIDs.value()); + } + } + } + + WHEN("Enter invalid pedestrian ID filter") + { + AND_WHEN("negative ID") + { + QString input("-1"); + auto receivedIDs = util::splitStringToInt(input); + + THEN("std::nullopt should be returned") { REQUIRE_FALSE(receivedIDs.has_value()); } + } + AND_WHEN("invalid range") + { + QString input("1-"); + auto receivedIDs = util::splitStringToInt(input); + + THEN("std::nullopt should be returned") { REQUIRE_FALSE(receivedIDs.has_value()); } + } + AND_WHEN("too many -'s") + { + QString input("1-2-"); + auto receivedIDs = util::splitStringToInt(input); + + THEN("std::nullopt should be returned") { REQUIRE_FALSE(receivedIDs.has_value()); } + } + AND_WHEN("not int values (single values)") + { + QString input("1, 5, a, b, 6"); + auto receivedIDs = util::splitStringToInt(input); + + THEN("std::nullopt should be returned") { REQUIRE_FALSE(receivedIDs.has_value()); } + } + } +} \ No newline at end of file