From e7ac423673d6385585cbef8b4cbead3654167790 2017-07-26 08:41:31 From: Alexandre Leroux Date: 2017-07-26 08:41:31 Subject: [PATCH] Merge branch 'feature/SortDataSeries' into develop --- diff --git a/core/include/Common/SortUtils.h b/core/include/Common/SortUtils.h new file mode 100644 index 0000000..111216f --- /dev/null +++ b/core/include/Common/SortUtils.h @@ -0,0 +1,38 @@ +#ifndef SCIQLOP_SORTUTILS_H +#define SCIQLOP_SORTUTILS_H + +#include +#include + +/** + * Utility class with methods for sorting data + */ +struct SortUtils { + /** + * Generates a vector representing the index of insertion of each data of a container if this + * one had to be sorted according to a comparison function. + * + * For example: + * If the container is a vector {1; 4; 2; 5; 3} and the comparison function is std::less, the + * result would be : {0; 3; 1; 4; 2} + * + * @tparam Container the type of the container. + * @tparam Compare the type of the comparison function + * @param container the container from which to generate the result. The container must have a + * at() method that returns a value associated to an index + * @param compare the comparison function + */ + template + static std::vector sortPermutation(const Container &container, const Compare &compare) + { + auto permutation = std::vector{}; + permutation.resize(container.size()); + + std::iota(permutation.begin(), permutation.end(), 0); + std::sort(permutation.begin(), permutation.end(), + [&](int i, int j) { return compare(container.at(i), container.at(j)); }); + return permutation; + } +}; + +#endif // SCIQLOP_SORTUTILS_H diff --git a/core/include/Data/ArrayData.h b/core/include/Data/ArrayData.h index dfa62ea..edab83a 100644 --- a/core/include/Data/ArrayData.h +++ b/core/include/Data/ArrayData.h @@ -4,6 +4,9 @@ #include #include #include + +#include + /** * @brief The ArrayData class represents a dataset for a data series. * @@ -47,6 +50,18 @@ public: } /** + * @return the data at a specified index + * @remarks index must be a valid position + * @remarks this method is only available for a unidimensional ArrayData + */ + template > + double at(int index) const noexcept + { + QReadLocker locker{&m_Lock}; + return m_Data[0].at(index); + } + + /** * Sets a data at a specified index. The index has to be valid to be effective * @param index the index to which the data will be set * @param data the data to set @@ -73,24 +88,44 @@ public: } /** - * @return the data as a vector + * @return the data as a vector, as a const reference * @remarks this method is only available for a unidimensional ArrayData */ template > - QVector data(double tStart, double tEnd) const noexcept + const QVector &cdata() const noexcept { QReadLocker locker{&m_Lock}; - return m_Data.at(tStart); + return m_Data.at(0); } - // TODO Comment + /** + * Merges into the array data an other array data + * @param other the array data to merge with + * @param prepend if true, the other array data is inserted at the beginning, otherwise it is + * inserted at the end + * @remarks this method is only available for a unidimensional ArrayData + */ template > - void merge(const ArrayData<1> &arrayData) + void add(const ArrayData<1> &other, bool prepend = false) { QWriteLocker locker{&m_Lock}; if (!m_Data.empty()) { - QReadLocker otherLocker{&arrayData.m_Lock}; - m_Data[0] += arrayData.data(); + QReadLocker otherLocker{&other.m_Lock}; + + if (prepend) { + const auto &otherData = other.data(); + const auto otherDataSize = otherData.size(); + + auto &data = m_Data[0]; + data.insert(data.begin(), otherDataSize, 0.); + + for (auto i = 0; i < otherDataSize; ++i) { + data.replace(i, otherData.at(i)); + } + } + else { + m_Data[0] += other.data(); + } } } @@ -101,10 +136,28 @@ public: return m_Data[0].size(); } + template > + std::shared_ptr > sort(const std::vector sortPermutation) + { + QReadLocker locker{&m_Lock}; + + const auto &data = m_Data.at(0); + + // Inits result + auto sortedData = QVector{}; + sortedData.resize(data.size()); + + std::transform(sortPermutation.cbegin(), sortPermutation.cend(), sortedData.begin(), + [&data](int i) { return data[i]; }); + + return std::make_shared >(std::move(sortedData)); + } + + template > void clear() { QWriteLocker locker{&m_Lock}; - m_Data.clear(); + m_Data[0].clear(); } diff --git a/core/include/Data/DataSeries.h b/core/include/Data/DataSeries.h index 936df33..f6cbde5 100644 --- a/core/include/Data/DataSeries.h +++ b/core/include/Data/DataSeries.h @@ -1,6 +1,8 @@ #ifndef SCIQLOP_DATASERIES_H #define SCIQLOP_DATASERIES_H +#include + #include #include @@ -17,7 +19,9 @@ Q_LOGGING_CATEGORY(LOG_DataSeries, "DataSeries") /** * @brief The DataSeries class is the base (abstract) implementation of IDataSeries. * - * It proposes to set a dimension for the values ​​data + * It proposes to set a dimension for the values ​​data. + * + * A DataSeries is always sorted on its x-axis data. * * @tparam Dim The dimension of the values data * @@ -45,18 +49,64 @@ public: m_ValuesData->clear(); } - /// @sa IDataSeries::merge() + /// Merges into the data series an other data series + /// @remarks the data series to merge with is cleared after the operation void merge(IDataSeries *dataSeries) override { - if (auto dimDataSeries = dynamic_cast *>(dataSeries)) { - m_XAxisData->merge(*dimDataSeries->xAxisData()); - m_ValuesData->merge(*dimDataSeries->valuesData()); - dimDataSeries->clear(); + dataSeries->lockWrite(); + lockWrite(); + + if (auto other = dynamic_cast *>(dataSeries)) { + const auto &otherXAxisData = other->xAxisData()->cdata(); + const auto &xAxisData = m_XAxisData->cdata(); + + // As data series are sorted, we can improve performances of merge, by call the sort + // method only if the two data series overlap. + if (!otherXAxisData.empty()) { + auto firstValue = otherXAxisData.front(); + auto lastValue = otherXAxisData.back(); + + auto xAxisDataBegin = xAxisData.cbegin(); + auto xAxisDataEnd = xAxisData.cend(); + + bool prepend; + bool sortNeeded; + + if (std::lower_bound(xAxisDataBegin, xAxisDataEnd, firstValue) == xAxisDataEnd) { + // Other data series if after data series + prepend = false; + sortNeeded = false; + } + else if (std::upper_bound(xAxisDataBegin, xAxisDataEnd, lastValue) + == xAxisDataBegin) { + // Other data series if before data series + prepend = true; + sortNeeded = false; + } + else { + // The two data series overlap + prepend = false; + sortNeeded = true; + } + + // Makes the merge + m_XAxisData->add(*other->xAxisData(), prepend); + m_ValuesData->add(*other->valuesData(), prepend); + + if (sortNeeded) { + sort(); + } + } + + // Clears the other data series + other->clear(); } else { qCWarning(LOG_DataSeries()) - << QObject::tr("Dection of a type of IDataSeries we cannot merge with !"); + << QObject::tr("Detection of a type of IDataSeries we cannot merge with !"); } + unlock(); + dataSeries->unlock(); } virtual void lockRead() { m_Lock.lockForRead(); } @@ -64,7 +114,9 @@ public: virtual void unlock() { m_Lock.unlock(); } protected: - /// Protected ctor (DataSeries is abstract) + /// Protected ctor (DataSeries is abstract). The vectors must have the same size, otherwise a + /// DataSeries with no values will be created. + /// @remarks data series is automatically sorted on its x-axis data explicit DataSeries(std::shared_ptr > xAxisData, const Unit &xAxisUnit, std::shared_ptr > valuesData, const Unit &valuesUnit) : m_XAxisData{xAxisData}, @@ -72,6 +124,15 @@ protected: m_ValuesData{valuesData}, m_ValuesUnit{valuesUnit} { + if (m_XAxisData->size() != m_ValuesData->size()) { + clear(); + } + + // Sorts data if it's not the case + const auto &xAxisCData = m_XAxisData->cdata(); + if (!std::is_sorted(xAxisCData.cbegin(), xAxisCData.cend())) { + sort(); + } } /// Copy ctor @@ -81,6 +142,8 @@ protected: m_ValuesData{std::make_shared >(*other.m_ValuesData)}, m_ValuesUnit{other.m_ValuesUnit} { + // Since a series is ordered from its construction and is always ordered, it is not + // necessary to call the sort method here ('other' is sorted) } /// Assignment operator @@ -96,6 +159,16 @@ protected: } private: + /** + * Sorts data series on its x-axis data + */ + void sort() noexcept + { + auto permutation = SortUtils::sortPermutation(*m_XAxisData, std::less()); + m_XAxisData = m_XAxisData->sort(permutation); + m_ValuesData = m_ValuesData->sort(permutation); + } + std::shared_ptr > m_XAxisData; Unit m_XAxisUnit; std::shared_ptr > m_ValuesData; diff --git a/core/include/Data/ScalarSeries.h b/core/include/Data/ScalarSeries.h index bbc2168..6be68df 100644 --- a/core/include/Data/ScalarSeries.h +++ b/core/include/Data/ScalarSeries.h @@ -9,14 +9,6 @@ class ScalarSeries : public DataSeries<1> { public: /** - * Ctor - * @param size the number of data the series will hold - * @param xAxisUnit x-axis unit - * @param valuesUnit values unit - */ - explicit ScalarSeries(int size, const Unit &xAxisUnit, const Unit &valuesUnit); - - /** * Ctor with two vectors. The vectors must have the same size, otherwise a ScalarSeries with no * values will be created. * @param xAxisData x-axis data @@ -25,14 +17,6 @@ public: explicit ScalarSeries(QVector xAxisData, QVector valuesData, const Unit &xAxisUnit, const Unit &valuesUnit); - /** - * Sets data for a specific index. The index has to be valid to be effective - * @param index the index to which the data will be set - * @param x the x-axis data - * @param value the value data - */ - void setData(int index, double x, double value) noexcept; - std::unique_ptr clone() const; }; diff --git a/core/src/Data/ScalarSeries.cpp b/core/src/Data/ScalarSeries.cpp index b38d8b0..8080c67 100644 --- a/core/src/Data/ScalarSeries.cpp +++ b/core/src/Data/ScalarSeries.cpp @@ -1,11 +1,5 @@ #include -ScalarSeries::ScalarSeries(int size, const Unit &xAxisUnit, const Unit &valuesUnit) - : DataSeries{std::make_shared >(size), xAxisUnit, - std::make_shared >(size), valuesUnit} -{ -} - ScalarSeries::ScalarSeries(QVector xAxisData, QVector valuesData, const Unit &xAxisUnit, const Unit &valuesUnit) : DataSeries{std::make_shared >(std::move(xAxisData)), xAxisUnit, @@ -13,12 +7,6 @@ ScalarSeries::ScalarSeries(QVector xAxisData, QVector valuesData { } -void ScalarSeries::setData(int index, double x, double value) noexcept -{ - xAxisData()->setData(index, x); - valuesData()->setData(index, value); -} - std::unique_ptr ScalarSeries::clone() const { return std::make_unique(*this); diff --git a/core/src/Variable/Variable.cpp b/core/src/Variable/Variable.cpp index 286ef52..c866661 100644 --- a/core/src/Variable/Variable.cpp +++ b/core/src/Variable/Variable.cpp @@ -55,11 +55,7 @@ void Variable::setDataSeries(std::shared_ptr dataSeries) noexcept impl->m_DataSeries = dataSeries->clone(); } else { - dataSeries->lockWrite(); - impl->m_DataSeries->lockWrite(); impl->m_DataSeries->merge(dataSeries.get()); - impl->m_DataSeries->unlock(); - dataSeries->unlock(); // emit updated(); } } diff --git a/core/tests/Data/TestDataSeries.cpp b/core/tests/Data/TestDataSeries.cpp new file mode 100644 index 0000000..4be88cc --- /dev/null +++ b/core/tests/Data/TestDataSeries.cpp @@ -0,0 +1,164 @@ +#include "Data/DataSeries.h" +#include "Data/ScalarSeries.h" + +#include +#include + +Q_DECLARE_METATYPE(std::shared_ptr) + +class TestDataSeries : public QObject { + Q_OBJECT +private slots: + /// Input test data + /// @sa testCtor() + void testCtor_data(); + + /// Tests construction of a data series + void testCtor(); + + /// Input test data + /// @sa testMerge() + void testMerge_data(); + + /// Tests merge of two data series + void testMerge(); +}; + +void TestDataSeries::testCtor_data() +{ + // ////////////// // + // Test structure // + // ////////////// // + + // x-axis data + QTest::addColumn >("xAxisData"); + // values data + QTest::addColumn >("valuesData"); + + // expected x-axis data + QTest::addColumn >("expectedXAxisData"); + // expected values data + QTest::addColumn >("expectedValuesData"); + + // ////////// // + // Test cases // + // ////////// // + + QTest::newRow("invalidData (different sizes of vectors)") + << QVector{1., 2., 3., 4., 5.} << QVector{100., 200., 300.} + << QVector{} << QVector{}; + + QTest::newRow("sortedData") << QVector{1., 2., 3., 4., 5.} + << QVector{100., 200., 300., 400., 500.} + << QVector{1., 2., 3., 4., 5.} + << QVector{100., 200., 300., 400., 500.}; + + QTest::newRow("unsortedData") << QVector{5., 4., 3., 2., 1.} + << QVector{100., 200., 300., 400., 500.} + << QVector{1., 2., 3., 4., 5.} + << QVector{500., 400., 300., 200., 100.}; + + QTest::newRow("unsortedData2") + << QVector{1., 4., 3., 5., 2.} << QVector{100., 200., 300., 400., 500.} + << QVector{1., 2., 3., 4., 5.} << QVector{100., 500., 300., 200., 400.}; +} + +void TestDataSeries::testCtor() +{ + // Creates series + QFETCH(QVector, xAxisData); + QFETCH(QVector, valuesData); + + auto series = std::make_shared(std::move(xAxisData), std::move(valuesData), + Unit{}, Unit{}); + + // Validates results : we check that the data series is sorted on its x-axis data + QFETCH(QVector, expectedXAxisData); + QFETCH(QVector, expectedValuesData); + + auto seriesXAxisData = series->xAxisData()->data(); + auto seriesValuesData = series->valuesData()->data(); + + QVERIFY( + std::equal(expectedXAxisData.cbegin(), expectedXAxisData.cend(), seriesXAxisData.cbegin())); + QVERIFY(std::equal(expectedValuesData.cbegin(), expectedValuesData.cend(), + seriesValuesData.cbegin())); +} + +namespace { + +std::shared_ptr createSeries(QVector xAxisData, QVector valuesData) +{ + return std::make_shared(std::move(xAxisData), std::move(valuesData), Unit{}, + Unit{}); +} + +} // namespace + +void TestDataSeries::testMerge_data() +{ + // ////////////// // + // Test structure // + // ////////////// // + + // Data series to merge + QTest::addColumn >("dataSeries"); + QTest::addColumn >("dataSeries2"); + + // Expected values in the first data series after merge + QTest::addColumn >("expectedXAxisData"); + QTest::addColumn >("expectedValuesData"); + + // ////////// // + // Test cases // + // ////////// // + + QTest::newRow("sortedMerge") + << createSeries({1., 2., 3., 4., 5.}, {100., 200., 300., 400., 500.}) + << createSeries({6., 7., 8., 9., 10.}, {600., 700., 800., 900., 1000.}) + << QVector{1., 2., 3., 4., 5., 6., 7., 8., 9., 10.} + << QVector{100., 200., 300., 400., 500., 600., 700., 800., 900., 1000.}; + + QTest::newRow("unsortedMerge") + << createSeries({6., 7., 8., 9., 10.}, {600., 700., 800., 900., 1000.}) + << createSeries({1., 2., 3., 4., 5.}, {100., 200., 300., 400., 500.}) + << QVector{1., 2., 3., 4., 5., 6., 7., 8., 9., 10.} + << QVector{100., 200., 300., 400., 500., 600., 700., 800., 900., 1000.}; + + QTest::newRow("unsortedMerge2") + << createSeries({1., 2., 8., 9., 10}, {100., 200., 300., 400., 500.}) + << createSeries({3., 4., 5., 6., 7.}, {600., 700., 800., 900., 1000.}) + << QVector{1., 2., 3., 4., 5., 6., 7., 8., 9., 10.} + << QVector{100., 200., 600., 700., 800., 900., 1000., 300., 400., 500.}; + + QTest::newRow("unsortedMerge3") + << createSeries({3., 5., 8., 7., 2}, {100., 200., 300., 400., 500.}) + << createSeries({6., 4., 9., 10., 1.}, {600., 700., 800., 900., 1000.}) + << QVector{1., 2., 3., 4., 5., 6., 7., 8., 9., 10.} + << QVector{1000., 500., 100., 700., 200., 600., 400., 300., 800., 900.}; +} + +void TestDataSeries::testMerge() +{ + // Merges series + QFETCH(std::shared_ptr, dataSeries); + QFETCH(std::shared_ptr, dataSeries2); + + dataSeries->merge(dataSeries2.get()); + + // Validates results : we check that the merge is valid and the data series is sorted on its + // x-axis data + QFETCH(QVector, expectedXAxisData); + QFETCH(QVector, expectedValuesData); + + auto seriesXAxisData = dataSeries->xAxisData()->data(); + auto seriesValuesData = dataSeries->valuesData()->data(); + + QVERIFY( + std::equal(expectedXAxisData.cbegin(), expectedXAxisData.cend(), seriesXAxisData.cbegin())); + QVERIFY(std::equal(expectedValuesData.cbegin(), expectedValuesData.cend(), + seriesValuesData.cbegin())); +} + +QTEST_MAIN(TestDataSeries) +#include "TestDataSeries.moc" diff --git a/gui/src/Visualization/VisualizationGraphHelper.cpp b/gui/src/Visualization/VisualizationGraphHelper.cpp index 66b9d7c..68725bf 100644 --- a/gui/src/Visualization/VisualizationGraphHelper.cpp +++ b/gui/src/Visualization/VisualizationGraphHelper.cpp @@ -11,7 +11,7 @@ namespace { class SqpDataContainer : public QCPGraphDataContainer { public: - void appendGraphDataUnsorted(const QCPGraphData &data) { mData.append(data); } + void appendGraphData(const QCPGraphData &data) { mData.append(data); } }; @@ -40,30 +40,32 @@ void updateScalarData(QCPAbstractPlottable *component, ScalarSeries &scalarSerie qCDebug(LOG_VisualizationGraphHelper()) << "TORM: updateScalarData" << QThread::currentThread()->objectName(); if (auto qcpGraph = dynamic_cast(component)) { - // Clean the graph - // NAIVE approch scalarSeries.lockRead(); { - const auto xData = scalarSeries.xAxisData()->data(); - const auto valuesData = scalarSeries.valuesData()->data(); - const auto count = xData.count(); - qCInfo(LOG_VisualizationGraphHelper()) << "TORM: Current points in cache" - << xData.count(); - - auto dataContainer = qcpGraph->data(); - dataContainer->clear(); + const auto &xData = scalarSeries.xAxisData()->cdata(); + const auto &valuesData = scalarSeries.valuesData()->cdata(); + + auto xDataBegin = xData.cbegin(); + auto xDataEnd = xData.cend(); + + qCInfo(LOG_VisualizationGraphHelper()) + << "TORM: Current points in cache" << xData.count(); + auto sqpDataContainer = QSharedPointer::create(); qcpGraph->setData(sqpDataContainer); - for (auto i = 0; i < count; ++i) { - const auto x = xData[i]; - if (x >= dateTime.m_TStart && x <= dateTime.m_TEnd) { - sqpDataContainer->appendGraphDataUnsorted(QCPGraphData(x, valuesData[i])); - } + auto lowerIt = std::lower_bound(xDataBegin, xDataEnd, dateTime.m_TStart); + auto upperIt = std::upper_bound(xDataBegin, xDataEnd, dateTime.m_TEnd); + auto distance = std::distance(xDataBegin, lowerIt); + + auto valuesDataIt = valuesData.cbegin() + distance; + for (auto xAxisDataIt = lowerIt; xAxisDataIt != upperIt; + ++xAxisDataIt, ++valuesDataIt) { + sqpDataContainer->appendGraphData(QCPGraphData(*xAxisDataIt, *valuesDataIt)); } - sqpDataContainer->sort(); - qCInfo(LOG_VisualizationGraphHelper()) << "TORM: Current points displayed" - << sqpDataContainer->size(); + + qCInfo(LOG_VisualizationGraphHelper()) + << "TORM: Current points displayed" << sqpDataContainer->size(); } scalarSeries.unlock(); diff --git a/plugins/mockplugin/src/CosinusProvider.cpp b/plugins/mockplugin/src/CosinusProvider.cpp index 82a8bdd..d49bd10 100644 --- a/plugins/mockplugin/src/CosinusProvider.cpp +++ b/plugins/mockplugin/src/CosinusProvider.cpp @@ -19,8 +19,8 @@ std::shared_ptr CosinusProvider::retrieveData(QUuid token, const Sq // Gets the timerange from the parameters double freq = 100.0; - double start = dateTime.m_TStart * freq; // 100 htz - double end = dateTime.m_TEnd * freq; // 100 htz + double start = std::ceil(dateTime.m_TStart * freq); // 100 htz + double end = std::floor(dateTime.m_TEnd * freq); // 100 htz // We assure that timerange is valid if (end < start) { @@ -28,17 +28,23 @@ std::shared_ptr CosinusProvider::retrieveData(QUuid token, const Sq } // Generates scalar series containing cosinus values (one value per second) - auto scalarSeries - = std::make_shared(end - start, Unit{QStringLiteral("t"), true}, Unit{}); + auto dataCount = end - start; + auto xAxisData = QVector{}; + xAxisData.resize(dataCount); + + auto valuesData = QVector{}; + valuesData.resize(dataCount); int progress = 0; - auto progressEnd = end - start; + auto progressEnd = dataCount; for (auto time = start; time < end; ++time, ++dataIndex) { auto it = m_VariableToEnableProvider.find(token); if (it != m_VariableToEnableProvider.end() && it.value()) { const auto timeOnFreq = time / freq; - scalarSeries->setData(dataIndex, timeOnFreq, std::cos(timeOnFreq)); + + xAxisData.replace(dataIndex, timeOnFreq); + valuesData.replace(dataIndex, std::cos(timeOnFreq)); // progression int currentProgress = (time - start) * 100.0 / progressEnd; @@ -58,8 +64,8 @@ std::shared_ptr CosinusProvider::retrieveData(QUuid token, const Sq } emit dataProvidedProgress(token, 0.0); - - return scalarSeries; + return std::make_shared(std::move(xAxisData), std::move(valuesData), + Unit{QStringLiteral("t"), true}, Unit{}); } void CosinusProvider::requestDataLoading(QUuid token, const DataProviderParameters ¶meters)