##// END OF EJS Templates
Implements spectrograms display (3)...
Alexandre Leroux -
r907:b4b8a9876f21
parent child
Show More
@@ -1,331 +1,342
1 #include "Visualization/VisualizationGraphHelper.h"
1 #include "Visualization/VisualizationGraphHelper.h"
2 #include "Visualization/qcustomplot.h"
2 #include "Visualization/qcustomplot.h"
3
3
4 #include <Common/ColorUtils.h>
4 #include <Common/ColorUtils.h>
5
5
6 #include <Data/ScalarSeries.h>
6 #include <Data/ScalarSeries.h>
7 #include <Data/SpectrogramSeries.h>
7 #include <Data/SpectrogramSeries.h>
8 #include <Data/VectorSeries.h>
8 #include <Data/VectorSeries.h>
9
9
10 #include <Variable/Variable.h>
10 #include <Variable/Variable.h>
11
11
12 Q_LOGGING_CATEGORY(LOG_VisualizationGraphHelper, "VisualizationGraphHelper")
12 Q_LOGGING_CATEGORY(LOG_VisualizationGraphHelper, "VisualizationGraphHelper")
13
13
14 namespace {
14 namespace {
15
15
16 class SqpDataContainer : public QCPGraphDataContainer {
16 class SqpDataContainer : public QCPGraphDataContainer {
17 public:
17 public:
18 void appendGraphData(const QCPGraphData &data) { mData.append(data); }
18 void appendGraphData(const QCPGraphData &data) { mData.append(data); }
19 };
19 };
20
20
21 /**
21 /**
22 * Struct used to create plottables, depending on the type of the data series from which to create
22 * Struct used to create plottables, depending on the type of the data series from which to create
23 * them
23 * them
24 * @tparam T the data series' type
24 * @tparam T the data series' type
25 * @remarks Default implementation can't create plottables
25 * @remarks Default implementation can't create plottables
26 */
26 */
27 template <typename T, typename Enabled = void>
27 template <typename T, typename Enabled = void>
28 struct PlottablesCreator {
28 struct PlottablesCreator {
29 static PlottablesMap createPlottables(T &, QCustomPlot &)
29 static PlottablesMap createPlottables(T &, QCustomPlot &)
30 {
30 {
31 qCCritical(LOG_DataSeries())
31 qCCritical(LOG_DataSeries())
32 << QObject::tr("Can't create plottables: unmanaged data series type");
32 << QObject::tr("Can't create plottables: unmanaged data series type");
33 return {};
33 return {};
34 }
34 }
35 };
35 };
36
36
37 /**
37 /**
38 * Specialization of PlottablesCreator for scalars and vectors
38 * Specialization of PlottablesCreator for scalars and vectors
39 * @sa ScalarSeries
39 * @sa ScalarSeries
40 * @sa VectorSeries
40 * @sa VectorSeries
41 */
41 */
42 template <typename T>
42 template <typename T>
43 struct PlottablesCreator<T,
43 struct PlottablesCreator<T,
44 typename std::enable_if_t<std::is_base_of<ScalarSeries, T>::value
44 typename std::enable_if_t<std::is_base_of<ScalarSeries, T>::value
45 or std::is_base_of<VectorSeries, T>::value> > {
45 or std::is_base_of<VectorSeries, T>::value> > {
46 static PlottablesMap createPlottables(T &dataSeries, QCustomPlot &plot)
46 static PlottablesMap createPlottables(T &dataSeries, QCustomPlot &plot)
47 {
47 {
48 PlottablesMap result{};
48 PlottablesMap result{};
49
49
50 // Gets the number of components of the data series
50 // Gets the number of components of the data series
51 dataSeries.lockRead();
51 dataSeries.lockRead();
52 auto componentCount = dataSeries.valuesData()->componentCount();
52 auto componentCount = dataSeries.valuesData()->componentCount();
53 dataSeries.unlock();
53 dataSeries.unlock();
54
54
55 auto colors = ColorUtils::colors(Qt::blue, Qt::red, componentCount);
55 auto colors = ColorUtils::colors(Qt::blue, Qt::red, componentCount);
56
56
57 // For each component of the data series, creates a QCPGraph to add to the plot
57 // For each component of the data series, creates a QCPGraph to add to the plot
58 for (auto i = 0; i < componentCount; ++i) {
58 for (auto i = 0; i < componentCount; ++i) {
59 auto graph = plot.addGraph();
59 auto graph = plot.addGraph();
60 graph->setPen(QPen{colors.at(i)});
60 graph->setPen(QPen{colors.at(i)});
61
61
62 result.insert({i, graph});
62 result.insert({i, graph});
63 }
63 }
64
64
65 plot.replot();
65 plot.replot();
66
66
67 return result;
67 return result;
68 }
68 }
69 };
69 };
70
70
71 /**
71 /**
72 * Specialization of PlottablesCreator for spectrograms
72 * Specialization of PlottablesCreator for spectrograms
73 * @sa SpectrogramSeries
73 * @sa SpectrogramSeries
74 */
74 */
75 template <typename T>
75 template <typename T>
76 struct PlottablesCreator<T,
76 struct PlottablesCreator<T,
77 typename std::enable_if_t<std::is_base_of<SpectrogramSeries, T>::value> > {
77 typename std::enable_if_t<std::is_base_of<SpectrogramSeries, T>::value> > {
78 static PlottablesMap createPlottables(T &dataSeries, QCustomPlot &plot)
78 static PlottablesMap createPlottables(T &dataSeries, QCustomPlot &plot)
79 {
79 {
80 PlottablesMap result{};
80 PlottablesMap result{};
81 result.insert({0, new QCPColorMap{plot.xAxis, plot.yAxis}});
81 result.insert({0, new QCPColorMap{plot.xAxis, plot.yAxis}});
82
82
83 plot.replot();
83 plot.replot();
84
84
85 return result;
85 return result;
86 }
86 }
87 };
87 };
88
88
89 /**
89 /**
90 * Struct used to update plottables, depending on the type of the data series from which to update
90 * Struct used to update plottables, depending on the type of the data series from which to update
91 * them
91 * them
92 * @tparam T the data series' type
92 * @tparam T the data series' type
93 * @remarks Default implementation can't update plottables
93 * @remarks Default implementation can't update plottables
94 */
94 */
95 template <typename T, typename Enabled = void>
95 template <typename T, typename Enabled = void>
96 struct PlottablesUpdater {
96 struct PlottablesUpdater {
97 static void setPlotYAxisRange(T &, const SqpRange &, QCustomPlot &)
97 static void setPlotYAxisRange(T &, const SqpRange &, QCustomPlot &)
98 {
98 {
99 qCCritical(LOG_DataSeries())
99 qCCritical(LOG_DataSeries())
100 << QObject::tr("Can't set plot y-axis range: unmanaged data series type");
100 << QObject::tr("Can't set plot y-axis range: unmanaged data series type");
101 }
101 }
102
102
103 static void updatePlottables(T &, PlottablesMap &, const SqpRange &, bool)
103 static void updatePlottables(T &, PlottablesMap &, const SqpRange &, bool)
104 {
104 {
105 qCCritical(LOG_DataSeries())
105 qCCritical(LOG_DataSeries())
106 << QObject::tr("Can't update plottables: unmanaged data series type");
106 << QObject::tr("Can't update plottables: unmanaged data series type");
107 }
107 }
108 };
108 };
109
109
110 /**
110 /**
111 * Specialization of PlottablesUpdater for scalars and vectors
111 * Specialization of PlottablesUpdater for scalars and vectors
112 * @sa ScalarSeries
112 * @sa ScalarSeries
113 * @sa VectorSeries
113 * @sa VectorSeries
114 */
114 */
115 template <typename T>
115 template <typename T>
116 struct PlottablesUpdater<T,
116 struct PlottablesUpdater<T,
117 typename std::enable_if_t<std::is_base_of<ScalarSeries, T>::value
117 typename std::enable_if_t<std::is_base_of<ScalarSeries, T>::value
118 or std::is_base_of<VectorSeries, T>::value> > {
118 or std::is_base_of<VectorSeries, T>::value> > {
119 static void setPlotYAxisRange(T &dataSeries, const SqpRange &xAxisRange, QCustomPlot &plot)
119 static void setPlotYAxisRange(T &dataSeries, const SqpRange &xAxisRange, QCustomPlot &plot)
120 {
120 {
121 auto minValue = 0., maxValue = 0.;
121 auto minValue = 0., maxValue = 0.;
122
122
123 dataSeries.lockRead();
123 dataSeries.lockRead();
124 auto valuesBounds = dataSeries.valuesBounds(xAxisRange.m_TStart, xAxisRange.m_TEnd);
124 auto valuesBounds = dataSeries.valuesBounds(xAxisRange.m_TStart, xAxisRange.m_TEnd);
125 auto end = dataSeries.cend();
125 auto end = dataSeries.cend();
126 if (valuesBounds.first != end && valuesBounds.second != end) {
126 if (valuesBounds.first != end && valuesBounds.second != end) {
127 auto rangeValue = [](const auto &value) { return std::isnan(value) ? 0. : value; };
127 auto rangeValue = [](const auto &value) { return std::isnan(value) ? 0. : value; };
128
128
129 minValue = rangeValue(valuesBounds.first->minValue());
129 minValue = rangeValue(valuesBounds.first->minValue());
130 maxValue = rangeValue(valuesBounds.second->maxValue());
130 maxValue = rangeValue(valuesBounds.second->maxValue());
131 }
131 }
132 dataSeries.unlock();
132 dataSeries.unlock();
133
133
134 plot.yAxis->setRange(QCPRange{minValue, maxValue});
134 plot.yAxis->setRange(QCPRange{minValue, maxValue});
135 }
135 }
136
136
137 static void updatePlottables(T &dataSeries, PlottablesMap &plottables, const SqpRange &range,
137 static void updatePlottables(T &dataSeries, PlottablesMap &plottables, const SqpRange &range,
138 bool rescaleAxes)
138 bool rescaleAxes)
139 {
139 {
140
140
141 // For each plottable to update, resets its data
141 // For each plottable to update, resets its data
142 std::map<int, QSharedPointer<SqpDataContainer> > dataContainers{};
142 std::map<int, QSharedPointer<SqpDataContainer> > dataContainers{};
143 for (const auto &plottable : plottables) {
143 for (const auto &plottable : plottables) {
144 if (auto graph = dynamic_cast<QCPGraph *>(plottable.second)) {
144 if (auto graph = dynamic_cast<QCPGraph *>(plottable.second)) {
145 auto dataContainer = QSharedPointer<SqpDataContainer>::create();
145 auto dataContainer = QSharedPointer<SqpDataContainer>::create();
146 graph->setData(dataContainer);
146 graph->setData(dataContainer);
147
147
148 dataContainers.insert({plottable.first, dataContainer});
148 dataContainers.insert({plottable.first, dataContainer});
149 }
149 }
150 }
150 }
151 dataSeries.lockRead();
151 dataSeries.lockRead();
152
152
153 // - Gets the data of the series included in the current range
153 // - Gets the data of the series included in the current range
154 // - Updates each plottable by adding, for each data item, a point that takes x-axis data
154 // - Updates each plottable by adding, for each data item, a point that takes x-axis data
155 // and value data. The correct value is retrieved according to the index of the component
155 // and value data. The correct value is retrieved according to the index of the component
156 auto subDataIts = dataSeries.xAxisRange(range.m_TStart, range.m_TEnd);
156 auto subDataIts = dataSeries.xAxisRange(range.m_TStart, range.m_TEnd);
157 for (auto it = subDataIts.first; it != subDataIts.second; ++it) {
157 for (auto it = subDataIts.first; it != subDataIts.second; ++it) {
158 for (const auto &dataContainer : dataContainers) {
158 for (const auto &dataContainer : dataContainers) {
159 auto componentIndex = dataContainer.first;
159 auto componentIndex = dataContainer.first;
160 dataContainer.second->appendGraphData(
160 dataContainer.second->appendGraphData(
161 QCPGraphData(it->x(), it->value(componentIndex)));
161 QCPGraphData(it->x(), it->value(componentIndex)));
162 }
162 }
163 }
163 }
164
164
165 dataSeries.unlock();
165 dataSeries.unlock();
166
166
167 if (!plottables.empty()) {
167 if (!plottables.empty()) {
168 auto plot = plottables.begin()->second->parentPlot();
168 auto plot = plottables.begin()->second->parentPlot();
169
169
170 if (rescaleAxes) {
170 if (rescaleAxes) {
171 plot->rescaleAxes();
171 plot->rescaleAxes();
172 }
172 }
173
173
174 plot->replot();
174 plot->replot();
175 }
175 }
176 }
176 }
177 };
177 };
178
178
179 /**
179 /**
180 * Specialization of PlottablesUpdater for spectrograms
180 * Specialization of PlottablesUpdater for spectrograms
181 * @sa SpectrogramSeries
181 * @sa SpectrogramSeries
182 */
182 */
183 template <typename T>
183 template <typename T>
184 struct PlottablesUpdater<T,
184 struct PlottablesUpdater<T,
185 typename std::enable_if_t<std::is_base_of<SpectrogramSeries, T>::value> > {
185 typename std::enable_if_t<std::is_base_of<SpectrogramSeries, T>::value> > {
186 static void setPlotYAxisRange(T &dataSeries, const SqpRange &xAxisRange, QCustomPlot &plot)
187 {
188 double min, max;
189 /// @todo ALX: use iterators here
190 std::tie(min, max) = dataSeries.yAxis().bounds();
191
192 if (!std::isnan(min) && !std::isnan(max)) {
193 plot.yAxis->setRange(QCPRange{min, max});
194 }
195 }
196
186 static void updatePlottables(T &dataSeries, PlottablesMap &plottables, const SqpRange &range,
197 static void updatePlottables(T &dataSeries, PlottablesMap &plottables, const SqpRange &range,
187 bool rescaleAxes)
198 bool rescaleAxes)
188 {
199 {
189 if (plottables.empty()) {
200 if (plottables.empty()) {
190 qCDebug(LOG_VisualizationGraphHelper())
201 qCDebug(LOG_VisualizationGraphHelper())
191 << QObject::tr("Can't update spectrogram: no colormap has been associated");
202 << QObject::tr("Can't update spectrogram: no colormap has been associated");
192 return;
203 return;
193 }
204 }
194
205
195 // Gets the colormap to update (normally there is only one colormap)
206 // Gets the colormap to update (normally there is only one colormap)
196 Q_ASSERT(plottables.size() == 1);
207 Q_ASSERT(plottables.size() == 1);
197 auto colormap = dynamic_cast<QCPColorMap *>(plottables.at(0));
208 auto colormap = dynamic_cast<QCPColorMap *>(plottables.at(0));
198 Q_ASSERT(colormap != nullptr);
209 Q_ASSERT(colormap != nullptr);
199
210
200 dataSeries.lockRead();
211 dataSeries.lockRead();
201
212
202 auto its = dataSeries.xAxisRange(range.m_TStart, range.m_TEnd);
213 auto its = dataSeries.xAxisRange(range.m_TStart, range.m_TEnd);
203 /// @todo ALX: use iterators here
214 /// @todo ALX: use iterators here
204 auto yAxis = dataSeries.yAxis();
215 auto yAxis = dataSeries.yAxis();
205
216
206 // Gets properties of x-axis and y-axis to set size and range of the colormap
217 // Gets properties of x-axis and y-axis to set size and range of the colormap
207 auto nbX = std::distance(its.first, its.second);
218 auto nbX = std::distance(its.first, its.second);
208 auto xMin = nbX != 0 ? its.first->x() : 0.;
219 auto xMin = nbX != 0 ? its.first->x() : 0.;
209 auto xMax = nbX != 0 ? (its.second - 1)->x() : 0.;
220 auto xMax = nbX != 0 ? (its.second - 1)->x() : 0.;
210
221
211 auto nbY = yAxis.size();
222 auto nbY = yAxis.size();
212 auto yMin = 0., yMax = 0.;
223 auto yMin = 0., yMax = 0.;
213 if (nbY != 0) {
224 if (nbY != 0) {
214 std::tie(yMin, yMax) = yAxis.bounds();
225 std::tie(yMin, yMax) = yAxis.bounds();
215 }
226 }
216
227
217 colormap->data()->setSize(nbX, nbY);
228 colormap->data()->setSize(nbX, nbY);
218 colormap->data()->setRange(QCPRange{xMin, xMax}, QCPRange{yMin, yMax});
229 colormap->data()->setRange(QCPRange{xMin, xMax}, QCPRange{yMin, yMax});
219
230
220 // Sets values
231 // Sets values
221 auto xIndex = 0;
232 auto xIndex = 0;
222 for (auto it = its.first; it != its.second; ++it, ++xIndex) {
233 for (auto it = its.first; it != its.second; ++it, ++xIndex) {
223 for (auto yIndex = 0; yIndex < nbY; ++yIndex) {
234 for (auto yIndex = 0; yIndex < nbY; ++yIndex) {
224 colormap->data()->setCell(xIndex, yIndex, it->value(yIndex));
235 colormap->data()->setCell(xIndex, yIndex, it->value(yIndex));
225 }
236 }
226 }
237 }
227
238
228 dataSeries.unlock();
239 dataSeries.unlock();
229
240
230 // Rescales axes
241 // Rescales axes
231 auto plot = colormap->parentPlot();
242 auto plot = colormap->parentPlot();
232
243
233 if (rescaleAxes) {
244 if (rescaleAxes) {
234 plot->rescaleAxes();
245 plot->rescaleAxes();
235 }
246 }
236
247
237 plot->replot();
248 plot->replot();
238 }
249 }
239 };
250 };
240
251
241 /**
252 /**
242 * Helper used to create/update plottables
253 * Helper used to create/update plottables
243 */
254 */
244 struct IPlottablesHelper {
255 struct IPlottablesHelper {
245 virtual ~IPlottablesHelper() noexcept = default;
256 virtual ~IPlottablesHelper() noexcept = default;
246 virtual PlottablesMap create(QCustomPlot &plot) const = 0;
257 virtual PlottablesMap create(QCustomPlot &plot) const = 0;
247 virtual void setYAxisRange(const SqpRange &xAxisRange, QCustomPlot &plot) const = 0;
258 virtual void setYAxisRange(const SqpRange &xAxisRange, QCustomPlot &plot) const = 0;
248 virtual void update(PlottablesMap &plottables, const SqpRange &range,
259 virtual void update(PlottablesMap &plottables, const SqpRange &range,
249 bool rescaleAxes = false) const = 0;
260 bool rescaleAxes = false) const = 0;
250 };
261 };
251
262
252 /**
263 /**
253 * Default implementation of IPlottablesHelper, which takes data series to create/update plottables
264 * Default implementation of IPlottablesHelper, which takes data series to create/update plottables
254 * @tparam T the data series' type
265 * @tparam T the data series' type
255 */
266 */
256 template <typename T>
267 template <typename T>
257 struct PlottablesHelper : public IPlottablesHelper {
268 struct PlottablesHelper : public IPlottablesHelper {
258 explicit PlottablesHelper(T &dataSeries) : m_DataSeries{dataSeries} {}
269 explicit PlottablesHelper(T &dataSeries) : m_DataSeries{dataSeries} {}
259
270
260 PlottablesMap create(QCustomPlot &plot) const override
271 PlottablesMap create(QCustomPlot &plot) const override
261 {
272 {
262 return PlottablesCreator<T>::createPlottables(m_DataSeries, plot);
273 return PlottablesCreator<T>::createPlottables(m_DataSeries, plot);
263 }
274 }
264
275
265 void update(PlottablesMap &plottables, const SqpRange &range, bool rescaleAxes) const override
276 void update(PlottablesMap &plottables, const SqpRange &range, bool rescaleAxes) const override
266 {
277 {
267 PlottablesUpdater<T>::updatePlottables(m_DataSeries, plottables, range, rescaleAxes);
278 PlottablesUpdater<T>::updatePlottables(m_DataSeries, plottables, range, rescaleAxes);
268 }
279 }
269
280
270 void setYAxisRange(const SqpRange &xAxisRange, QCustomPlot &plot) const override
281 void setYAxisRange(const SqpRange &xAxisRange, QCustomPlot &plot) const override
271 {
282 {
272 return PlottablesUpdater<T>::setPlotYAxisRange(m_DataSeries, xAxisRange, plot);
283 return PlottablesUpdater<T>::setPlotYAxisRange(m_DataSeries, xAxisRange, plot);
273 }
284 }
274
285
275 T &m_DataSeries;
286 T &m_DataSeries;
276 };
287 };
277
288
278 /// Creates IPlottablesHelper according to a data series
289 /// Creates IPlottablesHelper according to a data series
279 std::unique_ptr<IPlottablesHelper> createHelper(std::shared_ptr<IDataSeries> dataSeries) noexcept
290 std::unique_ptr<IPlottablesHelper> createHelper(std::shared_ptr<IDataSeries> dataSeries) noexcept
280 {
291 {
281 if (auto scalarSeries = std::dynamic_pointer_cast<ScalarSeries>(dataSeries)) {
292 if (auto scalarSeries = std::dynamic_pointer_cast<ScalarSeries>(dataSeries)) {
282 return std::make_unique<PlottablesHelper<ScalarSeries> >(*scalarSeries);
293 return std::make_unique<PlottablesHelper<ScalarSeries> >(*scalarSeries);
283 }
294 }
284 else if (auto spectrogramSeries = std::dynamic_pointer_cast<SpectrogramSeries>(dataSeries)) {
295 else if (auto spectrogramSeries = std::dynamic_pointer_cast<SpectrogramSeries>(dataSeries)) {
285 return std::make_unique<PlottablesHelper<SpectrogramSeries> >(*spectrogramSeries);
296 return std::make_unique<PlottablesHelper<SpectrogramSeries> >(*spectrogramSeries);
286 }
297 }
287 else if (auto vectorSeries = std::dynamic_pointer_cast<VectorSeries>(dataSeries)) {
298 else if (auto vectorSeries = std::dynamic_pointer_cast<VectorSeries>(dataSeries)) {
288 return std::make_unique<PlottablesHelper<VectorSeries> >(*vectorSeries);
299 return std::make_unique<PlottablesHelper<VectorSeries> >(*vectorSeries);
289 }
300 }
290 else {
301 else {
291 return std::make_unique<PlottablesHelper<IDataSeries> >(*dataSeries);
302 return std::make_unique<PlottablesHelper<IDataSeries> >(*dataSeries);
292 }
303 }
293 }
304 }
294
305
295 } // namespace
306 } // namespace
296
307
297 PlottablesMap VisualizationGraphHelper::create(std::shared_ptr<Variable> variable,
308 PlottablesMap VisualizationGraphHelper::create(std::shared_ptr<Variable> variable,
298 QCustomPlot &plot) noexcept
309 QCustomPlot &plot) noexcept
299 {
310 {
300 if (variable) {
311 if (variable) {
301 auto helper = createHelper(variable->dataSeries());
312 auto helper = createHelper(variable->dataSeries());
302 auto plottables = helper->create(plot);
313 auto plottables = helper->create(plot);
303 return plottables;
314 return plottables;
304 }
315 }
305 else {
316 else {
306 qCDebug(LOG_VisualizationGraphHelper())
317 qCDebug(LOG_VisualizationGraphHelper())
307 << QObject::tr("Can't create graph plottables : the variable is null");
318 << QObject::tr("Can't create graph plottables : the variable is null");
308 return PlottablesMap{};
319 return PlottablesMap{};
309 }
320 }
310 }
321 }
311
322
312 void VisualizationGraphHelper::setYAxisRange(std::shared_ptr<Variable> variable,
323 void VisualizationGraphHelper::setYAxisRange(std::shared_ptr<Variable> variable,
313 QCustomPlot &plot) noexcept
324 QCustomPlot &plot) noexcept
314 {
325 {
315 if (variable) {
326 if (variable) {
316 auto helper = createHelper(variable->dataSeries());
327 auto helper = createHelper(variable->dataSeries());
317 helper->setYAxisRange(variable->range(), plot);
328 helper->setYAxisRange(variable->range(), plot);
318 }
329 }
319 else {
330 else {
320 qCDebug(LOG_VisualizationGraphHelper())
331 qCDebug(LOG_VisualizationGraphHelper())
321 << QObject::tr("Can't set y-axis range of plot: the variable is null");
332 << QObject::tr("Can't set y-axis range of plot: the variable is null");
322 }
333 }
323 }
334 }
324
335
325 void VisualizationGraphHelper::updateData(PlottablesMap &plottables,
336 void VisualizationGraphHelper::updateData(PlottablesMap &plottables,
326 std::shared_ptr<IDataSeries> dataSeries,
337 std::shared_ptr<IDataSeries> dataSeries,
327 const SqpRange &dateTime)
338 const SqpRange &dateTime)
328 {
339 {
329 auto helper = createHelper(dataSeries);
340 auto helper = createHelper(dataSeries);
330 helper->update(plottables, dateTime);
341 helper->update(plottables, dateTime);
331 }
342 }
General Comments 0
You need to be logged in to leave comments. Login now