From aa5771ec396cbd15b00326cf58dd28914c37c780 2017-08-21 13:25:35 From: perrinel Date: 2017-08-21 13:25:35 Subject: [PATCH] Merge pull request #229 from SCIQLOP-Initialisation develop Develop --- diff --git a/core/include/Data/ArrayData.h b/core/include/Data/ArrayData.h index 18eb70c..5cd885b 100644 --- a/core/include/Data/ArrayData.h +++ b/core/include/Data/ArrayData.h @@ -218,6 +218,8 @@ public: } } + int componentCount() const noexcept { return m_Data.size(); } + /** * @return the data of a component * @param componentIndex the index of the component to retrieve the data diff --git a/core/include/Data/DataSeries.h b/core/include/Data/DataSeries.h index 0cba40a..f2fcd2a 100644 --- a/core/include/Data/DataSeries.h +++ b/core/include/Data/DataSeries.h @@ -208,11 +208,11 @@ public: auto lowerIt = std::lower_bound(begin, end, min, [](const auto &itValue, const auto &value) { - return itValue.x() == value; + return itValue.x() < value; }); auto upperIt = std::upper_bound(begin, end, max, [](const auto &value, const auto &itValue) { - return itValue.x() == value; + return value < itValue.x(); }); return std::make_pair(lowerIt, upperIt); diff --git a/core/tests/Data/TestDataSeries.cpp b/core/tests/Data/TestDataSeries.cpp index 4be88cc..a8f6525 100644 --- a/core/tests/Data/TestDataSeries.cpp +++ b/core/tests/Data/TestDataSeries.cpp @@ -22,6 +22,13 @@ private slots: /// Tests merge of two data series void testMerge(); + + /// Input test data + /// @sa testSubdata() + void testSubdata_data(); + + /// Tests get subdata of two data series + void testSubdata(); }; void TestDataSeries::testCtor_data() @@ -160,5 +167,66 @@ void TestDataSeries::testMerge() seriesValuesData.cbegin())); } +void TestDataSeries::testSubdata_data() +{ + // ////////////// // + // Test structure // + // ////////////// // + + // Data series to get subdata + QTest::addColumn >("dataSeries"); + + // Min/max values + QTest::addColumn("min"); + QTest::addColumn("max"); + + // Expected values after subdata + QTest::addColumn >("expectedXAxisData"); + QTest::addColumn >("expectedValuesData"); + + // ////////// // + // Test cases // + // ////////// // + + QTest::newRow("subData1") << createSeries({1., 2., 3., 4., 5.}, {100., 200., 300., 400., 500.}) + << -1. << 3.2 << QVector{1., 2., 3.} + << QVector{100., 200., 300.}; + QTest::newRow("subData2") << createSeries({1., 2., 3., 4., 5.}, {100., 200., 300., 400., 500.}) + << 1. << 4. << QVector{1., 2., 3., 4.} + << QVector{100., 200., 300., 400.}; + QTest::newRow("subData3") << createSeries({1., 2., 3., 4., 5.}, {100., 200., 300., 400., 500.}) + << 1. << 3.9 << QVector{1., 2., 3.} + << QVector{100., 200., 300.}; + QTest::newRow("subData4") << createSeries({1., 2., 3., 4., 5.}, {100., 200., 300., 400., 500.}) + << 0. << 0.9 << QVector{} << QVector{}; + QTest::newRow("subData5") << createSeries({1., 2., 3., 4., 5.}, {100., 200., 300., 400., 500.}) + << 0. << 1. << QVector{1.} << QVector{100.}; + QTest::newRow("subData6") << createSeries({1., 2., 3., 4., 5.}, {100., 200., 300., 400., 500.}) + << 2.1 << 6. << QVector{3., 4., 5.} + << QVector{300., 400., 500.}; + QTest::newRow("subData7") << createSeries({1., 2., 3., 4., 5.}, {100., 200., 300., 400., 500.}) + << 6. << 9. << QVector{} << QVector{}; + QTest::newRow("subData8") << createSeries({1., 2., 3., 4., 5.}, {100., 200., 300., 400., 500.}) + << 5. << 9. << QVector{5.} << QVector{500.}; +} + +void TestDataSeries::testSubdata() +{ + QFETCH(std::shared_ptr, dataSeries); + QFETCH(double, min); + QFETCH(double, max); + + QFETCH(QVector, expectedXAxisData); + QFETCH(QVector, expectedValuesData); + + auto bounds = dataSeries->subData(min, max); + QVERIFY(std::equal(bounds.first, bounds.second, expectedXAxisData.cbegin(), + expectedXAxisData.cend(), + [](const auto &it, const auto &expectedX) { return it.x() == expectedX; })); + QVERIFY(std::equal( + bounds.first, bounds.second, expectedValuesData.cbegin(), expectedValuesData.cend(), + [](const auto &it, const auto &expectedVal) { return it.value() == expectedVal; })); +} + QTEST_MAIN(TestDataSeries) #include "TestDataSeries.moc" diff --git a/gui/include/Common/ColorUtils.h b/gui/include/Common/ColorUtils.h new file mode 100644 index 0000000..2ef4f9e --- /dev/null +++ b/gui/include/Common/ColorUtils.h @@ -0,0 +1,19 @@ +#ifndef SCIQLOP_COLORUTILS_H +#define SCIQLOP_COLORUTILS_H + +#include + +class QColor; + +/** + * Utility class with methods for colors + */ +struct ColorUtils { + /// Generates a color scale from min / max values and a number of colors. + /// The algorithm uses the HSV color model to generate color variations (see + /// http://doc.qt.io/qt-4.8/qcolor.html#the-hsv-color-model) + static std::vector colors(const QColor &minColor, const QColor &maxColor, + int nbColors) noexcept; +}; + +#endif // SCIQLOP_COLORUTILS_H diff --git a/gui/include/Visualization/VisualizationDefs.h b/gui/include/Visualization/VisualizationDefs.h new file mode 100644 index 0000000..06c5945 --- /dev/null +++ b/gui/include/Visualization/VisualizationDefs.h @@ -0,0 +1,10 @@ +#ifndef SCIQLOP_VISUALIZATIONDEFS_H +#define SCIQLOP_VISUALIZATIONDEFS_H + +#include + +class QCPAbstractPlottable; + +using PlottablesMap = std::map; + +#endif // SCIQLOP_VISUALIZATIONDEFS_H diff --git a/gui/include/Visualization/VisualizationGraphHelper.h b/gui/include/Visualization/VisualizationGraphHelper.h index b721858..33eee26 100644 --- a/gui/include/Visualization/VisualizationGraphHelper.h +++ b/gui/include/Visualization/VisualizationGraphHelper.h @@ -1,6 +1,8 @@ #ifndef SCIQLOP_VISUALIZATIONGRAPHHELPER_H #define SCIQLOP_VISUALIZATIONGRAPHHELPER_H +#include "Visualization/VisualizationDefs.h" + #include #include @@ -28,13 +30,10 @@ struct VisualizationGraphHelper { * components. * @return the list of the components created */ - static QVector create(std::shared_ptr variable, - QCustomPlot &plot) noexcept; - static QVector createV2(std::shared_ptr variable, - QCustomPlot &plot) noexcept; + static PlottablesMap create(std::shared_ptr variable, QCustomPlot &plot) noexcept; - static void updateData(QVector plotableVect, - std::shared_ptr dataSeries, const SqpRange &dateTime); + static void updateData(PlottablesMap &plottables, std::shared_ptr dataSeries, + const SqpRange &dateTime); }; #endif // SCIQLOP_VISUALIZATIONGRAPHHELPER_H diff --git a/gui/src/Common/ColorUtils.cpp b/gui/src/Common/ColorUtils.cpp new file mode 100644 index 0000000..c4f6324 --- /dev/null +++ b/gui/src/Common/ColorUtils.cpp @@ -0,0 +1,29 @@ +#include "Common/ColorUtils.h" + +#include + +std::vector ColorUtils::colors(const QColor &minColor, const QColor &maxColor, + int nbColors) noexcept +{ + auto result = std::vector{}; + + if (nbColors == 1) { + result.push_back(minColor); + } + else if (nbColors > 0) { + const auto nbSteps = static_cast(nbColors - 1); + + const auto colorHStep = (maxColor.hue() - minColor.hue()) / nbSteps; + const auto colorSStep = (maxColor.saturation() - minColor.saturation()) / nbSteps; + const auto colorVStep = (maxColor.value() - minColor.value()) / nbSteps; + const auto colorAStep = (maxColor.alpha() - minColor.alpha()) / nbSteps; + + for (auto i = 0; i < nbColors; ++i) { + result.push_back(QColor::fromHsv( + minColor.hue() + i * colorHStep, minColor.saturation() + i * colorSStep, + minColor.value() + i * colorVStep, minColor.alpha() + i * colorAStep)); + } + } + + return result; +} diff --git a/gui/src/Visualization/VisualizationGraphHelper.cpp b/gui/src/Visualization/VisualizationGraphHelper.cpp index 80ae762..1339c16 100644 --- a/gui/src/Visualization/VisualizationGraphHelper.cpp +++ b/gui/src/Visualization/VisualizationGraphHelper.cpp @@ -1,7 +1,10 @@ #include "Visualization/VisualizationGraphHelper.h" #include "Visualization/qcustomplot.h" +#include + #include +#include #include @@ -35,161 +38,203 @@ QSharedPointer axisTicker(bool isTimeAxis) } } -void updateScalarData(QCPAbstractPlottable *component, std::shared_ptr scalarSeries, - const SqpRange &range) +/// Sets axes properties according to the properties of a data series +template +void setAxesProperties(const DataSeries &dataSeries, QCustomPlot &plot) noexcept { - qCDebug(LOG_VisualizationGraphHelper()) << "TORM: updateScalarData" - << QThread::currentThread()->objectName(); - if (auto qcpGraph = dynamic_cast(component)) { - scalarSeries->lockRead(); - { - auto sqpDataContainer = QSharedPointer::create(); - qcpGraph->setData(sqpDataContainer); - auto bounds = scalarSeries->subData(range.m_TStart, range.m_TEnd); - for (auto it = bounds.first; it != bounds.second; ++it) { - sqpDataContainer->appendGraphData(QCPGraphData(it->x(), it->value())); - } - - qCInfo(LOG_VisualizationGraphHelper()) << "TODEBUG: Current points displayed" - << sqpDataContainer->size(); - } - scalarSeries->unlock(); - + /// @todo : for the moment, no control is performed on the axes: the units and the tickers + /// are fixed for the default x-axis and y-axis of the plot, and according to the new graph + auto setAxisProperties = [](auto axis, const auto &unit) { + // label (unit name) + axis->setLabel(unit.m_Name); + + // ticker (depending on the type of unit) + axis->setTicker(axisTicker(unit.m_TimeUnit)); + }; + setAxisProperties(plot.xAxis, dataSeries.xAxisUnit()); + setAxisProperties(plot.yAxis, dataSeries.valuesUnit()); +} - // Display all data - component->parentPlot()->replot(); - } - else { - /// @todo DEBUG +/** + * Struct used to create plottables, depending on the type of the data series from which to create them + * @tparam T the data series' type + * @remarks Default implementation can't create plottables + */ +template +struct PlottablesCreator { + static PlottablesMap createPlottables(T &, QCustomPlot &) + { + qCCritical(LOG_DataSeries()) + << QObject::tr("Can't create plottables: unmanaged data series type"); + return {}; } -} +}; -QCPAbstractPlottable *createScalarSeriesComponentV2(std::shared_ptr scalarSeries, - QCustomPlot &plot) -{ - auto component = plot.addGraph(); +/** + * Specialization of PlottablesCreator for scalars and vectors + * @sa ScalarSeries + * @sa VectorSeries + */ +template +struct PlottablesCreator::value + or std::is_base_of::value> > { + static PlottablesMap createPlottables(T &dataSeries, QCustomPlot &plot) + { + PlottablesMap result{}; + + // Gets the number of components of the data series + auto componentCount = dataSeries.valuesData()->componentCount(); + + auto colors = ColorUtils::colors(Qt::blue, Qt::red, componentCount); + + // For each component of the data series, creates a QCPGraph to add to the plot + for (auto i = 0; i < componentCount; ++i) { + auto graph = plot.addGraph(); + graph->setPen(QPen{colors.at(i)}); + + result.insert({i, graph}); + } - if (component) { // Axes properties - /// @todo : for the moment, no control is performed on the axes: the units and the tickers - /// are fixed for the default x-axis and y-axis of the plot, and according to the new graph - - auto setAxisProperties = [](auto axis, const auto &unit) { - // label (unit name) - axis->setLabel(unit.m_Name); - - // ticker (depending on the type of unit) - axis->setTicker(axisTicker(unit.m_TimeUnit)); - }; - setAxisProperties(plot.xAxis, scalarSeries->xAxisUnit()); - setAxisProperties(plot.yAxis, scalarSeries->valuesUnit()); + setAxesProperties(dataSeries, plot); + + plot.replot(); + + return result; } - return component; -} +}; -QCPAbstractPlottable *createScalarSeriesComponent(std::shared_ptr scalarSeries, - QCustomPlot &plot, const SqpRange &dateTime) -{ - auto component = plot.addGraph(); +/** + * Struct used to update plottables, depending on the type of the data series from which to update them + * @tparam T the data series' type + * @remarks Default implementation can't update plottables + */ +template +struct PlottablesUpdater { + static void updatePlottables(T &, PlottablesMap &, const SqpRange &, bool) + { + qCCritical(LOG_DataSeries()) + << QObject::tr("Can't update plottables: unmanaged data series type"); + } +}; - if (component) { - // // Graph data - component->setData(scalarSeries->xAxisData()->data(), scalarSeries->valuesData()->data(), - true); +/** + * Specialization of PlottablesUpdater for scalars and vectors + * @sa ScalarSeries + * @sa VectorSeries + */ +template +struct PlottablesUpdater::value + or std::is_base_of::value> > { + static void updatePlottables(T &dataSeries, PlottablesMap &plottables, const SqpRange &range, + bool rescaleAxes) + { + dataSeries.lockRead(); + + // For each plottable to update, resets its data + std::map > dataContainers{}; + for (const auto &plottable : plottables) { + if (auto graph = dynamic_cast(plottable.second)) { + auto dataContainer = QSharedPointer::create(); + graph->setData(dataContainer); + + dataContainers.insert({plottable.first, dataContainer}); + } + } - updateScalarData(component, scalarSeries, dateTime); + // - Gets the data of the series included in the current range + // - Updates each plottable by adding, for each data item, a point that takes x-axis data and value data. The correct value is retrieved according to the index of the component + auto subDataIts = dataSeries.subData(range.m_TStart, range.m_TEnd); + for (auto it = subDataIts.first; it != subDataIts.second; ++it) { + for (const auto &dataContainer : dataContainers) { + auto componentIndex = dataContainer.first; + dataContainer.second->appendGraphData( + QCPGraphData(it->x(), it->value(componentIndex))); + } + } - // Axes properties - /// @todo : for the moment, no control is performed on the axes: the units and the tickers - /// are fixed for the default x-axis and y-axis of the plot, and according to the new graph + dataSeries.unlock(); - auto setAxisProperties = [](auto axis, const auto &unit) { - // label (unit name) - axis->setLabel(unit.m_Name); + if (!plottables.empty()) { + auto plot = plottables.begin()->second->parentPlot(); - // ticker (depending on the type of unit) - axis->setTicker(axisTicker(unit.m_TimeUnit)); - }; - setAxisProperties(plot.xAxis, scalarSeries->xAxisUnit()); - setAxisProperties(plot.yAxis, scalarSeries->valuesUnit()); + if (rescaleAxes) { + plot->rescaleAxes(); + } - // Display all data - component->rescaleAxes(); - plot.replot(); + plot->replot(); + } } - else { - qCDebug(LOG_VisualizationGraphHelper()) - << QObject::tr("Can't create graph for the scalar series"); +}; + +/** + * Helper used to create/update plottables + */ +struct IPlottablesHelper { + virtual ~IPlottablesHelper() noexcept = default; + virtual PlottablesMap create(QCustomPlot &plot) const = 0; + virtual void update(PlottablesMap &plottables, const SqpRange &range, + bool rescaleAxes = false) const = 0; +}; + +/** + * Default implementation of IPlottablesHelper, which takes data series to create/update plottables + * @tparam T the data series' type + */ +template +struct PlottablesHelper : public IPlottablesHelper { + explicit PlottablesHelper(T &dataSeries) : m_DataSeries{dataSeries} {} + + PlottablesMap create(QCustomPlot &plot) const override + { + return PlottablesCreator::createPlottables(m_DataSeries, plot); } - return component; -} + void update(PlottablesMap &plottables, const SqpRange &range, bool rescaleAxes) const override + { + PlottablesUpdater::updatePlottables(m_DataSeries, plottables, range, rescaleAxes); + } -} // namespace + T &m_DataSeries; +}; -QVector -VisualizationGraphHelper::createV2(std::shared_ptr variable, QCustomPlot &plot) noexcept +/// Creates IPlottablesHelper according to a data series +std::unique_ptr createHelper(std::shared_ptr dataSeries) noexcept { - auto result = QVector{}; - - if (variable) { - // Gets the data series of the variable to call the creation of the right components - // according to its type - if (auto scalarSeries = std::dynamic_pointer_cast(variable->dataSeries())) { - result.append(createScalarSeriesComponentV2(scalarSeries, plot)); - } - else { - qCDebug(LOG_VisualizationGraphHelper()) - << QObject::tr("Can't create graph plottables : unmanaged data series type"); - } + if (auto scalarSeries = std::dynamic_pointer_cast(dataSeries)) { + return std::make_unique >(*scalarSeries); + } + else if (auto vectorSeries = std::dynamic_pointer_cast(dataSeries)) { + return std::make_unique >(*vectorSeries); } else { - qCDebug(LOG_VisualizationGraphHelper()) - << QObject::tr("Can't create graph plottables : the variable is null"); + return std::make_unique >(*dataSeries); } - - return result; } -QVector VisualizationGraphHelper::create(std::shared_ptr variable, - QCustomPlot &plot) noexcept -{ - auto result = QVector{}; +} // namespace +PlottablesMap VisualizationGraphHelper::create(std::shared_ptr variable, + QCustomPlot &plot) noexcept +{ if (variable) { - // Gets the data series of the variable to call the creation of the right components - // according to its type - if (auto scalarSeries = std::dynamic_pointer_cast(variable->dataSeries())) { - result.append(createScalarSeriesComponent(scalarSeries, plot, variable->range())); - } - else { - qCDebug(LOG_VisualizationGraphHelper()) - << QObject::tr("Can't create graph plottables : unmanaged data series type"); - } + auto helper = createHelper(variable->dataSeries()); + auto plottables = helper->create(plot); + return plottables; } else { qCDebug(LOG_VisualizationGraphHelper()) << QObject::tr("Can't create graph plottables : the variable is null"); + return PlottablesMap{}; } - - return result; } -void VisualizationGraphHelper::updateData(QVector plotableVect, +void VisualizationGraphHelper::updateData(PlottablesMap &plottables, std::shared_ptr dataSeries, const SqpRange &dateTime) { - if (auto scalarSeries = std::dynamic_pointer_cast(dataSeries)) { - if (plotableVect.size() == 1) { - updateScalarData(plotableVect.at(0), scalarSeries, dateTime); - } - else { - qCCritical(LOG_VisualizationGraphHelper()) << QObject::tr( - "Can't update Data of a scalarSeries because there is not only one component " - "associated"); - } - } - else { - /// @todo DEBUG - } + auto helper = createHelper(dataSeries); + helper->update(plottables, dateTime); } diff --git a/gui/src/Visualization/VisualizationGraphWidget.cpp b/gui/src/Visualization/VisualizationGraphWidget.cpp index ad7b12a..2245196 100644 --- a/gui/src/Visualization/VisualizationGraphWidget.cpp +++ b/gui/src/Visualization/VisualizationGraphWidget.cpp @@ -1,5 +1,6 @@ #include "Visualization/VisualizationGraphWidget.h" #include "Visualization/IVisualizationWidgetVisitor.h" +#include "Visualization/VisualizationDefs.h" #include "Visualization/VisualizationGraphHelper.h" #include "Visualization/VisualizationGraphRenderingDelegate.h" #include "ui_VisualizationGraphWidget.h" @@ -33,7 +34,7 @@ struct VisualizationGraphWidget::VisualizationGraphWidgetPrivate { } // 1 variable -> n qcpplot - std::multimap, QCPAbstractPlottable *> m_VariableToPlotMultiMap; + std::map, PlottablesMap> m_VariableToPlotMultiMap; bool m_DoAcquisition; bool m_IsCalibration; QCPItemTracer *m_TextTracer; @@ -99,11 +100,8 @@ void VisualizationGraphWidget::enableAcquisition(bool enable) void VisualizationGraphWidget::addVariable(std::shared_ptr variable, SqpRange range) { // Uses delegate to create the qcpplot components according to the variable - auto createdPlottables = VisualizationGraphHelper::createV2(variable, *ui->widget); - - for (auto createdPlottable : qAsConst(createdPlottables)) { - impl->m_VariableToPlotMultiMap.insert({variable, createdPlottable}); - } + auto createdPlottables = VisualizationGraphHelper::create(variable, *ui->widget); + impl->m_VariableToPlotMultiMap.insert({variable, std::move(createdPlottables)}); connect(variable.get(), SIGNAL(updated()), this, SLOT(onDataCacheVariableUpdated())); @@ -124,10 +122,17 @@ void VisualizationGraphWidget::removeVariable(std::shared_ptr variable // Each component associated to the variable : // - is removed from qcpplot (which deletes it) // - is no longer referenced in the map - auto componentsIt = impl->m_VariableToPlotMultiMap.equal_range(variable); - for (auto it = componentsIt.first; it != componentsIt.second;) { - ui->widget->removePlottable(it->second); - it = impl->m_VariableToPlotMultiMap.erase(it); + auto variableIt = impl->m_VariableToPlotMultiMap.find(variable); + if (variableIt != impl->m_VariableToPlotMultiMap.cend()) { + auto &plottablesMap = variableIt->second; + + for (auto plottableIt = plottablesMap.cbegin(), plottableEnd = plottablesMap.cend(); + plottableIt != plottableEnd;) { + ui->widget->removePlottable(plottableIt->second); + plottableIt = plottablesMap.erase(plottableIt); + } + + impl->m_VariableToPlotMultiMap.erase(variableIt); } // Updates graph @@ -282,29 +287,18 @@ void VisualizationGraphWidget::onMouseRelease(QMouseEvent *event) noexcept void VisualizationGraphWidget::onDataCacheVariableUpdated() { - // NOTE: - // We don't want to call the method for each component of a variable unitarily, but for - // all - // its components at once (eg its three components in the case of a vector). - - // The unordered_multimap does not do this easily, so the question is whether to: - // - use an ordered_multimap and the algos of std to group the values by key - // - use a map (unique keys) and store as values directly the list of components - auto graphRange = ui->widget->xAxis->range(); auto dateTime = SqpRange{graphRange.lower, graphRange.upper}; - for (auto it = impl->m_VariableToPlotMultiMap.cbegin(); - it != impl->m_VariableToPlotMultiMap.cend(); ++it) { - auto variable = it->first; + for (auto &variableEntry : impl->m_VariableToPlotMultiMap) { + auto variable = variableEntry.first; qCDebug(LOG_VisualizationGraphWidget()) << "TORM: VisualizationGraphWidget::onDataCacheVariableUpdated S" << variable->range(); qCDebug(LOG_VisualizationGraphWidget()) << "TORM: VisualizationGraphWidget::onDataCacheVariableUpdated E" << dateTime; if (dateTime.contains(variable->range()) || dateTime.intersect(variable->range())) { - - VisualizationGraphHelper::updateData(QVector{} << it->second, - variable->dataSeries(), variable->range()); + VisualizationGraphHelper::updateData(variableEntry.second, variable->dataSeries(), + variable->range()); } } } @@ -312,9 +306,8 @@ void VisualizationGraphWidget::onDataCacheVariableUpdated() void VisualizationGraphWidget::onUpdateVarDisplaying(std::shared_ptr variable, const SqpRange &range) { - auto componentsIt = impl->m_VariableToPlotMultiMap.equal_range(variable); - for (auto it = componentsIt.first; it != componentsIt.second;) { - VisualizationGraphHelper::updateData(QVector{} << it->second, - variable->dataSeries(), range); + auto it = impl->m_VariableToPlotMultiMap.find(variable); + if (it != impl->m_VariableToPlotMultiMap.end()) { + VisualizationGraphHelper::updateData(it->second, variable->dataSeries(), range); } }