VisualizationGraphHelper.cpp
545 lines
| 18.7 KiB
| text/x-c
|
CppLexer
r243 | #include "Visualization/VisualizationGraphHelper.h" | |||
|
r181 | #include "Visualization/qcustomplot.h" | ||
r1420 | #include <Data/ScalarTimeSerie.h> | |||
#include <Data/SpectrogramTimeSerie.h> | ||||
r1465 | #include <Data/TimeSeriesUtils.h> | |||
r1420 | #include <Data/VectorTimeSerie.h> | |||
|
r182 | |||
r1465 | #include <Common/cpp_utils.h> | |||
r1420 | #include <Variable/Variable2.h> | |||
r1465 | #include <algorithm> | |||
|
r181 | |||
r243 | Q_LOGGING_CATEGORY(LOG_VisualizationGraphHelper, "VisualizationGraphHelper") | |||
|
r181 | |||
r1420 | namespace | |||
{ | ||||
|
r182 | |||
r1420 | class SqpDataContainer : public QCPGraphDataContainer | |||
{ | ||||
r364 | public: | |||
r1420 | void appendGraphData(const QCPGraphData& data) { mData.append(data); } | |||
r364 | }; | |||
|
r583 | /** | ||
r591 | * Struct used to create plottables, depending on the type of the data series from which to create | |||
* them | ||||
|
r583 | * @tparam T the data series' type | ||
* @remarks Default implementation can't create plottables | ||||
*/ | ||||
template <typename T, typename Enabled = void> | ||||
r1420 | struct PlottablesCreator | |||
{ | ||||
r1431 | static PlottablesMap createPlottables(QCustomPlot&, const std::shared_ptr<T>& dataSeries) | |||
{ | ||||
return {}; | ||||
} | ||||
|
r583 | }; | ||
r235 | ||||
r1420 | PlottablesMap createGraphs(QCustomPlot& plot, int nbGraphs) | |||
|
r1280 | { | ||
r1420 | PlottablesMap result {}; | |||
|
r1280 | |||
// Creates {nbGraphs} QCPGraph to add to the plot | ||||
r1420 | for (auto i = 0; i < nbGraphs; ++i) | |||
{ | ||||
|
r1280 | auto graph = plot.addGraph(); | ||
r1420 | result.insert({ i, graph }); | |||
|
r1280 | } | ||
plot.replot(); | ||||
return result; | ||||
} | ||||
|
r583 | /** | ||
|
r1280 | * Specialization of PlottablesCreator for scalars | ||
|
r583 | * @sa ScalarSeries | ||
*/ | ||||
template <typename T> | ||||
r1420 | struct PlottablesCreator<T, typename std::enable_if_t<std::is_base_of<ScalarTimeSerie, T>::value>> | |||
{ | ||||
r1431 | static PlottablesMap createPlottables(QCustomPlot& plot, const std::shared_ptr<T>& dataSeries) | |||
{ | ||||
return createGraphs(plot, 1); | ||||
} | ||||
|
r1280 | }; | ||
|
r583 | |||
|
r1280 | /** | ||
* Specialization of PlottablesCreator for vectors | ||||
* @sa VectorSeries | ||||
*/ | ||||
template <typename T> | ||||
r1420 | struct PlottablesCreator<T, typename std::enable_if_t<std::is_base_of<VectorTimeSerie, T>::value>> | |||
{ | ||||
r1431 | static PlottablesMap createPlottables(QCustomPlot& plot, const std::shared_ptr<T>& dataSeries) | |||
{ | ||||
return createGraphs(plot, 3); | ||||
} | ||||
}; | ||||
/** | ||||
* Specialization of PlottablesCreator for MultiComponentTimeSeries | ||||
* @sa VectorSeries | ||||
*/ | ||||
template <typename T> | ||||
struct PlottablesCreator<T, | ||||
typename std::enable_if_t<std::is_base_of<MultiComponentTimeSerie, T>::value>> | ||||
{ | ||||
static PlottablesMap createPlottables(QCustomPlot& plot, const std::shared_ptr<T>& dataSeries) | ||||
{ | ||||
return createGraphs(plot, dataSeries->size(1)); | ||||
} | ||||
|
r583 | }; | ||
r548 | ||||
|
r902 | /** | ||
* Specialization of PlottablesCreator for spectrograms | ||||
* @sa SpectrogramSeries | ||||
*/ | ||||
template <typename T> | ||||
struct PlottablesCreator<T, | ||||
r1420 | typename std::enable_if_t<std::is_base_of<SpectrogramTimeSerie, T>::value>> | |||
{ | ||||
r1431 | static PlottablesMap createPlottables(QCustomPlot& plot, const std::shared_ptr<T>& dataSeries) | |||
|
r902 | { | ||
r1420 | PlottablesMap result {}; | |||
result.insert({ 0, new QCPColorMap { plot.xAxis, plot.yAxis } }); | ||||
|
r902 | |||
plot.replot(); | ||||
return result; | ||||
} | ||||
}; | ||||
|
r583 | /** | ||
r591 | * Struct used to update plottables, depending on the type of the data series from which to update | |||
* them | ||||
|
r583 | * @tparam T the data series' type | ||
* @remarks Default implementation can't update plottables | ||||
*/ | ||||
template <typename T, typename Enabled = void> | ||||
r1420 | struct PlottablesUpdater | |||
{ | ||||
static void setPlotYAxisRange(T&, const DateTimeRange&, QCustomPlot&) | ||||
|
r900 | { | ||
|
r905 | qCCritical(LOG_VisualizationGraphHelper()) | ||
|
r900 | << QObject::tr("Can't set plot y-axis range: unmanaged data series type"); | ||
} | ||||
r1420 | static void updatePlottables(T&, PlottablesMap&, const DateTimeRange&, bool) | |||
|
r583 | { | ||
|
r905 | qCCritical(LOG_VisualizationGraphHelper()) | ||
|
r583 | << QObject::tr("Can't update plottables: unmanaged data series type"); | ||
} | ||||
}; | ||||
|
r182 | |||
|
r583 | /** | ||
* Specialization of PlottablesUpdater for scalars and vectors | ||||
* @sa ScalarSeries | ||||
* @sa VectorSeries | ||||
*/ | ||||
template <typename T> | ||||
r1420 | struct PlottablesUpdater<T, typename std::enable_if_t<std::is_base_of<ScalarTimeSerie, T>::value>> | |||
{ | ||||
static void setPlotYAxisRange(T& dataSeries, const DateTimeRange& xAxisRange, QCustomPlot& plot) | ||||
|
r900 | { | ||
auto minValue = 0., maxValue = 0.; | ||||
r1420 | if (auto serie = dynamic_cast<ScalarTimeSerie*>(&dataSeries)) | |||
{ | ||||
r1423 | if (serie->size()) | |||
{ | ||||
maxValue = (*std::max_element(std::begin(*serie), std::end(*serie))).v(); | ||||
minValue = (*std::min_element(std::begin(*serie), std::end(*serie))).v(); | ||||
} | ||||
|
r900 | } | ||
r1420 | plot.yAxis->setRange(QCPRange { minValue, maxValue }); | |||
|
r900 | } | ||
r1420 | static void updatePlottables( | |||
T& dataSeries, PlottablesMap& plottables, const DateTimeRange& range, bool rescaleAxes) | ||||
|
r583 | { | ||
// For each plottable to update, resets its data | ||||
r1420 | for (const auto& plottable : plottables) | |||
{ | ||||
if (auto graph = dynamic_cast<QCPGraph*>(plottable.second)) | ||||
{ | ||||
|
r583 | auto dataContainer = QSharedPointer<SqpDataContainer>::create(); | ||
r1420 | if (auto serie = dynamic_cast<ScalarTimeSerie*>(&dataSeries)) | |||
{ | ||||
std::for_each( | ||||
std::begin(*serie), std::end(*serie), [&dataContainer](const auto& value) { | ||||
dataContainer->appendGraphData(QCPGraphData(value.t(), value.v())); | ||||
}); | ||||
} | ||||
|
r583 | graph->setData(dataContainer); | ||
} | ||||
} | ||||
r235 | ||||
r1420 | if (!plottables.empty()) | |||
{ | ||||
|
r583 | auto plot = plottables.begin()->second->parentPlot(); | ||
|
r183 | |||
r1420 | if (rescaleAxes) | |||
{ | ||||
|
r583 | plot->rescaleAxes(); | ||
} | ||||
} | ||||
|
r182 | } | ||
|
r583 | }; | ||
r1420 | ||||
|
r903 | template <typename T> | ||
r1420 | struct PlottablesUpdater<T, typename std::enable_if_t<std::is_base_of<VectorTimeSerie, T>::value>> | |||
{ | ||||
static void setPlotYAxisRange(T& dataSeries, const DateTimeRange& xAxisRange, QCustomPlot& plot) | ||||
|
r904 | { | ||
r1420 | double minValue = 0., maxValue = 0.; | |||
if (auto serie = dynamic_cast<VectorTimeSerie*>(&dataSeries)) | ||||
{ | ||||
std::for_each( | ||||
std::begin(*serie), std::end(*serie), [&minValue, &maxValue](const auto& v) { | ||||
minValue = std::min({ minValue, v.v().x, v.v().y, v.v().z }); | ||||
maxValue = std::max({ maxValue, v.v().x, v.v().y, v.v().z }); | ||||
}); | ||||
|
r904 | } | ||
r1420 | ||||
plot.yAxis->setRange(QCPRange { minValue, maxValue }); | ||||
|
r904 | } | ||
r1420 | static void updatePlottables( | |||
T& dataSeries, PlottablesMap& plottables, const DateTimeRange& range, bool rescaleAxes) | ||||
|
r903 | { | ||
r1420 | // For each plottable to update, resets its data | |||
for (const auto& plottable : plottables) | ||||
{ | ||||
if (auto graph = dynamic_cast<QCPGraph*>(plottable.second)) | ||||
{ | ||||
auto dataContainer = QSharedPointer<SqpDataContainer>::create(); | ||||
if (auto serie = dynamic_cast<VectorTimeSerie*>(&dataSeries)) | ||||
{ | ||||
switch (plottable.first) | ||||
{ | ||||
case 0: | ||||
std::for_each(std::begin(*serie), std::end(*serie), | ||||
[&dataContainer](const auto& value) { | ||||
dataContainer->appendGraphData( | ||||
QCPGraphData(value.t(), value.v().x)); | ||||
}); | ||||
break; | ||||
case 1: | ||||
std::for_each(std::begin(*serie), std::end(*serie), | ||||
[&dataContainer](const auto& value) { | ||||
dataContainer->appendGraphData( | ||||
QCPGraphData(value.t(), value.v().y)); | ||||
}); | ||||
break; | ||||
case 2: | ||||
std::for_each(std::begin(*serie), std::end(*serie), | ||||
[&dataContainer](const auto& value) { | ||||
dataContainer->appendGraphData( | ||||
QCPGraphData(value.t(), value.v().z)); | ||||
}); | ||||
break; | ||||
default: | ||||
break; | ||||
} | ||||
|
r922 | } | ||
r1420 | graph->setData(dataContainer); | |||
|
r903 | } | ||
} | ||||
r1420 | if (!plottables.empty()) | |||
{ | ||||
auto plot = plottables.begin()->second->parentPlot(); | ||||
|
r903 | |||
r1420 | if (rescaleAxes) | |||
{ | ||||
plot->rescaleAxes(); | ||||
} | ||||
|
r903 | } | ||
} | ||||
}; | ||||
r1431 | ||||
template <typename T> | ||||
r1432 | struct PlottablesUpdater<T, | |||
typename std::enable_if_t<std::is_base_of<MultiComponentTimeSerie, T>::value>> | ||||
r1431 | { | |||
static void setPlotYAxisRange(T& dataSeries, const DateTimeRange& xAxisRange, QCustomPlot& plot) | ||||
{ | ||||
double minValue = 0., maxValue = 0.; | ||||
if (auto serie = dynamic_cast<MultiComponentTimeSerie*>(&dataSeries)) | ||||
{ | ||||
r1436 | std::for_each( | |||
std::begin(*serie), std::end(*serie), [&minValue, &maxValue](const auto& v) { | ||||
r1464 | minValue = std::min(minValue, std::min_element(v.begin(), v.end())->v()); | |||
maxValue = std::max(maxValue, std::max_element(v.begin(), v.end())->v()); | ||||
r1436 | }); | |||
r1431 | } | |||
plot.yAxis->setRange(QCPRange { minValue, maxValue }); | ||||
} | ||||
static void updatePlottables( | ||||
T& dataSeries, PlottablesMap& plottables, const DateTimeRange& range, bool rescaleAxes) | ||||
{ | ||||
for (const auto& plottable : plottables) | ||||
{ | ||||
if (auto graph = dynamic_cast<QCPGraph*>(plottable.second)) | ||||
{ | ||||
auto dataContainer = QSharedPointer<SqpDataContainer>::create(); | ||||
if (auto serie = dynamic_cast<MultiComponentTimeSerie*>(&dataSeries)) | ||||
{ | ||||
r1432 | // TODO | |||
std::for_each(std::begin(*serie), std::end(*serie), | ||||
[&dataContainer, component = plottable.first](const auto& value) { | ||||
dataContainer->appendGraphData( | ||||
QCPGraphData(value.t(), value[component])); | ||||
}); | ||||
r1431 | } | |||
graph->setData(dataContainer); | ||||
} | ||||
} | ||||
if (!plottables.empty()) | ||||
{ | ||||
auto plot = plottables.begin()->second->parentPlot(); | ||||
if (rescaleAxes) | ||||
{ | ||||
plot->rescaleAxes(); | ||||
} | ||||
} | ||||
} | ||||
}; | ||||
r1420 | /** | |||
* Specialization of PlottablesUpdater for spectrograms | ||||
* @sa SpectrogramSeries | ||||
*/ | ||||
template <typename T> | ||||
struct PlottablesUpdater<T, | ||||
typename std::enable_if_t<std::is_base_of<SpectrogramTimeSerie, T>::value>> | ||||
{ | ||||
static void setPlotYAxisRange(T& dataSeries, const DateTimeRange& xAxisRange, QCustomPlot& plot) | ||||
{ | ||||
r1465 | auto [minValue, maxValue] = dataSeries.axis_range(1); | |||
std::cout << "min=" << minValue << " max=" << maxValue << std::endl; | ||||
r1464 | plot.yAxis->setRange(QCPRange { minValue, maxValue }); | |||
r1420 | } | |||
static void updatePlottables( | ||||
T& dataSeries, PlottablesMap& plottables, const DateTimeRange& range, bool rescaleAxes) | ||||
{ | ||||
r1464 | if (plottables.empty()) | |||
{ | ||||
qCDebug(LOG_VisualizationGraphHelper()) | ||||
<< QObject::tr("Can't update spectrogram: no colormap has been associated"); | ||||
return; | ||||
} | ||||
r1420 | ||||
r1465 | // Gets the colormap to update (normally there is only one colormap) | |||
r1464 | Q_ASSERT(plottables.size() == 1); | |||
auto colormap = dynamic_cast<QCPColorMap*>(plottables.at(0)); | ||||
Q_ASSERT(colormap != nullptr); | ||||
r1465 | auto plot = colormap->parentPlot(); | |||
auto [minValue, maxValue] = dataSeries.axis_range(1); | ||||
plot->yAxis->setRange(QCPRange { minValue, maxValue }); | ||||
r1464 | if (auto serie = dynamic_cast<SpectrogramTimeSerie*>(&dataSeries)) | |||
{ | ||||
r1465 | if (serie->size(0) > 2) | |||
r1464 | { | |||
r1465 | const auto& xAxis = serie->axis(0); | |||
auto yAxis = serie->axis(1); // copy for in place reverse order | ||||
std::reverse(std::begin(yAxis), std::end(yAxis)); | ||||
auto xAxisProperties = TimeSeriesUtils::axis_analysis<TimeSeriesUtils::IsLinear, | ||||
TimeSeriesUtils::CheckMedian>(xAxis); | ||||
auto yAxisProperties = TimeSeriesUtils::axis_analysis<TimeSeriesUtils::IsLog, | ||||
TimeSeriesUtils::DontCheckMedian>(yAxis); | ||||
int colormap_h_size = std::min(32000, | ||||
static_cast<int>(xAxisProperties.range / xAxisProperties.max_resolution)); | ||||
auto colormap_v_size | ||||
= static_cast<int>(yAxisProperties.range / yAxisProperties.max_resolution); | ||||
colormap->data()->setSize(colormap_h_size, colormap_v_size); | ||||
r1464 | colormap->data()->setRange( | |||
QCPRange { serie->begin()->t(), (serie->end() - 1)->t() }, | ||||
r1465 | { minValue, maxValue }); | |||
std::vector<std::pair<int, int>> y_access_pattern; | ||||
for (int y_index = 0, cel_index = 0; y_index < colormap_v_size; y_index++) | ||||
r1464 | { | |||
r1465 | double current_y = pow( | |||
10., (yAxisProperties.max_resolution * y_index) + std::log10(minValue)); | ||||
if (current_y > yAxis[cel_index]) | ||||
cel_index++; | ||||
y_access_pattern.push_back({ y_index, yAxis.size() - 1 - cel_index }); | ||||
} | ||||
auto line = serie->begin(); | ||||
double current_time = xAxis[0]; | ||||
int x_index = 0; | ||||
while (x_index < colormap_h_size) | ||||
{ | ||||
if (current_time > (line + 1)->t()) | ||||
r1464 | { | |||
r1465 | line++; | |||
} | ||||
if ((current_time - xAxis[0]) | ||||
> (x_index * xAxisProperties.range / colormap_h_size)) | ||||
{ | ||||
x_index++; | ||||
} | ||||
if (line->t() <= (current_time + xAxisProperties.max_resolution)) | ||||
{ | ||||
std::for_each(std::cbegin(y_access_pattern), std::cend(y_access_pattern), | ||||
[&colormap, &line, x_index](const auto& acc) { | ||||
colormap->data()->setCell(x_index, acc.first, (*line)[acc.second]); | ||||
}); | ||||
} | ||||
else | ||||
{ | ||||
for (int y_index = 0; y_index < colormap_v_size; y_index++) | ||||
r1464 | { | |||
r1465 | colormap->data()->setCell(x_index, y_index, std::nan("")); | |||
r1464 | } | |||
} | ||||
r1465 | current_time += xAxisProperties.max_resolution; | |||
r1464 | } | |||
} | ||||
r1465 | ||||
if (rescaleAxes) | ||||
{ | ||||
plot->rescaleAxes(); | ||||
} | ||||
r1464 | } | |||
r1420 | } | |||
}; | ||||
|
r583 | /** | ||
* Helper used to create/update plottables | ||||
*/ | ||||
r1420 | struct IPlottablesHelper | |||
{ | ||||
|
r583 | virtual ~IPlottablesHelper() noexcept = default; | ||
r1420 | virtual PlottablesMap create(QCustomPlot& plot) const = 0; | |||
virtual void setYAxisRange(const DateTimeRange& xAxisRange, QCustomPlot& plot) const = 0; | ||||
virtual void update( | ||||
PlottablesMap& plottables, const DateTimeRange& range, bool rescaleAxes = false) const = 0; | ||||
|
r583 | }; | ||
/** | ||||
r1465 | * Default implementation of IPlottablesHelper, which takes data series to create/update | |||
* plottables | ||||
|
r583 | * @tparam T the data series' type | ||
*/ | ||||
template <typename T> | ||||
r1420 | struct PlottablesHelper : public IPlottablesHelper | |||
{ | ||||
r1421 | explicit PlottablesHelper(std::shared_ptr<T> dataSeries) : m_DataSeries { dataSeries } {} | |||
|
r583 | |||
r1420 | PlottablesMap create(QCustomPlot& plot) const override | |||
|
r583 | { | ||
r1431 | return PlottablesCreator<T>::createPlottables(plot, m_DataSeries); | |||
|
r182 | } | ||
r1420 | void update( | |||
PlottablesMap& plottables, const DateTimeRange& range, bool rescaleAxes) const override | ||||
|
r583 | { | ||
r1420 | if (m_DataSeries) | |||
{ | ||||
|
r1280 | PlottablesUpdater<T>::updatePlottables(*m_DataSeries, plottables, range, rescaleAxes); | ||
} | ||||
r1420 | else | |||
{ | ||||
|
r1280 | qCCritical(LOG_VisualizationGraphHelper()) << "Can't update plottables: inconsistency " | ||
"between the type of data series and the " | ||||
"type supposed"; | ||||
} | ||||
|
r583 | } | ||
|
r182 | |||
r1420 | void setYAxisRange(const DateTimeRange& xAxisRange, QCustomPlot& plot) const override | |||
|
r900 | { | ||
r1420 | if (m_DataSeries) | |||
{ | ||||
|
r1280 | PlottablesUpdater<T>::setPlotYAxisRange(*m_DataSeries, xAxisRange, plot); | ||
} | ||||
r1420 | else | |||
{ | ||||
|
r1280 | qCCritical(LOG_VisualizationGraphHelper()) << "Can't update plottables: inconsistency " | ||
"between the type of data series and the " | ||||
"type supposed"; | ||||
} | ||||
|
r900 | } | ||
r1421 | std::shared_ptr<T> m_DataSeries; | |||
|
r583 | }; | ||
|
r182 | |||
|
r1280 | /// Creates IPlottablesHelper according to the type of data series a variable holds | ||
r1420 | std::unique_ptr<IPlottablesHelper> createHelper(std::shared_ptr<Variable2> variable) noexcept | |||
r548 | { | |||
r1420 | switch (variable->type()) | |||
{ | ||||
|
r1280 | case DataSeriesType::SCALAR: | ||
r1420 | return std::make_unique<PlottablesHelper<ScalarTimeSerie>>( | |||
r1421 | std::dynamic_pointer_cast<ScalarTimeSerie>(variable->data())); | |||
|
r1280 | case DataSeriesType::SPECTROGRAM: | ||
r1420 | return std::make_unique<PlottablesHelper<SpectrogramTimeSerie>>( | |||
r1421 | std::dynamic_pointer_cast<SpectrogramTimeSerie>(variable->data())); | |||
|
r1280 | case DataSeriesType::VECTOR: | ||
r1420 | return std::make_unique<PlottablesHelper<VectorTimeSerie>>( | |||
r1421 | std::dynamic_pointer_cast<VectorTimeSerie>(variable->data())); | |||
r1431 | case DataSeriesType::MULTICOMPONENT: | |||
return std::make_unique<PlottablesHelper<MultiComponentTimeSerie>>( | ||||
std::dynamic_pointer_cast<MultiComponentTimeSerie>(variable->data())); | ||||
|
r1280 | default: | ||
// Creates default helper | ||||
break; | ||||
r548 | } | |||
|
r1280 | |||
r1420 | return std::make_unique<PlottablesHelper<TimeSeries::ITimeSerie>>(nullptr); | |||
r548 | } | |||
|
r583 | } // namespace | ||
|
r181 | |||
r1420 | PlottablesMap VisualizationGraphHelper::create( | |||
std::shared_ptr<Variable2> variable, QCustomPlot& plot) noexcept | ||||
|
r583 | { | ||
r1420 | if (variable) | |||
{ | ||||
|
r1280 | auto helper = createHelper(variable); | ||
|
r583 | auto plottables = helper->create(plot); | ||
return plottables; | ||||
|
r181 | } | ||
r1420 | else | |||
{ | ||||
r243 | qCDebug(LOG_VisualizationGraphHelper()) | |||
|
r181 | << QObject::tr("Can't create graph plottables : the variable is null"); | ||
r1420 | return PlottablesMap {}; | |||
|
r181 | } | ||
} | ||||
r235 | ||||
r1420 | void VisualizationGraphHelper::setYAxisRange( | |||
std::shared_ptr<Variable2> variable, QCustomPlot& plot) noexcept | ||||
|
r900 | { | ||
r1420 | if (variable) | |||
{ | ||||
|
r1280 | auto helper = createHelper(variable); | ||
|
r900 | helper->setYAxisRange(variable->range(), plot); | ||
} | ||||
r1420 | else | |||
{ | ||||
|
r900 | qCDebug(LOG_VisualizationGraphHelper()) | ||
<< QObject::tr("Can't set y-axis range of plot: the variable is null"); | ||||
} | ||||
} | ||||
r1420 | void VisualizationGraphHelper::updateData( | |||
PlottablesMap& plottables, std::shared_ptr<Variable2> variable, const DateTimeRange& dateTime) | ||||
r235 | { | |||
|
r1280 | auto helper = createHelper(variable); | ||
|
r583 | helper->update(plottables, dateTime); | ||
r235 | } | |||