##// END OF EJS Templates
Merge branch 'feature/MockSpectrogram2' into develop
Alexandre Leroux -
r909:c6a1fc96dbd2 merge
parent child
Show More
@@ -1,482 +1,483
1 1 #ifndef SCIQLOP_DATASERIES_H
2 2 #define SCIQLOP_DATASERIES_H
3 3
4 4 #include "CoreGlobal.h"
5 5
6 6 #include <Common/SortUtils.h>
7 7
8 8 #include <Data/ArrayData.h>
9 9 #include <Data/DataSeriesMergeHelper.h>
10 10 #include <Data/IDataSeries.h>
11 11 #include <Data/OptionalAxis.h>
12 12
13 13 #include <QLoggingCategory>
14 14 #include <QReadLocker>
15 15 #include <QReadWriteLock>
16 16 #include <memory>
17 17
18 18 // We don't use the Qt macro since the log is used in the header file, which causes multiple log
19 19 // definitions with inheritance. Inline method is used instead
20 20 inline const QLoggingCategory &LOG_DataSeries()
21 21 {
22 22 static const QLoggingCategory category{"DataSeries"};
23 23 return category;
24 24 }
25 25
26 26 template <int Dim>
27 27 class DataSeries;
28 28
29 29 namespace dataseries_detail {
30 30
31 31 template <int Dim, bool IsConst>
32 32 class IteratorValue : public DataSeriesIteratorValue::Impl {
33 33 public:
34 34 friend class DataSeries<Dim>;
35 35
36 36 template <bool IC = IsConst, typename = std::enable_if_t<IC == false> >
37 37 explicit IteratorValue(DataSeries<Dim> &dataSeries, bool begin)
38 38 : m_XIt(begin ? dataSeries.xAxisData()->begin() : dataSeries.xAxisData()->end()),
39 39 m_ValuesIt(begin ? dataSeries.valuesData()->begin() : dataSeries.valuesData()->end())
40 40 {
41 41 }
42 42
43 43 template <bool IC = IsConst, typename = std::enable_if_t<IC == true> >
44 44 explicit IteratorValue(const DataSeries<Dim> &dataSeries, bool begin)
45 45 : m_XIt(begin ? dataSeries.xAxisData()->cbegin() : dataSeries.xAxisData()->cend()),
46 46 m_ValuesIt(begin ? dataSeries.valuesData()->cbegin()
47 47 : dataSeries.valuesData()->cend())
48 48 {
49 49 }
50 50
51 51 IteratorValue(const IteratorValue &other) = default;
52 52
53 53 std::unique_ptr<DataSeriesIteratorValue::Impl> clone() const override
54 54 {
55 55 return std::make_unique<IteratorValue<Dim, IsConst> >(*this);
56 56 }
57 57
58 58 int distance(const DataSeriesIteratorValue::Impl &other) const override try {
59 59 const auto &otherImpl = dynamic_cast<const IteratorValue &>(other);
60 60 return m_XIt->distance(*otherImpl.m_XIt);
61 61 }
62 62 catch (const std::bad_cast &) {
63 63 return 0;
64 64 }
65 65
66 66 bool equals(const DataSeriesIteratorValue::Impl &other) const override try {
67 67 const auto &otherImpl = dynamic_cast<const IteratorValue &>(other);
68 68 return std::tie(m_XIt, m_ValuesIt) == std::tie(otherImpl.m_XIt, otherImpl.m_ValuesIt);
69 69 }
70 70 catch (const std::bad_cast &) {
71 71 return false;
72 72 }
73 73
74 74 bool lowerThan(const DataSeriesIteratorValue::Impl &other) const override try {
75 75 const auto &otherImpl = dynamic_cast<const IteratorValue &>(other);
76 76 return m_XIt->lowerThan(*otherImpl.m_XIt);
77 77 }
78 78 catch (const std::bad_cast &) {
79 79 return false;
80 80 }
81 81
82 82 std::unique_ptr<DataSeriesIteratorValue::Impl> advance(int offset) const override
83 83 {
84 84 auto result = clone();
85 85 result->next(offset);
86 86 return result;
87 87 }
88 88
89 89 void next(int offset) override
90 90 {
91 91 m_XIt->next(offset);
92 92 m_ValuesIt->next(offset);
93 93 }
94 94
95 95 void prev() override
96 96 {
97 97 --m_XIt;
98 98 --m_ValuesIt;
99 99 }
100 100
101 101 double x() const override { return m_XIt->at(0); }
102 102 double value() const override { return m_ValuesIt->at(0); }
103 103 double value(int componentIndex) const override { return m_ValuesIt->at(componentIndex); }
104 104 double minValue() const override { return m_ValuesIt->min(); }
105 105 double maxValue() const override { return m_ValuesIt->max(); }
106 106 QVector<double> values() const override { return m_ValuesIt->values(); }
107 107
108 108 void swap(DataSeriesIteratorValue::Impl &other) override
109 109 {
110 110 auto &otherImpl = dynamic_cast<IteratorValue &>(other);
111 111 m_XIt->impl()->swap(*otherImpl.m_XIt->impl());
112 112 m_ValuesIt->impl()->swap(*otherImpl.m_ValuesIt->impl());
113 113 }
114 114
115 115 private:
116 116 ArrayDataIterator m_XIt;
117 117 ArrayDataIterator m_ValuesIt;
118 118 };
119 119 } // namespace dataseries_detail
120 120
121 121 /**
122 122 * @brief The DataSeries class is the base (abstract) implementation of IDataSeries.
123 123 *
124 124 * The DataSeries represents values on one or two axes, according to these rules:
125 125 * - the x-axis is always defined
126 126 * - an y-axis can be defined or not. If set, additional consistency checks apply to the values (see
127 127 * below)
128 128 * - the values are defined on one or two dimensions. In the case of 2-dim values, the data is
129 129 * distributed into components (for example, a vector defines three components)
130 130 * - New values can be added to the series, on the x-axis.
131 131 * - Once initialized to the series creation, the y-axis (if defined) is no longer modifiable
132 132 * - Data representing values and axes are associated with a unit
133 133 * - The data series is always sorted in ascending order on the x-axis.
134 134 *
135 135 * Consistency checks are carried out between the axes and the values. These controls are provided
136 136 * throughout the DataSeries lifecycle:
137 137 * - the number of data on the x-axis must be equal to the number of values (in the case of
138 138 * 2-dim ArrayData for values, the test is performed on the number of values per component)
139 139 * - if the y-axis is defined, the number of components of the ArrayData for values must equal the
140 140 * number of data on the y-axis.
141 141 *
142 142 * Examples:
143 143 * 1)
144 144 * - x-axis: [1 ; 2 ; 3]
145 145 * - y-axis: not defined
146 146 * - values: [10 ; 20 ; 30] (1-dim ArrayData)
147 147 * => the DataSeries is valid, as x-axis and values have the same number of data
148 148 *
149 149 * 2)
150 150 * - x-axis: [1 ; 2 ; 3]
151 151 * - y-axis: not defined
152 152 * - values: [10 ; 20 ; 30 ; 40] (1-dim ArrayData)
153 153 * => the DataSeries is invalid, as x-axis and values haven't the same number of data
154 154 *
155 155 * 3)
156 156 * - x-axis: [1 ; 2 ; 3]
157 157 * - y-axis: not defined
158 158 * - values: [10 ; 20 ; 30
159 159 * 40 ; 50 ; 60] (2-dim ArrayData)
160 160 * => the DataSeries is valid, as x-axis has 3 data and values contains 2 components with 3
161 161 * data each
162 162 *
163 163 * 4)
164 164 * - x-axis: [1 ; 2 ; 3]
165 165 * - y-axis: [1 ; 2]
166 166 * - values: [10 ; 20 ; 30
167 167 * 40 ; 50 ; 60] (2-dim ArrayData)
168 168 * => the DataSeries is valid, as:
169 169 * - x-axis has 3 data and values contains 2 components with 3 data each AND
170 170 * - y-axis has 2 data and values contains 2 components
171 171 *
172 172 * 5)
173 173 * - x-axis: [1 ; 2 ; 3]
174 174 * - y-axis: [1 ; 2 ; 3]
175 175 * - values: [10 ; 20 ; 30
176 176 * 40 ; 50 ; 60] (2-dim ArrayData)
177 177 * => the DataSeries is invalid, as:
178 178 * - x-axis has 3 data and values contains 2 components with 3 data each BUT
179 179 * - y-axis has 3 data and values contains only 2 components
180 180 *
181 181 * @tparam Dim The dimension of the values data
182 182 *
183 183 */
184 184 template <int Dim>
185 185 class SCIQLOP_CORE_EXPORT DataSeries : public IDataSeries {
186 186 friend class DataSeriesMergeHelper;
187 187
188 188 public:
189 189 /// @sa IDataSeries::xAxisData()
190 190 std::shared_ptr<ArrayData<1> > xAxisData() override { return m_XAxisData; }
191 191 const std::shared_ptr<ArrayData<1> > xAxisData() const { return m_XAxisData; }
192 192
193 193 /// @sa IDataSeries::xAxisUnit()
194 194 Unit xAxisUnit() const override { return m_XAxisUnit; }
195 195
196 196 /// @return the values dataset
197 197 std::shared_ptr<ArrayData<Dim> > valuesData() { return m_ValuesData; }
198 198 const std::shared_ptr<ArrayData<Dim> > valuesData() const { return m_ValuesData; }
199 199
200 200 /// @sa IDataSeries::valuesUnit()
201 201 Unit valuesUnit() const override { return m_ValuesUnit; }
202 202
203 203 int nbPoints() const override { return m_ValuesData->totalSize(); }
204 204
205 205 void clear()
206 206 {
207 207 m_XAxisData->clear();
208 208 m_ValuesData->clear();
209 209 }
210 210
211 211 bool isEmpty() const noexcept { return m_XAxisData->size() == 0; }
212 212
213 213 /// Merges into the data series an other data series.
214 214 ///
215 215 /// The two dataseries:
216 216 /// - must be of the same dimension
217 217 /// - must have the same y-axis (if defined)
218 218 ///
219 219 /// If the prerequisites are not valid, the method does nothing
220 220 ///
221 221 /// @remarks the data series to merge with is cleared after the operation
222 222 void merge(IDataSeries *dataSeries) override
223 223 {
224 224 dataSeries->lockWrite();
225 225 lockWrite();
226 226
227 227 if (auto other = dynamic_cast<DataSeries<Dim> *>(dataSeries)) {
228 228 if (m_YAxis == other->m_YAxis) {
229 229 DataSeriesMergeHelper::merge(*other, *this);
230 230 }
231 231 else {
232 232 qCWarning(LOG_DataSeries())
233 233 << QObject::tr("Can't merge data series that have not the same y-axis");
234 234 }
235 235 }
236 236 else {
237 237 qCWarning(LOG_DataSeries())
238 238 << QObject::tr("Detection of a type of IDataSeries we cannot merge with !");
239 239 }
240 240 unlock();
241 241 dataSeries->unlock();
242 242 }
243 243
244 244 void purge(double min, double max) override
245 245 {
246 246 // Nothing to purge if series is empty
247 247 if (isEmpty()) {
248 248 return;
249 249 }
250 250
251 251 if (min > max) {
252 252 std::swap(min, max);
253 253 }
254 254
255 255 // Nothing to purge if series min/max are inside purge range
256 256 auto xMin = cbegin()->x();
257 257 auto xMax = (--cend())->x();
258 258 if (xMin >= min && xMax <= max) {
259 259 return;
260 260 }
261 261
262 262 auto lowerIt = std::lower_bound(
263 263 begin(), end(), min, [](const auto &it, const auto &val) { return it.x() < val; });
264 264 erase(begin(), lowerIt);
265 265 auto upperIt = std::upper_bound(
266 266 begin(), end(), max, [](const auto &val, const auto &it) { return val < it.x(); });
267 267 erase(upperIt, end());
268 268 }
269 269
270 270 // ///////// //
271 271 // Iterators //
272 272 // ///////// //
273 273
274 274 DataSeriesIterator begin() override
275 275 {
276 276 return DataSeriesIterator{DataSeriesIteratorValue{
277 277 std::make_unique<dataseries_detail::IteratorValue<Dim, false> >(*this, true)}};
278 278 }
279 279
280 280 DataSeriesIterator end() override
281 281 {
282 282 return DataSeriesIterator{DataSeriesIteratorValue{
283 283 std::make_unique<dataseries_detail::IteratorValue<Dim, false> >(*this, false)}};
284 284 }
285 285
286 286 DataSeriesIterator cbegin() const override
287 287 {
288 288 return DataSeriesIterator{DataSeriesIteratorValue{
289 289 std::make_unique<dataseries_detail::IteratorValue<Dim, true> >(*this, true)}};
290 290 }
291 291
292 292 DataSeriesIterator cend() const override
293 293 {
294 294 return DataSeriesIterator{DataSeriesIteratorValue{
295 295 std::make_unique<dataseries_detail::IteratorValue<Dim, true> >(*this, false)}};
296 296 }
297 297
298 298 void erase(DataSeriesIterator first, DataSeriesIterator last)
299 299 {
300 300 auto firstImpl
301 301 = dynamic_cast<dataseries_detail::IteratorValue<Dim, false> *>(first->impl());
302 302 auto lastImpl = dynamic_cast<dataseries_detail::IteratorValue<Dim, false> *>(last->impl());
303 303
304 304 if (firstImpl && lastImpl) {
305 305 m_XAxisData->erase(firstImpl->m_XIt, lastImpl->m_XIt);
306 306 m_ValuesData->erase(firstImpl->m_ValuesIt, lastImpl->m_ValuesIt);
307 307 }
308 308 }
309 309
310 310 void insert(DataSeriesIterator first, DataSeriesIterator last, bool prepend = false)
311 311 {
312 312 auto firstImpl = dynamic_cast<dataseries_detail::IteratorValue<Dim, true> *>(first->impl());
313 313 auto lastImpl = dynamic_cast<dataseries_detail::IteratorValue<Dim, true> *>(last->impl());
314 314
315 315 if (firstImpl && lastImpl) {
316 316 m_XAxisData->insert(firstImpl->m_XIt, lastImpl->m_XIt, prepend);
317 317 m_ValuesData->insert(firstImpl->m_ValuesIt, lastImpl->m_ValuesIt, prepend);
318 318 }
319 319 }
320 320
321 321 /// @sa IDataSeries::minXAxisData()
322 322 DataSeriesIterator minXAxisData(double minXAxisData) const override
323 323 {
324 324 return std::lower_bound(
325 325 cbegin(), cend(), minXAxisData,
326 326 [](const auto &itValue, const auto &value) { return itValue.x() < value; });
327 327 }
328 328
329 329 /// @sa IDataSeries::maxXAxisData()
330 330 DataSeriesIterator maxXAxisData(double maxXAxisData) const override
331 331 {
332 332 // Gets the first element that greater than max value
333 333 auto it = std::upper_bound(
334 334 cbegin(), cend(), maxXAxisData,
335 335 [](const auto &value, const auto &itValue) { return value < itValue.x(); });
336 336
337 337 return it == cbegin() ? cend() : --it;
338 338 }
339 339
340 340 std::pair<DataSeriesIterator, DataSeriesIterator> xAxisRange(double minXAxisData,
341 341 double maxXAxisData) const override
342 342 {
343 343 if (minXAxisData > maxXAxisData) {
344 344 std::swap(minXAxisData, maxXAxisData);
345 345 }
346 346
347 347 auto begin = cbegin();
348 348 auto end = cend();
349 349
350 350 auto lowerIt = std::lower_bound(
351 351 begin, end, minXAxisData,
352 352 [](const auto &itValue, const auto &value) { return itValue.x() < value; });
353 353 auto upperIt = std::upper_bound(
354 354 lowerIt, end, maxXAxisData,
355 355 [](const auto &value, const auto &itValue) { return value < itValue.x(); });
356 356
357 357 return std::make_pair(lowerIt, upperIt);
358 358 }
359 359
360 360 std::pair<DataSeriesIterator, DataSeriesIterator>
361 361 valuesBounds(double minXAxisData, double maxXAxisData) const override
362 362 {
363 363 // Places iterators to the correct x-axis range
364 364 auto xAxisRangeIts = xAxisRange(minXAxisData, maxXAxisData);
365 365
366 366 // Returns end iterators if the range is empty
367 367 if (xAxisRangeIts.first == xAxisRangeIts.second) {
368 368 return std::make_pair(cend(), cend());
369 369 }
370 370
371 371 // Gets the iterator on the min of all values data
372 372 auto minIt = std::min_element(
373 373 xAxisRangeIts.first, xAxisRangeIts.second, [](const auto &it1, const auto &it2) {
374 374 return SortUtils::minCompareWithNaN(it1.minValue(), it2.minValue());
375 375 });
376 376
377 377 // Gets the iterator on the max of all values data
378 378 auto maxIt = std::max_element(
379 379 xAxisRangeIts.first, xAxisRangeIts.second, [](const auto &it1, const auto &it2) {
380 380 return SortUtils::maxCompareWithNaN(it1.maxValue(), it2.maxValue());
381 381 });
382 382
383 383 return std::make_pair(minIt, maxIt);
384 384 }
385 385
386 /// @return the y-axis associated to the data series
387 /// @todo pass getter as protected and use iterators to access the y-axis data
388 OptionalAxis yAxis() const { return m_YAxis; }
389
386 390 // /////// //
387 391 // Mutexes //
388 392 // /////// //
389 393
390 394 virtual void lockRead() { m_Lock.lockForRead(); }
391 395 virtual void lockWrite() { m_Lock.lockForWrite(); }
392 396 virtual void unlock() { m_Lock.unlock(); }
393 397
394 398 protected:
395 399 /// Protected ctor (DataSeries is abstract).
396 400 ///
397 401 /// Data vectors must be consistent with each other, otherwise an exception will be thrown (@sa
398 402 /// class description for consistent rules)
399 403 /// @remarks data series is automatically sorted on its x-axis data
400 404 /// @throws std::invalid_argument if the data are inconsistent with each other
401 405 explicit DataSeries(std::shared_ptr<ArrayData<1> > xAxisData, const Unit &xAxisUnit,
402 406 std::shared_ptr<ArrayData<Dim> > valuesData, const Unit &valuesUnit,
403 407 OptionalAxis yAxis = OptionalAxis{})
404 408 : m_XAxisData{xAxisData},
405 409 m_XAxisUnit{xAxisUnit},
406 410 m_ValuesData{valuesData},
407 411 m_ValuesUnit{valuesUnit},
408 412 m_YAxis{std::move(yAxis)}
409 413 {
410 414 if (m_XAxisData->size() != m_ValuesData->size()) {
411 415 throw std::invalid_argument{
412 416 "The number of values by component must be equal to the number of x-axis data"};
413 417 }
414 418
415 419 // Validates y-axis (if defined)
416 420 if (yAxis.isDefined() && (yAxis.size() != m_ValuesData->componentCount())) {
417 421 throw std::invalid_argument{
418 422 "As the y-axis is defined, the number of value components must be equal to the "
419 423 "number of y-axis data"};
420 424 }
421 425
422 426 // Sorts data if it's not the case
423 427 const auto &xAxisCData = m_XAxisData->cdata();
424 428 if (!std::is_sorted(xAxisCData.cbegin(), xAxisCData.cend())) {
425 429 sort();
426 430 }
427 431 }
428 432
429 433 /// Copy ctor
430 434 explicit DataSeries(const DataSeries<Dim> &other)
431 435 : m_XAxisData{std::make_shared<ArrayData<1> >(*other.m_XAxisData)},
432 436 m_XAxisUnit{other.m_XAxisUnit},
433 437 m_ValuesData{std::make_shared<ArrayData<Dim> >(*other.m_ValuesData)},
434 438 m_ValuesUnit{other.m_ValuesUnit},
435 439 m_YAxis{other.m_YAxis}
436 440 {
437 441 // Since a series is ordered from its construction and is always ordered, it is not
438 442 // necessary to call the sort method here ('other' is sorted)
439 443 }
440 444
441 /// @return the y-axis associated to the data series
442 OptionalAxis yAxis() const { return m_YAxis; }
443
444 445 /// Assignment operator
445 446 template <int D>
446 447 DataSeries &operator=(DataSeries<D> other)
447 448 {
448 449 std::swap(m_XAxisData, other.m_XAxisData);
449 450 std::swap(m_XAxisUnit, other.m_XAxisUnit);
450 451 std::swap(m_ValuesData, other.m_ValuesData);
451 452 std::swap(m_ValuesUnit, other.m_ValuesUnit);
452 453 std::swap(m_YAxis, other.m_YAxis);
453 454
454 455 return *this;
455 456 }
456 457
457 458 private:
458 459 /**
459 460 * Sorts data series on its x-axis data
460 461 */
461 462 void sort() noexcept
462 463 {
463 464 auto permutation = SortUtils::sortPermutation(*m_XAxisData, std::less<double>());
464 465 m_XAxisData = m_XAxisData->sort(permutation);
465 466 m_ValuesData = m_ValuesData->sort(permutation);
466 467 }
467 468
468 469 // x-axis
469 470 std::shared_ptr<ArrayData<1> > m_XAxisData;
470 471 Unit m_XAxisUnit;
471 472
472 473 // values
473 474 std::shared_ptr<ArrayData<Dim> > m_ValuesData;
474 475 Unit m_ValuesUnit;
475 476
476 477 // y-axis (optional)
477 478 OptionalAxis m_YAxis;
478 479
479 480 QReadWriteLock m_Lock;
480 481 };
481 482
482 483 #endif // SCIQLOP_DATASERIES_H
@@ -1,58 +1,62
1 1 #ifndef SCIQLOP_OPTIONALAXIS_H
2 2 #define SCIQLOP_OPTIONALAXIS_H
3 3
4 4 #include "CoreGlobal.h"
5 5 #include "Unit.h"
6 6
7 7 #include <memory>
8 8
9 9 template <int Dim>
10 10 class ArrayData;
11 11
12 12 /**
13 13 * @brief The OptionalAxis class defines an optional data axis for a series of data.
14 14 *
15 15 * An optional data axis is an axis that can be defined or not for a data series. If defined, it
16 16 * contains a unit and data (1-dim ArrayData). It is then possible to access the data or the unit.
17 17 * In the case of an undefined axis, the axis has no data and no unit. The methods for accessing the
18 18 * data or the unit are always callable but will return undefined values.
19 19 *
20 20 * @sa DataSeries
21 21 * @sa ArrayData
22 22 */
23 23 class SCIQLOP_CORE_EXPORT OptionalAxis {
24 24 public:
25 25 /// Ctor for an undefined axis
26 26 explicit OptionalAxis();
27 27 /// Ctor for a defined axis
28 28 /// @param data the axis' data
29 29 /// @param unit the axis' unit
30 30 /// @throws std::invalid_argument if no data is associated to the axis
31 31 explicit OptionalAxis(std::shared_ptr<ArrayData<1> > data, Unit unit);
32 32
33 33 /// Copy ctor
34 34 OptionalAxis(const OptionalAxis &other);
35 35 /// Assignment operator
36 36 OptionalAxis &operator=(OptionalAxis other);
37 37
38 38 /// @return the flag that indicates if the axis is defined or not
39 39 bool isDefined() const;
40 40
41 41 /// @return gets the data at the index passed in parameter, NaN if the index is outside the
42 42 /// bounds of the axis, or if the axis is undefined
43 43 double at(int index) const;
44
45 ///@return the min and max values of the data on the axis, NaN values if there is no data
46 std::pair<double, double> bounds() const;
47
44 48 /// @return the number of data on the axis, 0 if the axis is not defined
45 49 int size() const;
46 50 /// @return the unit of the axis, an empty unit if the axis is not defined
47 51 Unit unit() const;
48 52
49 53 bool operator==(const OptionalAxis &other);
50 54 bool operator!=(const OptionalAxis &other);
51 55
52 56 private:
53 57 bool m_Defined; ///< Axis is defined or not
54 58 std::shared_ptr<ArrayData<1> > m_Data; ///< Axis' data
55 59 Unit m_Unit; ///< Axis' unit
56 60 };
57 61
58 62 #endif // SCIQLOP_OPTIONALAXIS_H
@@ -1,74 +1,99
1 1 #include <Data/OptionalAxis.h>
2 2
3 3 #include "Data/ArrayData.h"
4 4
5 5 OptionalAxis::OptionalAxis() : m_Defined{false}, m_Data{nullptr}, m_Unit{}
6 6 {
7 7 }
8 8
9 9 OptionalAxis::OptionalAxis(std::shared_ptr<ArrayData<1> > data, Unit unit)
10 10 : m_Defined{true}, m_Data{data}, m_Unit{std::move(unit)}
11 11 {
12 12 if (m_Data == nullptr) {
13 13 throw std::invalid_argument{"Data can't be null for a defined axis"};
14 14 }
15 15 }
16 16
17 17 OptionalAxis::OptionalAxis(const OptionalAxis &other)
18 18 : m_Defined{other.m_Defined},
19 19 m_Data{other.m_Data ? std::make_shared<ArrayData<1> >(*other.m_Data) : nullptr},
20 20 m_Unit{other.m_Unit}
21 21 {
22 22 }
23 23
24 24 OptionalAxis &OptionalAxis::operator=(OptionalAxis other)
25 25 {
26 26 std::swap(m_Defined, other.m_Defined);
27 27 std::swap(m_Data, other.m_Data);
28 28 std::swap(m_Unit, other.m_Unit);
29 29 }
30 30
31 31 bool OptionalAxis::isDefined() const
32 32 {
33 33 return m_Defined;
34 34 }
35 35
36 36 double OptionalAxis::at(int index) const
37 37 {
38 38 if (m_Defined) {
39 39 return (index >= 0 && index < m_Data->size()) ? m_Data->at(index)
40 40 : std::numeric_limits<double>::quiet_NaN();
41 41 }
42 42 else {
43 43 return std::numeric_limits<double>::quiet_NaN();
44 44 }
45 45 }
46 46
47 std::pair<double, double> OptionalAxis::bounds() const
48 {
49 if (!m_Defined || m_Data->size() == 0) {
50 return std::make_pair(std::numeric_limits<double>::quiet_NaN(),
51 std::numeric_limits<double>::quiet_NaN());
52 }
53 else {
54
55 auto minIt = std::min_element(
56 m_Data->cbegin(), m_Data->cend(), [](const auto &it1, const auto &it2) {
57 return SortUtils::minCompareWithNaN(it1.first(), it2.first());
58 });
59
60 // Gets the iterator on the max of all values data
61 auto maxIt = std::max_element(
62 m_Data->cbegin(), m_Data->cend(), [](const auto &it1, const auto &it2) {
63 return SortUtils::maxCompareWithNaN(it1.first(), it2.first());
64 });
65
66 auto pair = std::make_pair(minIt->first(), maxIt->first());
67
68 return std::make_pair(minIt->first(), maxIt->first());
69 }
70 }
71
47 72 int OptionalAxis::size() const
48 73 {
49 74 return m_Defined ? m_Data->size() : 0;
50 75 }
51 76
52 77 Unit OptionalAxis::unit() const
53 78 {
54 79 return m_Defined ? m_Unit : Unit{};
55 80 }
56 81
57 82 bool OptionalAxis::operator==(const OptionalAxis &other)
58 83 {
59 84 // Axis not defined
60 85 if (!m_Defined) {
61 86 return !other.m_Defined;
62 87 }
63 88
64 89 // Axis defined
65 90 return m_Unit == other.m_Unit
66 91 && std::equal(
67 92 m_Data->cbegin(), m_Data->cend(), other.m_Data->cbegin(), other.m_Data->cend(),
68 93 [](const auto &it1, const auto &it2) { return it1.values() == it2.values(); });
69 94 }
70 95
71 96 bool OptionalAxis::operator!=(const OptionalAxis &other)
72 97 {
73 98 return !(*this == other);
74 99 }
@@ -1,199 +1,198
1 1 #include "Data/SpectrogramSeries.h"
2 2
3 3 #include "DataSeriesBuilders.h"
4 4 #include "DataSeriesUtils.h"
5 5
6 6 #include <QObject>
7 7 #include <QtTest>
8 8
9 9 namespace {
10 10
11 11 // Aliases used to facilitate reading of test inputs
12 12 using X = DataContainer;
13 13 using Y = DataContainer;
14 14 using Values = DataContainer;
15 15 using Components = std::vector<DataContainer>;
16 16
17 17 } // namespace
18 18
19 19 /**
20 20 * @brief The TestSpectrogramSeries class defines unit tests on spectrogram series.
21 21 *
22 22 * Most of these unit tests use generic tests defined for DataSeries (@sa DataSeriesUtils)
23 23 */
24 24 class TestSpectrogramSeries : public QObject {
25 25 Q_OBJECT
26 26 private slots:
27 27
28 28 /// Tests construction of a spectrogram series
29 29 void testCtor_data();
30 30 void testCtor();
31 31
32 32 /// Tests merge of two spectrogram series
33 33 void testMerge_data();
34 34 void testMerge();
35 35
36 /// @todo ALX: test subdataseries
37 36 /// Tests get subdata of a spectrogram series
38 37 void testSubDataSeries_data();
39 38 void testSubDataSeries();
40 39 };
41 40
42 41 void TestSpectrogramSeries::testCtor_data()
43 42 {
44 43 // x-axis data
45 44 QTest::addColumn<X>("xAxisData");
46 45 // y-axis data
47 46 QTest::addColumn<Y>("yAxisData");
48 47 // values data
49 48 QTest::addColumn<Values>("valuesData");
50 49
51 50 // construction expected to be valid
52 51 QTest::addColumn<bool>("expectOK");
53 52 // expected x-axis data (when construction is valid)
54 53 QTest::addColumn<X>("expectedXAxisData");
55 54 // expected components data (when construction is valid)
56 55 QTest::addColumn<Components>("expectedComponentsData");
57 56
58 57 QTest::newRow(
59 58 "invalidData (number of values by component aren't equal to the number of x-axis data)")
60 59 << X{1., 2., 3., 4., 5.} << Y{1., 2., 3.} << Values{1., 2., 3.} << false << X{}
61 60 << Components{};
62 61
63 62 QTest::newRow("invalidData (number of components aren't equal to the number of y-axis data)")
64 63 << X{1., 2., 3., 4., 5.} << Y{1., 2.} // 2 y-axis data
65 64 << Values{1., 2., 3., 4., 5.} // 1 component
66 65 << false << X{} << Components{};
67 66
68 67 QTest::newRow("sortedData") << X{1., 2., 3., 4., 5.} << Y{1., 2.} // 2 y-axis data
69 68 << Values{1., 2., 3., 4., 5., 6., 7., 8., 9., 10.} // 2 components
70 69 << true << X{1., 2., 3., 4., 5.}
71 70 << Components{{1., 3., 5., 7., 9.}, {2., 4., 6., 8., 10.}};
72 71
73 72 QTest::newRow("unsortedData") << X{5., 4., 3., 2., 1.} << Y{1., 2.}
74 73 << Values{1., 2., 3., 4., 5., 6., 7., 8., 9., 10.} << true
75 74 << X{1., 2., 3., 4., 5.}
76 75 << Components{{9., 7., 5., 3., 1.}, {10., 8., 6., 4., 2.}};
77 76 }
78 77
79 78 void TestSpectrogramSeries::testCtor()
80 79 {
81 80 // Creates series
82 81 QFETCH(X, xAxisData);
83 82 QFETCH(Y, yAxisData);
84 83 QFETCH(Values, valuesData);
85 84 QFETCH(bool, expectOK);
86 85
87 86 if (expectOK) {
88 87 auto series = SpectrogramBuilder{}
89 88 .setX(std::move(xAxisData))
90 89 .setY(std::move(yAxisData))
91 90 .setValues(std::move(valuesData))
92 91 .build();
93 92
94 93 // Validates results
95 94 QFETCH(X, expectedXAxisData);
96 95 QFETCH(Components, expectedComponentsData);
97 96 validateRange(series->cbegin(), series->cend(), expectedXAxisData, expectedComponentsData);
98 97 }
99 98 else {
100 99 QVERIFY_EXCEPTION_THROWN(SpectrogramBuilder{}
101 100 .setX(std::move(xAxisData))
102 101 .setY(std::move(yAxisData))
103 102 .setValues(std::move(valuesData))
104 103 .build(),
105 104 std::invalid_argument);
106 105 }
107 106 }
108 107
109 108 void TestSpectrogramSeries::testMerge_data()
110 109 {
111 110 testMerge_struct<SpectrogramSeries, Components>();
112 111
113 112 QTest::newRow("sortedMerge") << SpectrogramBuilder{}
114 113 .setX({1., 2., 3.})
115 114 .setY({1., 2.})
116 115 .setValues({10., 11., 20., 21., 30., 31})
117 116 .build()
118 117 << SpectrogramBuilder{}
119 118 .setX({4., 5., 6.})
120 119 .setY({1., 2.})
121 120 .setValues({40., 41., 50., 51., 60., 61})
122 121 .build()
123 122 << DataContainer{1., 2., 3., 4., 5., 6.}
124 123 << Components{{10., 20., 30., 40., 50., 60.},
125 124 {11., 21., 31., 41., 51., 61}};
126 125
127 126 QTest::newRow(
128 127 "unsortedMerge (merge not made because the two data series have different y-axes)")
129 128 << SpectrogramBuilder{}
130 129 .setX({4., 5., 6.})
131 130 .setY({1., 2.})
132 131 .setValues({40., 41., 50., 51., 60., 61})
133 132 .build()
134 133 << SpectrogramBuilder{}
135 134 .setX({1., 2., 3.})
136 135 .setY({3., 4.})
137 136 .setValues({10., 11., 20., 21., 30., 31})
138 137 .build()
139 138 << DataContainer{4., 5., 6.} << Components{{40., 50., 60.}, {41., 51., 61}};
140 139
141 140 QTest::newRow(
142 141 "unsortedMerge (unsortedMerge (merge is made because the two data series have the same "
143 142 "y-axis)")
144 143 << SpectrogramBuilder{}
145 144 .setX({4., 5., 6.})
146 145 .setY({1., 2.})
147 146 .setValues({40., 41., 50., 51., 60., 61})
148 147 .build()
149 148 << SpectrogramBuilder{}
150 149 .setX({1., 2., 3.})
151 150 .setY({1., 2.})
152 151 .setValues({10., 11., 20., 21., 30., 31})
153 152 .build()
154 153 << DataContainer{1., 2., 3., 4., 5., 6.}
155 154 << Components{{10., 20., 30., 40., 50., 60.}, {11., 21., 31., 41., 51., 61}};
156 155 }
157 156
158 157 void TestSpectrogramSeries::testMerge()
159 158 {
160 159 testMerge_t<SpectrogramSeries, Components>();
161 160 }
162 161
163 162 void TestSpectrogramSeries::testSubDataSeries_data()
164 163 {
165 164 testSubDataSeries_struct<SpectrogramSeries, Components>();
166 165
167 166 QTest::newRow("subDataSeries (the range includes all data)")
168 167 << SpectrogramBuilder{}
169 168 .setX({1., 2., 3.})
170 169 .setY({1., 2.})
171 170 .setValues({10., 11., 20., 21., 30., 31})
172 171 .build()
173 172 << SqpRange{0., 5.} << DataContainer{1., 2., 3.}
174 173 << Components{{10., 20., 30.}, {11., 21., 31.}};
175 174
176 175 QTest::newRow("subDataSeries (the range includes no data)")
177 176 << SpectrogramBuilder{}
178 177 .setX({1., 2., 3.})
179 178 .setY({1., 2.})
180 179 .setValues({10., 11., 20., 21., 30., 31})
181 180 .build()
182 181 << SqpRange{4., 5.} << DataContainer{} << Components{{}, {}};
183 182
184 183 QTest::newRow("subDataSeries (the range includes some data)")
185 184 << SpectrogramBuilder{}
186 185 .setX({1., 2., 3.})
187 186 .setY({1., 2.})
188 187 .setValues({10., 11., 20., 21., 30., 31})
189 188 .build()
190 189 << SqpRange{1.1, 3} << DataContainer{2., 3.} << Components{{20., 30.}, {21., 31.}};
191 190 }
192 191
193 192 void TestSpectrogramSeries::testSubDataSeries()
194 193 {
195 194 testSubDataSeries_t<SpectrogramSeries, Components>();
196 195 }
197 196
198 197 QTEST_MAIN(TestSpectrogramSeries)
199 198 #include "TestSpectrogramSeries.moc"
@@ -1,39 +1,41
1 1 #ifndef SCIQLOP_VISUALIZATIONGRAPHHELPER_H
2 2 #define SCIQLOP_VISUALIZATIONGRAPHHELPER_H
3 3
4 4 #include "Visualization/VisualizationDefs.h"
5 5
6 6 #include <Data/SqpRange.h>
7 7
8 8 #include <QLoggingCategory>
9 9 #include <QVector>
10 10
11 11 #include <memory>
12 12
13 13 Q_DECLARE_LOGGING_CATEGORY(LOG_VisualizationGraphHelper)
14 14
15 15 class IDataSeries;
16 16 class QCPAbstractPlottable;
17 17 class QCustomPlot;
18 18 class Variable;
19 19
20 20 /**
21 21 * @brief The VisualizationGraphHelper class aims to create the QCustomPlot components relative to a
22 22 * variable, depending on the data series of this variable
23 23 */
24 24 struct VisualizationGraphHelper {
25 25 /**
26 26 * Creates (if possible) the QCustomPlot components relative to the variable passed in
27 27 * parameter, and adds these to the plot passed in parameter.
28 28 * @param variable the variable for which to create the components
29 29 * @param plot the plot in which to add the created components. It takes ownership of these
30 30 * components.
31 31 * @return the list of the components created
32 32 */
33 33 static PlottablesMap create(std::shared_ptr<Variable> variable, QCustomPlot &plot) noexcept;
34 34
35 35 static void updateData(PlottablesMap &plottables, std::shared_ptr<IDataSeries> dataSeries,
36 36 const SqpRange &dateTime);
37
38 static void setYAxisRange(std::shared_ptr<Variable> variable, QCustomPlot &plot) noexcept;
37 39 };
38 40
39 41 #endif // SCIQLOP_VISUALIZATIONGRAPHHELPER_H
@@ -1,108 +1,109
1 1 #ifndef SCIQLOP_VISUALIZATIONGRAPHWIDGET_H
2 2 #define SCIQLOP_VISUALIZATIONGRAPHWIDGET_H
3 3
4 4 #include "Visualization/IVisualizationWidget.h"
5 5 #include "Visualization/VisualizationDragWidget.h"
6 6
7 7 #include <QLoggingCategory>
8 8 #include <QWidget>
9 9
10 10 #include <memory>
11 11
12 12 #include <Common/spimpl.h>
13 13
14 14 Q_DECLARE_LOGGING_CATEGORY(LOG_VisualizationGraphWidget)
15 15
16 16 class QCPRange;
17 17 class QCustomPlot;
18 18 class SqpRange;
19 19 class Variable;
20 20 class VisualizationZoneWidget;
21 21
22 22 namespace Ui {
23 23 class VisualizationGraphWidget;
24 24 } // namespace Ui
25 25
26 26 class VisualizationGraphWidget : public VisualizationDragWidget, public IVisualizationWidget {
27 27 Q_OBJECT
28 28
29 29 friend class QCustomPlotSynchronizer;
30 30 friend class VisualizationGraphRenderingDelegate;
31 31
32 32 public:
33 33 explicit VisualizationGraphWidget(const QString &name = {}, QWidget *parent = 0);
34 34 virtual ~VisualizationGraphWidget();
35 35
36 36 VisualizationZoneWidget *parentZoneWidget() const noexcept;
37 37
38 38 /// If acquisition isn't enable, requestDataLoading signal cannot be emit
39 39 void enableAcquisition(bool enable);
40 40
41 41 void addVariable(std::shared_ptr<Variable> variable, SqpRange range);
42 42
43 43 /// Removes a variable from the graph
44 44 void removeVariable(std::shared_ptr<Variable> variable) noexcept;
45 45
46 46 /// Returns the list of all variables used in the graph
47 47 QList<std::shared_ptr<Variable> > variables() const;
48 48
49 void setYRange(const SqpRange &range);
49 /// Sets the y-axis range based on the data of a variable
50 void setYRange(std::shared_ptr<Variable> variable);
50 51 SqpRange graphRange() const noexcept;
51 52 void setGraphRange(const SqpRange &range);
52 53
53 54 // IVisualizationWidget interface
54 55 void accept(IVisualizationWidgetVisitor *visitor) override;
55 56 bool canDrop(const Variable &variable) const override;
56 57 bool contains(const Variable &variable) const override;
57 58 QString name() const override;
58 59
59 60 // VisualisationDragWidget
60 61 QMimeData *mimeData() const override;
61 62 bool isDragAllowed() const override;
62 63 void highlightForMerge(bool highlighted) override;
63 64
64 65 signals:
65 66 void synchronize(const SqpRange &range, const SqpRange &oldRange);
66 67 void requestDataLoading(QVector<std::shared_ptr<Variable> > variable, const SqpRange &range,
67 68 bool synchronise);
68 69
69 70 /// Signal emitted when the variable is about to be removed from the graph
70 71 void variableAboutToBeRemoved(std::shared_ptr<Variable> var);
71 72 /// Signal emitted when the variable has been added to the graph
72 73 void variableAdded(std::shared_ptr<Variable> var);
73 74
74 75 protected:
75 76 void closeEvent(QCloseEvent *event) override;
76 77 void enterEvent(QEvent *event) override;
77 78 void leaveEvent(QEvent *event) override;
78 79
79 80 QCustomPlot &plot() noexcept;
80 81
81 82 private:
82 83 Ui::VisualizationGraphWidget *ui;
83 84
84 85 class VisualizationGraphWidgetPrivate;
85 86 spimpl::unique_impl_ptr<VisualizationGraphWidgetPrivate> impl;
86 87
87 88 private slots:
88 89 /// Slot called when right clicking on the graph (displays a menu)
89 90 void onGraphMenuRequested(const QPoint &pos) noexcept;
90 91
91 92 /// Rescale the X axe to range parameter
92 93 void onRangeChanged(const QCPRange &t1, const QCPRange &t2);
93 94
94 95 /// Slot called when a mouse move was made
95 96 void onMouseMove(QMouseEvent *event) noexcept;
96 97 /// Slot called when a mouse wheel was made, to perform some processing before the zoom is done
97 98 void onMouseWheel(QWheelEvent *event) noexcept;
98 99 /// Slot called when a mouse press was made, to activate the calibration of a graph
99 100 void onMousePress(QMouseEvent *event) noexcept;
100 101 /// Slot called when a mouse release was made, to deactivate the calibration of a graph
101 102 void onMouseRelease(QMouseEvent *event) noexcept;
102 103
103 104 void onDataCacheVariableUpdated();
104 105
105 106 void onUpdateVarDisplaying(std::shared_ptr<Variable> variable, const SqpRange &range);
106 107 };
107 108
108 109 #endif // SCIQLOP_VISUALIZATIONGRAPHWIDGET_H
@@ -1,204 +1,342
1 1 #include "Visualization/VisualizationGraphHelper.h"
2 2 #include "Visualization/qcustomplot.h"
3 3
4 4 #include <Common/ColorUtils.h>
5 5
6 6 #include <Data/ScalarSeries.h>
7 #include <Data/SpectrogramSeries.h>
7 8 #include <Data/VectorSeries.h>
8 9
9 10 #include <Variable/Variable.h>
10 11
11 12 Q_LOGGING_CATEGORY(LOG_VisualizationGraphHelper, "VisualizationGraphHelper")
12 13
13 14 namespace {
14 15
15 16 class SqpDataContainer : public QCPGraphDataContainer {
16 17 public:
17 18 void appendGraphData(const QCPGraphData &data) { mData.append(data); }
18 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 23 * them
23 24 * @tparam T the data series' type
24 25 * @remarks Default implementation can't create plottables
25 26 */
26 27 template <typename T, typename Enabled = void>
27 28 struct PlottablesCreator {
28 29 static PlottablesMap createPlottables(T &, QCustomPlot &)
29 30 {
30 31 qCCritical(LOG_DataSeries())
31 32 << QObject::tr("Can't create plottables: unmanaged data series type");
32 33 return {};
33 34 }
34 35 };
35 36
36 37 /**
37 38 * Specialization of PlottablesCreator for scalars and vectors
38 39 * @sa ScalarSeries
39 40 * @sa VectorSeries
40 41 */
41 42 template <typename T>
42 43 struct PlottablesCreator<T,
43 44 typename std::enable_if_t<std::is_base_of<ScalarSeries, T>::value
44 45 or std::is_base_of<VectorSeries, T>::value> > {
45 46 static PlottablesMap createPlottables(T &dataSeries, QCustomPlot &plot)
46 47 {
47 48 PlottablesMap result{};
48 49
49 50 // Gets the number of components of the data series
50 51 dataSeries.lockRead();
51 52 auto componentCount = dataSeries.valuesData()->componentCount();
52 53 dataSeries.unlock();
53 54
54 55 auto colors = ColorUtils::colors(Qt::blue, Qt::red, componentCount);
55 56
56 57 // For each component of the data series, creates a QCPGraph to add to the plot
57 58 for (auto i = 0; i < componentCount; ++i) {
58 59 auto graph = plot.addGraph();
59 60 graph->setPen(QPen{colors.at(i)});
60 61
61 62 result.insert({i, graph});
62 63 }
63 64
64 65 plot.replot();
65 66
66 67 return result;
67 68 }
68 69 };
69 70
70 71 /**
72 * Specialization of PlottablesCreator for spectrograms
73 * @sa SpectrogramSeries
74 */
75 template <typename T>
76 struct PlottablesCreator<T,
77 typename std::enable_if_t<std::is_base_of<SpectrogramSeries, T>::value> > {
78 static PlottablesMap createPlottables(T &dataSeries, QCustomPlot &plot)
79 {
80 PlottablesMap result{};
81 result.insert({0, new QCPColorMap{plot.xAxis, plot.yAxis}});
82
83 plot.replot();
84
85 return result;
86 }
87 };
88
89 /**
71 90 * Struct used to update plottables, depending on the type of the data series from which to update
72 91 * them
73 92 * @tparam T the data series' type
74 93 * @remarks Default implementation can't update plottables
75 94 */
76 95 template <typename T, typename Enabled = void>
77 96 struct PlottablesUpdater {
97 static void setPlotYAxisRange(T &, const SqpRange &, QCustomPlot &)
98 {
99 qCCritical(LOG_VisualizationGraphHelper())
100 << QObject::tr("Can't set plot y-axis range: unmanaged data series type");
101 }
102
78 103 static void updatePlottables(T &, PlottablesMap &, const SqpRange &, bool)
79 104 {
80 qCCritical(LOG_DataSeries())
105 qCCritical(LOG_VisualizationGraphHelper())
81 106 << QObject::tr("Can't update plottables: unmanaged data series type");
82 107 }
83 108 };
84 109
85 110 /**
86 111 * Specialization of PlottablesUpdater for scalars and vectors
87 112 * @sa ScalarSeries
88 113 * @sa VectorSeries
89 114 */
90 115 template <typename T>
91 116 struct PlottablesUpdater<T,
92 117 typename std::enable_if_t<std::is_base_of<ScalarSeries, T>::value
93 118 or std::is_base_of<VectorSeries, T>::value> > {
119 static void setPlotYAxisRange(T &dataSeries, const SqpRange &xAxisRange, QCustomPlot &plot)
120 {
121 auto minValue = 0., maxValue = 0.;
122
123 dataSeries.lockRead();
124 auto valuesBounds = dataSeries.valuesBounds(xAxisRange.m_TStart, xAxisRange.m_TEnd);
125 auto end = dataSeries.cend();
126 if (valuesBounds.first != end && valuesBounds.second != end) {
127 auto rangeValue = [](const auto &value) { return std::isnan(value) ? 0. : value; };
128
129 minValue = rangeValue(valuesBounds.first->minValue());
130 maxValue = rangeValue(valuesBounds.second->maxValue());
131 }
132 dataSeries.unlock();
133
134 plot.yAxis->setRange(QCPRange{minValue, maxValue});
135 }
136
94 137 static void updatePlottables(T &dataSeries, PlottablesMap &plottables, const SqpRange &range,
95 138 bool rescaleAxes)
96 139 {
97 140
98 141 // For each plottable to update, resets its data
99 142 std::map<int, QSharedPointer<SqpDataContainer> > dataContainers{};
100 143 for (const auto &plottable : plottables) {
101 144 if (auto graph = dynamic_cast<QCPGraph *>(plottable.second)) {
102 145 auto dataContainer = QSharedPointer<SqpDataContainer>::create();
103 146 graph->setData(dataContainer);
104 147
105 148 dataContainers.insert({plottable.first, dataContainer});
106 149 }
107 150 }
108 151 dataSeries.lockRead();
109 152
110 153 // - Gets the data of the series included in the current range
111 154 // - Updates each plottable by adding, for each data item, a point that takes x-axis data
112 155 // and value data. The correct value is retrieved according to the index of the component
113 156 auto subDataIts = dataSeries.xAxisRange(range.m_TStart, range.m_TEnd);
114 157 for (auto it = subDataIts.first; it != subDataIts.second; ++it) {
115 158 for (const auto &dataContainer : dataContainers) {
116 159 auto componentIndex = dataContainer.first;
117 160 dataContainer.second->appendGraphData(
118 161 QCPGraphData(it->x(), it->value(componentIndex)));
119 162 }
120 163 }
121 164
122 165 dataSeries.unlock();
123 166
124 167 if (!plottables.empty()) {
125 168 auto plot = plottables.begin()->second->parentPlot();
126 169
127 170 if (rescaleAxes) {
128 171 plot->rescaleAxes();
129 172 }
130 173
131 174 plot->replot();
132 175 }
133 176 }
134 177 };
135 178
136 179 /**
180 * Specialization of PlottablesUpdater for spectrograms
181 * @sa SpectrogramSeries
182 */
183 template <typename T>
184 struct PlottablesUpdater<T,
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
197 static void updatePlottables(T &dataSeries, PlottablesMap &plottables, const SqpRange &range,
198 bool rescaleAxes)
199 {
200 if (plottables.empty()) {
201 qCDebug(LOG_VisualizationGraphHelper())
202 << QObject::tr("Can't update spectrogram: no colormap has been associated");
203 return;
204 }
205
206 // Gets the colormap to update (normally there is only one colormap)
207 Q_ASSERT(plottables.size() == 1);
208 auto colormap = dynamic_cast<QCPColorMap *>(plottables.at(0));
209 Q_ASSERT(colormap != nullptr);
210
211 dataSeries.lockRead();
212
213 auto its = dataSeries.xAxisRange(range.m_TStart, range.m_TEnd);
214 /// @todo ALX: use iterators here
215 auto yAxis = dataSeries.yAxis();
216
217 // Gets properties of x-axis and y-axis to set size and range of the colormap
218 auto nbX = std::distance(its.first, its.second);
219 auto xMin = nbX != 0 ? its.first->x() : 0.;
220 auto xMax = nbX != 0 ? (its.second - 1)->x() : 0.;
221
222 auto nbY = yAxis.size();
223 auto yMin = 0., yMax = 0.;
224 if (nbY != 0) {
225 std::tie(yMin, yMax) = yAxis.bounds();
226 }
227
228 colormap->data()->setSize(nbX, nbY);
229 colormap->data()->setRange(QCPRange{xMin, xMax}, QCPRange{yMin, yMax});
230
231 // Sets values
232 auto xIndex = 0;
233 for (auto it = its.first; it != its.second; ++it, ++xIndex) {
234 for (auto yIndex = 0; yIndex < nbY; ++yIndex) {
235 colormap->data()->setCell(xIndex, yIndex, it->value(yIndex));
236 }
237 }
238
239 dataSeries.unlock();
240
241 // Rescales axes
242 auto plot = colormap->parentPlot();
243
244 if (rescaleAxes) {
245 plot->rescaleAxes();
246 }
247
248 plot->replot();
249 }
250 };
251
252 /**
137 253 * Helper used to create/update plottables
138 254 */
139 255 struct IPlottablesHelper {
140 256 virtual ~IPlottablesHelper() noexcept = default;
141 257 virtual PlottablesMap create(QCustomPlot &plot) const = 0;
258 virtual void setYAxisRange(const SqpRange &xAxisRange, QCustomPlot &plot) const = 0;
142 259 virtual void update(PlottablesMap &plottables, const SqpRange &range,
143 260 bool rescaleAxes = false) const = 0;
144 261 };
145 262
146 263 /**
147 264 * Default implementation of IPlottablesHelper, which takes data series to create/update plottables
148 265 * @tparam T the data series' type
149 266 */
150 267 template <typename T>
151 268 struct PlottablesHelper : public IPlottablesHelper {
152 269 explicit PlottablesHelper(T &dataSeries) : m_DataSeries{dataSeries} {}
153 270
154 271 PlottablesMap create(QCustomPlot &plot) const override
155 272 {
156 273 return PlottablesCreator<T>::createPlottables(m_DataSeries, plot);
157 274 }
158 275
159 276 void update(PlottablesMap &plottables, const SqpRange &range, bool rescaleAxes) const override
160 277 {
161 278 PlottablesUpdater<T>::updatePlottables(m_DataSeries, plottables, range, rescaleAxes);
162 279 }
163 280
281 void setYAxisRange(const SqpRange &xAxisRange, QCustomPlot &plot) const override
282 {
283 return PlottablesUpdater<T>::setPlotYAxisRange(m_DataSeries, xAxisRange, plot);
284 }
285
164 286 T &m_DataSeries;
165 287 };
166 288
167 289 /// Creates IPlottablesHelper according to a data series
168 290 std::unique_ptr<IPlottablesHelper> createHelper(std::shared_ptr<IDataSeries> dataSeries) noexcept
169 291 {
170 292 if (auto scalarSeries = std::dynamic_pointer_cast<ScalarSeries>(dataSeries)) {
171 293 return std::make_unique<PlottablesHelper<ScalarSeries> >(*scalarSeries);
172 294 }
295 else if (auto spectrogramSeries = std::dynamic_pointer_cast<SpectrogramSeries>(dataSeries)) {
296 return std::make_unique<PlottablesHelper<SpectrogramSeries> >(*spectrogramSeries);
297 }
173 298 else if (auto vectorSeries = std::dynamic_pointer_cast<VectorSeries>(dataSeries)) {
174 299 return std::make_unique<PlottablesHelper<VectorSeries> >(*vectorSeries);
175 300 }
176 301 else {
177 302 return std::make_unique<PlottablesHelper<IDataSeries> >(*dataSeries);
178 303 }
179 304 }
180 305
181 306 } // namespace
182 307
183 308 PlottablesMap VisualizationGraphHelper::create(std::shared_ptr<Variable> variable,
184 309 QCustomPlot &plot) noexcept
185 310 {
186 311 if (variable) {
187 312 auto helper = createHelper(variable->dataSeries());
188 313 auto plottables = helper->create(plot);
189 314 return plottables;
190 315 }
191 316 else {
192 317 qCDebug(LOG_VisualizationGraphHelper())
193 318 << QObject::tr("Can't create graph plottables : the variable is null");
194 319 return PlottablesMap{};
195 320 }
196 321 }
197 322
323 void VisualizationGraphHelper::setYAxisRange(std::shared_ptr<Variable> variable,
324 QCustomPlot &plot) noexcept
325 {
326 if (variable) {
327 auto helper = createHelper(variable->dataSeries());
328 helper->setYAxisRange(variable->range(), plot);
329 }
330 else {
331 qCDebug(LOG_VisualizationGraphHelper())
332 << QObject::tr("Can't set y-axis range of plot: the variable is null");
333 }
334 }
335
198 336 void VisualizationGraphHelper::updateData(PlottablesMap &plottables,
199 337 std::shared_ptr<IDataSeries> dataSeries,
200 338 const SqpRange &dateTime)
201 339 {
202 340 auto helper = createHelper(dataSeries);
203 341 helper->update(plottables, dateTime);
204 342 }
@@ -1,401 +1,405
1 1 #include "Visualization/VisualizationGraphWidget.h"
2 2 #include "Visualization/IVisualizationWidgetVisitor.h"
3 3 #include "Visualization/VisualizationDefs.h"
4 4 #include "Visualization/VisualizationGraphHelper.h"
5 5 #include "Visualization/VisualizationGraphRenderingDelegate.h"
6 6 #include "Visualization/VisualizationZoneWidget.h"
7 7 #include "ui_VisualizationGraphWidget.h"
8 8
9 9 #include <Common/MimeTypesDef.h>
10 10 #include <Data/ArrayData.h>
11 11 #include <Data/IDataSeries.h>
12 12 #include <DragAndDrop/DragDropHelper.h>
13 13 #include <Settings/SqpSettingsDefs.h>
14 14 #include <SqpApplication.h>
15 15 #include <Time/TimeController.h>
16 16 #include <Variable/Variable.h>
17 17 #include <Variable/VariableController.h>
18 18
19 19 #include <unordered_map>
20 20
21 21 Q_LOGGING_CATEGORY(LOG_VisualizationGraphWidget, "VisualizationGraphWidget")
22 22
23 23 namespace {
24 24
25 25 /// Key pressed to enable zoom on horizontal axis
26 26 const auto HORIZONTAL_ZOOM_MODIFIER = Qt::NoModifier;
27 27
28 28 /// Key pressed to enable zoom on vertical axis
29 29 const auto VERTICAL_ZOOM_MODIFIER = Qt::ControlModifier;
30 30
31 31 } // namespace
32 32
33 33 struct VisualizationGraphWidget::VisualizationGraphWidgetPrivate {
34 34
35 35 explicit VisualizationGraphWidgetPrivate(const QString &name)
36 36 : m_Name{name},
37 37 m_DoAcquisition{true},
38 38 m_IsCalibration{false},
39 39 m_RenderingDelegate{nullptr}
40 40 {
41 41 }
42 42
43 43 QString m_Name;
44 44 // 1 variable -> n qcpplot
45 45 std::map<std::shared_ptr<Variable>, PlottablesMap> m_VariableToPlotMultiMap;
46 46 bool m_DoAcquisition;
47 47 bool m_IsCalibration;
48 QCPItemTracer *m_TextTracer;
49 48 /// Delegate used to attach rendering features to the plot
50 49 std::unique_ptr<VisualizationGraphRenderingDelegate> m_RenderingDelegate;
51 50 };
52 51
53 52 VisualizationGraphWidget::VisualizationGraphWidget(const QString &name, QWidget *parent)
54 53 : VisualizationDragWidget{parent},
55 54 ui{new Ui::VisualizationGraphWidget},
56 55 impl{spimpl::make_unique_impl<VisualizationGraphWidgetPrivate>(name)}
57 56 {
58 57 ui->setupUi(this);
59 58
60 59 // 'Close' options : widget is deleted when closed
61 60 setAttribute(Qt::WA_DeleteOnClose);
62 61
63 62 // Set qcpplot properties :
64 63 // - Drag (on x-axis) and zoom are enabled
65 64 // - Mouse wheel on qcpplot is intercepted to determine the zoom orientation
66 65 ui->widget->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectItems);
67 66 ui->widget->axisRect()->setRangeDrag(Qt::Horizontal);
68 67
69 68 // The delegate must be initialized after the ui as it uses the plot
70 69 impl->m_RenderingDelegate = std::make_unique<VisualizationGraphRenderingDelegate>(*this);
71 70
72 71 connect(ui->widget, &QCustomPlot::mousePress, this, &VisualizationGraphWidget::onMousePress);
73 72 connect(ui->widget, &QCustomPlot::mouseRelease, this,
74 73 &VisualizationGraphWidget::onMouseRelease);
75 74 connect(ui->widget, &QCustomPlot::mouseMove, this, &VisualizationGraphWidget::onMouseMove);
76 75 connect(ui->widget, &QCustomPlot::mouseWheel, this, &VisualizationGraphWidget::onMouseWheel);
77 76 connect(ui->widget->xAxis, static_cast<void (QCPAxis::*)(const QCPRange &, const QCPRange &)>(
78 77 &QCPAxis::rangeChanged),
79 78 this, &VisualizationGraphWidget::onRangeChanged, Qt::DirectConnection);
80 79
81 80 // Activates menu when right clicking on the graph
82 81 ui->widget->setContextMenuPolicy(Qt::CustomContextMenu);
83 82 connect(ui->widget, &QCustomPlot::customContextMenuRequested, this,
84 83 &VisualizationGraphWidget::onGraphMenuRequested);
85 84
86 85 connect(this, &VisualizationGraphWidget::requestDataLoading, &sqpApp->variableController(),
87 86 &VariableController::onRequestDataLoading);
88 87
89 88 connect(&sqpApp->variableController(), &VariableController::updateVarDisplaying, this,
90 89 &VisualizationGraphWidget::onUpdateVarDisplaying);
91 90 }
92 91
93 92
94 93 VisualizationGraphWidget::~VisualizationGraphWidget()
95 94 {
96 95 delete ui;
97 96 }
98 97
99 98 VisualizationZoneWidget *VisualizationGraphWidget::parentZoneWidget() const noexcept
100 99 {
101 100 auto parent = parentWidget();
102 101 while (parent != nullptr && !qobject_cast<VisualizationZoneWidget *>(parent)) {
103 102 parent = parent->parentWidget();
104 103 }
105 104
106 105 return qobject_cast<VisualizationZoneWidget *>(parent);
107 106 }
108 107
109 108 void VisualizationGraphWidget::enableAcquisition(bool enable)
110 109 {
111 110 impl->m_DoAcquisition = enable;
112 111 }
113 112
114 113 void VisualizationGraphWidget::addVariable(std::shared_ptr<Variable> variable, SqpRange range)
115 114 {
116 115 // Uses delegate to create the qcpplot components according to the variable
117 116 auto createdPlottables = VisualizationGraphHelper::create(variable, *ui->widget);
118 117 impl->m_VariableToPlotMultiMap.insert({variable, std::move(createdPlottables)});
119 118
120 119 // Set axes properties according to the units of the data series
121 120 /// @todo : for the moment, no control is performed on the axes: the units and the tickers
122 121 /// are fixed for the default x-axis and y-axis of the plot, and according to the new graph
123 122 auto xAxisUnit = Unit{};
124 123 auto valuesUnit = Unit{};
125 124
126 125 if (auto dataSeries = variable->dataSeries()) {
127 126 dataSeries->lockRead();
128 127 xAxisUnit = dataSeries->xAxisUnit();
129 128 valuesUnit = dataSeries->valuesUnit();
130 129 dataSeries->unlock();
131 130 }
132 131 impl->m_RenderingDelegate->setAxesProperties(xAxisUnit, valuesUnit);
133 132
134 133 connect(variable.get(), SIGNAL(updated()), this, SLOT(onDataCacheVariableUpdated()));
135 134
136 135 this->enableAcquisition(false);
137 136 this->setGraphRange(range);
138 137 this->enableAcquisition(true);
139 138
140 139 emit requestDataLoading(QVector<std::shared_ptr<Variable> >() << variable, range, false);
141 140
142 141 emit variableAdded(variable);
143 142 }
144 143
145 144 void VisualizationGraphWidget::removeVariable(std::shared_ptr<Variable> variable) noexcept
146 145 {
147 146 // Each component associated to the variable :
148 147 // - is removed from qcpplot (which deletes it)
149 148 // - is no longer referenced in the map
150 149 auto variableIt = impl->m_VariableToPlotMultiMap.find(variable);
151 150 if (variableIt != impl->m_VariableToPlotMultiMap.cend()) {
152 151 emit variableAboutToBeRemoved(variable);
153 152
154 153 auto &plottablesMap = variableIt->second;
155 154
156 155 for (auto plottableIt = plottablesMap.cbegin(), plottableEnd = plottablesMap.cend();
157 156 plottableIt != plottableEnd;) {
158 157 ui->widget->removePlottable(plottableIt->second);
159 158 plottableIt = plottablesMap.erase(plottableIt);
160 159 }
161 160
162 161 impl->m_VariableToPlotMultiMap.erase(variableIt);
163 162 }
164 163
165 164 // Updates graph
166 165 ui->widget->replot();
167 166 }
168 167
169 168 QList<std::shared_ptr<Variable> > VisualizationGraphWidget::variables() const
170 169 {
171 170 auto variables = QList<std::shared_ptr<Variable> >{};
172 171 for (auto it = std::cbegin(impl->m_VariableToPlotMultiMap);
173 172 it != std::cend(impl->m_VariableToPlotMultiMap); ++it) {
174 173 variables << it->first;
175 174 }
176 175
177 176 return variables;
178 177 }
179 178
180 void VisualizationGraphWidget::setYRange(const SqpRange &range)
179 void VisualizationGraphWidget::setYRange(std::shared_ptr<Variable> variable)
181 180 {
182 ui->widget->yAxis->setRange(range.m_TStart, range.m_TEnd);
181 if (!variable) {
182 qCCritical(LOG_VisualizationGraphWidget()) << "Can't set y-axis range: variable is null";
183 return;
184 }
185
186 VisualizationGraphHelper::setYAxisRange(variable, *ui->widget);
183 187 }
184 188
185 189 SqpRange VisualizationGraphWidget::graphRange() const noexcept
186 190 {
187 191 auto graphRange = ui->widget->xAxis->range();
188 192 return SqpRange{graphRange.lower, graphRange.upper};
189 193 }
190 194
191 195 void VisualizationGraphWidget::setGraphRange(const SqpRange &range)
192 196 {
193 197 qCDebug(LOG_VisualizationGraphWidget()) << tr("VisualizationGraphWidget::setGraphRange START");
194 198 ui->widget->xAxis->setRange(range.m_TStart, range.m_TEnd);
195 199 ui->widget->replot();
196 200 qCDebug(LOG_VisualizationGraphWidget()) << tr("VisualizationGraphWidget::setGraphRange END");
197 201 }
198 202
199 203 void VisualizationGraphWidget::accept(IVisualizationWidgetVisitor *visitor)
200 204 {
201 205 if (visitor) {
202 206 visitor->visit(this);
203 207 }
204 208 else {
205 209 qCCritical(LOG_VisualizationGraphWidget())
206 210 << tr("Can't visit widget : the visitor is null");
207 211 }
208 212 }
209 213
210 214 bool VisualizationGraphWidget::canDrop(const Variable &variable) const
211 215 {
212 216 /// @todo : for the moment, a graph can always accomodate a variable
213 217 Q_UNUSED(variable);
214 218 return true;
215 219 }
216 220
217 221 bool VisualizationGraphWidget::contains(const Variable &variable) const
218 222 {
219 223 // Finds the variable among the keys of the map
220 224 auto variablePtr = &variable;
221 225 auto findVariable
222 226 = [variablePtr](const auto &entry) { return variablePtr == entry.first.get(); };
223 227
224 228 auto end = impl->m_VariableToPlotMultiMap.cend();
225 229 auto it = std::find_if(impl->m_VariableToPlotMultiMap.cbegin(), end, findVariable);
226 230 return it != end;
227 231 }
228 232
229 233 QString VisualizationGraphWidget::name() const
230 234 {
231 235 return impl->m_Name;
232 236 }
233 237
234 238 QMimeData *VisualizationGraphWidget::mimeData() const
235 239 {
236 240 auto mimeData = new QMimeData;
237 241 mimeData->setData(MIME_TYPE_GRAPH, QByteArray{});
238 242
239 243 auto timeRangeData = TimeController::mimeDataForTimeRange(graphRange());
240 244 mimeData->setData(MIME_TYPE_TIME_RANGE, timeRangeData);
241 245
242 246 return mimeData;
243 247 }
244 248
245 249 bool VisualizationGraphWidget::isDragAllowed() const
246 250 {
247 251 return true;
248 252 }
249 253
250 254 void VisualizationGraphWidget::highlightForMerge(bool highlighted)
251 255 {
252 256 if (highlighted) {
253 257 plot().setBackground(QBrush(QColor("#BBD5EE")));
254 258 }
255 259 else {
256 260 plot().setBackground(QBrush(Qt::white));
257 261 }
258 262
259 263 plot().update();
260 264 }
261 265
262 266 void VisualizationGraphWidget::closeEvent(QCloseEvent *event)
263 267 {
264 268 Q_UNUSED(event);
265 269
266 270 // Prevents that all variables will be removed from graph when it will be closed
267 271 for (auto &variableEntry : impl->m_VariableToPlotMultiMap) {
268 272 emit variableAboutToBeRemoved(variableEntry.first);
269 273 }
270 274 }
271 275
272 276 void VisualizationGraphWidget::enterEvent(QEvent *event)
273 277 {
274 278 Q_UNUSED(event);
275 279 impl->m_RenderingDelegate->showGraphOverlay(true);
276 280 }
277 281
278 282 void VisualizationGraphWidget::leaveEvent(QEvent *event)
279 283 {
280 284 Q_UNUSED(event);
281 285 impl->m_RenderingDelegate->showGraphOverlay(false);
282 286 }
283 287
284 288 QCustomPlot &VisualizationGraphWidget::plot() noexcept
285 289 {
286 290 return *ui->widget;
287 291 }
288 292
289 293 void VisualizationGraphWidget::onGraphMenuRequested(const QPoint &pos) noexcept
290 294 {
291 295 QMenu graphMenu{};
292 296
293 297 // Iterates on variables (unique keys)
294 298 for (auto it = impl->m_VariableToPlotMultiMap.cbegin(),
295 299 end = impl->m_VariableToPlotMultiMap.cend();
296 300 it != end; it = impl->m_VariableToPlotMultiMap.upper_bound(it->first)) {
297 301 // 'Remove variable' action
298 302 graphMenu.addAction(tr("Remove variable %1").arg(it->first->name()),
299 303 [ this, var = it->first ]() { removeVariable(var); });
300 304 }
301 305
302 306 if (!graphMenu.isEmpty()) {
303 307 graphMenu.exec(QCursor::pos());
304 308 }
305 309 }
306 310
307 311 void VisualizationGraphWidget::onRangeChanged(const QCPRange &t1, const QCPRange &t2)
308 312 {
309 313 qCDebug(LOG_VisualizationGraphWidget()) << tr("TORM: VisualizationGraphWidget::onRangeChanged")
310 314 << QThread::currentThread()->objectName() << "DoAcqui"
311 315 << impl->m_DoAcquisition;
312 316
313 317 auto graphRange = SqpRange{t1.lower, t1.upper};
314 318 auto oldGraphRange = SqpRange{t2.lower, t2.upper};
315 319
316 320 if (impl->m_DoAcquisition) {
317 321 QVector<std::shared_ptr<Variable> > variableUnderGraphVector;
318 322
319 323 for (auto it = impl->m_VariableToPlotMultiMap.begin(),
320 324 end = impl->m_VariableToPlotMultiMap.end();
321 325 it != end; it = impl->m_VariableToPlotMultiMap.upper_bound(it->first)) {
322 326 variableUnderGraphVector.push_back(it->first);
323 327 }
324 328 emit requestDataLoading(std::move(variableUnderGraphVector), graphRange,
325 329 !impl->m_IsCalibration);
326 330
327 331 if (!impl->m_IsCalibration) {
328 332 qCDebug(LOG_VisualizationGraphWidget())
329 333 << tr("TORM: VisualizationGraphWidget::Synchronize notify !!")
330 334 << QThread::currentThread()->objectName() << graphRange << oldGraphRange;
331 335 emit synchronize(graphRange, oldGraphRange);
332 336 }
333 337 }
334 338 }
335 339
336 340 void VisualizationGraphWidget::onMouseMove(QMouseEvent *event) noexcept
337 341 {
338 342 // Handles plot rendering when mouse is moving
339 343 impl->m_RenderingDelegate->onMouseMove(event);
340 344
341 345 VisualizationDragWidget::mouseMoveEvent(event);
342 346 }
343 347
344 348 void VisualizationGraphWidget::onMouseWheel(QWheelEvent *event) noexcept
345 349 {
346 350 auto zoomOrientations = QFlags<Qt::Orientation>{};
347 351
348 352 // Lambda that enables a zoom orientation if the key modifier related to this orientation
349 353 // has
350 354 // been pressed
351 355 auto enableOrientation
352 356 = [&zoomOrientations, event](const auto &orientation, const auto &modifier) {
353 357 auto orientationEnabled = event->modifiers().testFlag(modifier);
354 358 zoomOrientations.setFlag(orientation, orientationEnabled);
355 359 };
356 360 enableOrientation(Qt::Vertical, VERTICAL_ZOOM_MODIFIER);
357 361 enableOrientation(Qt::Horizontal, HORIZONTAL_ZOOM_MODIFIER);
358 362
359 363 ui->widget->axisRect()->setRangeZoom(zoomOrientations);
360 364 }
361 365
362 366 void VisualizationGraphWidget::onMousePress(QMouseEvent *event) noexcept
363 367 {
364 368 impl->m_IsCalibration = event->modifiers().testFlag(Qt::ControlModifier);
365 369
366 370 plot().setInteraction(QCP::iRangeDrag, !event->modifiers().testFlag(Qt::AltModifier));
367 371
368 372 VisualizationDragWidget::mousePressEvent(event);
369 373 }
370 374
371 375 void VisualizationGraphWidget::onMouseRelease(QMouseEvent *event) noexcept
372 376 {
373 377 impl->m_IsCalibration = false;
374 378 }
375 379
376 380 void VisualizationGraphWidget::onDataCacheVariableUpdated()
377 381 {
378 382 auto graphRange = ui->widget->xAxis->range();
379 383 auto dateTime = SqpRange{graphRange.lower, graphRange.upper};
380 384
381 385 for (auto &variableEntry : impl->m_VariableToPlotMultiMap) {
382 386 auto variable = variableEntry.first;
383 387 qCDebug(LOG_VisualizationGraphWidget())
384 388 << "TORM: VisualizationGraphWidget::onDataCacheVariableUpdated S" << variable->range();
385 389 qCDebug(LOG_VisualizationGraphWidget())
386 390 << "TORM: VisualizationGraphWidget::onDataCacheVariableUpdated E" << dateTime;
387 391 if (dateTime.contains(variable->range()) || dateTime.intersect(variable->range())) {
388 392 VisualizationGraphHelper::updateData(variableEntry.second, variable->dataSeries(),
389 393 variable->range());
390 394 }
391 395 }
392 396 }
393 397
394 398 void VisualizationGraphWidget::onUpdateVarDisplaying(std::shared_ptr<Variable> variable,
395 399 const SqpRange &range)
396 400 {
397 401 auto it = impl->m_VariableToPlotMultiMap.find(variable);
398 402 if (it != impl->m_VariableToPlotMultiMap.end()) {
399 403 VisualizationGraphHelper::updateData(it->second, variable->dataSeries(), range);
400 404 }
401 405 }
@@ -1,516 +1,500
1 1 #include "Visualization/VisualizationZoneWidget.h"
2 2
3 3 #include "Visualization/IVisualizationWidgetVisitor.h"
4 4 #include "Visualization/QCustomPlotSynchronizer.h"
5 5 #include "Visualization/VisualizationGraphWidget.h"
6 6 #include "Visualization/VisualizationWidget.h"
7 7 #include "ui_VisualizationZoneWidget.h"
8 8
9 9 #include "Common/MimeTypesDef.h"
10 10 #include "Common/VisualizationDef.h"
11 11
12 12 #include <Data/SqpRange.h>
13 13 #include <Time/TimeController.h>
14 14 #include <Variable/Variable.h>
15 15 #include <Variable/VariableController.h>
16 16
17 17 #include <Visualization/operations/FindVariableOperation.h>
18 18
19 19 #include <DragAndDrop/DragDropHelper.h>
20 20 #include <QUuid>
21 21 #include <SqpApplication.h>
22 22 #include <cmath>
23 23
24 24 #include <QLayout>
25 25
26 26 Q_LOGGING_CATEGORY(LOG_VisualizationZoneWidget, "VisualizationZoneWidget")
27 27
28 28 namespace {
29 29
30 30
31 31 /// Generates a default name for a new graph, according to the number of graphs already displayed in
32 32 /// the zone
33 33 QString defaultGraphName(const QLayout &layout)
34 34 {
35 35 auto count = 0;
36 36 for (auto i = 0; i < layout.count(); ++i) {
37 37 if (dynamic_cast<VisualizationGraphWidget *>(layout.itemAt(i)->widget())) {
38 38 count++;
39 39 }
40 40 }
41 41
42 42 return QObject::tr("Graph %1").arg(count + 1);
43 43 }
44 44
45 45 /**
46 46 * Applies a function to all graphs of the zone represented by its layout
47 47 * @param layout the layout that contains graphs
48 48 * @param fun the function to apply to each graph
49 49 */
50 50 template <typename Fun>
51 51 void processGraphs(QLayout &layout, Fun fun)
52 52 {
53 53 for (auto i = 0; i < layout.count(); ++i) {
54 54 if (auto item = layout.itemAt(i)) {
55 55 if (auto visualizationGraphWidget
56 56 = dynamic_cast<VisualizationGraphWidget *>(item->widget())) {
57 57 fun(*visualizationGraphWidget);
58 58 }
59 59 }
60 60 }
61 61 }
62 62
63 63 } // namespace
64 64
65 65 struct VisualizationZoneWidget::VisualizationZoneWidgetPrivate {
66 66
67 67 explicit VisualizationZoneWidgetPrivate()
68 68 : m_SynchronisationGroupId{QUuid::createUuid()},
69 69 m_Synchronizer{std::make_unique<QCustomPlotSynchronizer>()}
70 70 {
71 71 }
72 72 QUuid m_SynchronisationGroupId;
73 73 std::unique_ptr<IGraphSynchronizer> m_Synchronizer;
74 74
75 75 // Returns the first graph in the zone or nullptr if there is no graph inside
76 76 VisualizationGraphWidget *firstGraph(const VisualizationZoneWidget *zoneWidget) const
77 77 {
78 78 VisualizationGraphWidget *firstGraph = nullptr;
79 79 auto layout = zoneWidget->ui->dragDropContainer->layout();
80 80 if (layout->count() > 0) {
81 81 if (auto visualizationGraphWidget
82 82 = qobject_cast<VisualizationGraphWidget *>(layout->itemAt(0)->widget())) {
83 83 firstGraph = visualizationGraphWidget;
84 84 }
85 85 }
86 86
87 87 return firstGraph;
88 88 }
89 89
90 90 void dropGraph(int index, VisualizationZoneWidget *zoneWidget);
91 91 void dropVariables(const QList<std::shared_ptr<Variable> > &variables, int index,
92 92 VisualizationZoneWidget *zoneWidget);
93 93 };
94 94
95 95 VisualizationZoneWidget::VisualizationZoneWidget(const QString &name, QWidget *parent)
96 96 : VisualizationDragWidget{parent},
97 97 ui{new Ui::VisualizationZoneWidget},
98 98 impl{spimpl::make_unique_impl<VisualizationZoneWidgetPrivate>()}
99 99 {
100 100 ui->setupUi(this);
101 101
102 102 ui->zoneNameLabel->setText(name);
103 103
104 104 ui->dragDropContainer->setPlaceHolderType(DragDropHelper::PlaceHolderType::Graph);
105 105 ui->dragDropContainer->addAcceptedMimeType(
106 106 MIME_TYPE_GRAPH, VisualizationDragDropContainer::DropBehavior::Inserted);
107 107 ui->dragDropContainer->addAcceptedMimeType(
108 108 MIME_TYPE_VARIABLE_LIST, VisualizationDragDropContainer::DropBehavior::InsertedAndMerged);
109 109 ui->dragDropContainer->addAcceptedMimeType(
110 110 MIME_TYPE_TIME_RANGE, VisualizationDragDropContainer::DropBehavior::Merged);
111 111 ui->dragDropContainer->setAcceptMimeDataFunction([this](auto mimeData) {
112 112 return sqpApp->dragDropHelper().checkMimeDataForVisualization(mimeData,
113 113 ui->dragDropContainer);
114 114 });
115 115
116 116 connect(ui->dragDropContainer, &VisualizationDragDropContainer::dropOccuredInContainer, this,
117 117 &VisualizationZoneWidget::dropMimeData);
118 118 connect(ui->dragDropContainer, &VisualizationDragDropContainer::dropOccuredOnWidget, this,
119 119 &VisualizationZoneWidget::dropMimeDataOnGraph);
120 120
121 121 // 'Close' options : widget is deleted when closed
122 122 setAttribute(Qt::WA_DeleteOnClose);
123 123 connect(ui->closeButton, &QToolButton::clicked, this, &VisualizationZoneWidget::close);
124 124 ui->closeButton->setIcon(sqpApp->style()->standardIcon(QStyle::SP_TitleBarCloseButton));
125 125
126 126 // Synchronisation id
127 127 QMetaObject::invokeMethod(&sqpApp->variableController(), "onAddSynchronizationGroupId",
128 128 Qt::QueuedConnection, Q_ARG(QUuid, impl->m_SynchronisationGroupId));
129 129 }
130 130
131 131 VisualizationZoneWidget::~VisualizationZoneWidget()
132 132 {
133 133 delete ui;
134 134 }
135 135
136 136 void VisualizationZoneWidget::addGraph(VisualizationGraphWidget *graphWidget)
137 137 {
138 138 // Synchronize new graph with others in the zone
139 139 impl->m_Synchronizer->addGraph(*graphWidget);
140 140
141 141 ui->dragDropContainer->addDragWidget(graphWidget);
142 142 }
143 143
144 144 void VisualizationZoneWidget::insertGraph(int index, VisualizationGraphWidget *graphWidget)
145 145 {
146 146 // Synchronize new graph with others in the zone
147 147 impl->m_Synchronizer->addGraph(*graphWidget);
148 148
149 149 ui->dragDropContainer->insertDragWidget(index, graphWidget);
150 150 }
151 151
152 152 VisualizationGraphWidget *VisualizationZoneWidget::createGraph(std::shared_ptr<Variable> variable)
153 153 {
154 154 return createGraph(variable, -1);
155 155 }
156 156
157 157 VisualizationGraphWidget *VisualizationZoneWidget::createGraph(std::shared_ptr<Variable> variable,
158 158 int index)
159 159 {
160 160 auto graphWidget
161 161 = new VisualizationGraphWidget{defaultGraphName(*ui->dragDropContainer->layout()), this};
162 162
163 163
164 164 // Set graph properties
165 165 graphWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
166 166 graphWidget->setMinimumHeight(GRAPH_MINIMUM_HEIGHT);
167 167
168 168
169 169 // Lambda to synchronize zone widget
170 170 auto synchronizeZoneWidget = [this, graphWidget](const SqpRange &graphRange,
171 171 const SqpRange &oldGraphRange) {
172 172
173 173 auto zoomType = VariableController::getZoomType(graphRange, oldGraphRange);
174 174 auto frameLayout = ui->dragDropContainer->layout();
175 175 for (auto i = 0; i < frameLayout->count(); ++i) {
176 176 auto graphChild
177 177 = dynamic_cast<VisualizationGraphWidget *>(frameLayout->itemAt(i)->widget());
178 178 if (graphChild && (graphChild != graphWidget)) {
179 179
180 180 auto graphChildRange = graphChild->graphRange();
181 181 switch (zoomType) {
182 182 case AcquisitionZoomType::ZoomIn: {
183 183 auto deltaLeft = graphRange.m_TStart - oldGraphRange.m_TStart;
184 184 auto deltaRight = oldGraphRange.m_TEnd - graphRange.m_TEnd;
185 185 graphChildRange.m_TStart += deltaLeft;
186 186 graphChildRange.m_TEnd -= deltaRight;
187 187 qCDebug(LOG_VisualizationZoneWidget()) << tr("TORM: ZoomIn");
188 188 qCDebug(LOG_VisualizationZoneWidget()) << tr("TORM: deltaLeft")
189 189 << deltaLeft;
190 190 qCDebug(LOG_VisualizationZoneWidget()) << tr("TORM: deltaRight")
191 191 << deltaRight;
192 192 qCDebug(LOG_VisualizationZoneWidget())
193 193 << tr("TORM: dt") << graphRange.m_TEnd - graphRange.m_TStart;
194 194
195 195 break;
196 196 }
197 197
198 198 case AcquisitionZoomType::ZoomOut: {
199 199 qCDebug(LOG_VisualizationZoneWidget()) << tr("TORM: ZoomOut");
200 200 auto deltaLeft = oldGraphRange.m_TStart - graphRange.m_TStart;
201 201 auto deltaRight = graphRange.m_TEnd - oldGraphRange.m_TEnd;
202 202 qCDebug(LOG_VisualizationZoneWidget()) << tr("TORM: deltaLeft")
203 203 << deltaLeft;
204 204 qCDebug(LOG_VisualizationZoneWidget()) << tr("TORM: deltaRight")
205 205 << deltaRight;
206 206 qCDebug(LOG_VisualizationZoneWidget())
207 207 << tr("TORM: dt") << graphRange.m_TEnd - graphRange.m_TStart;
208 208 graphChildRange.m_TStart -= deltaLeft;
209 209 graphChildRange.m_TEnd += deltaRight;
210 210 break;
211 211 }
212 212 case AcquisitionZoomType::PanRight: {
213 213 qCDebug(LOG_VisualizationZoneWidget()) << tr("TORM: PanRight");
214 214 auto deltaLeft = graphRange.m_TStart - oldGraphRange.m_TStart;
215 215 auto deltaRight = graphRange.m_TEnd - oldGraphRange.m_TEnd;
216 216 graphChildRange.m_TStart += deltaLeft;
217 217 graphChildRange.m_TEnd += deltaRight;
218 218 qCDebug(LOG_VisualizationZoneWidget())
219 219 << tr("TORM: dt") << graphRange.m_TEnd - graphRange.m_TStart;
220 220 break;
221 221 }
222 222 case AcquisitionZoomType::PanLeft: {
223 223 qCDebug(LOG_VisualizationZoneWidget()) << tr("TORM: PanLeft");
224 224 auto deltaLeft = oldGraphRange.m_TStart - graphRange.m_TStart;
225 225 auto deltaRight = oldGraphRange.m_TEnd - graphRange.m_TEnd;
226 226 graphChildRange.m_TStart -= deltaLeft;
227 227 graphChildRange.m_TEnd -= deltaRight;
228 228 break;
229 229 }
230 230 case AcquisitionZoomType::Unknown: {
231 231 qCDebug(LOG_VisualizationZoneWidget())
232 232 << tr("Impossible to synchronize: zoom type unknown");
233 233 break;
234 234 }
235 235 default:
236 236 qCCritical(LOG_VisualizationZoneWidget())
237 237 << tr("Impossible to synchronize: zoom type not take into account");
238 238 // No action
239 239 break;
240 240 }
241 241 graphChild->enableAcquisition(false);
242 242 qCDebug(LOG_VisualizationZoneWidget()) << tr("TORM: Range before: ")
243 243 << graphChild->graphRange();
244 244 qCDebug(LOG_VisualizationZoneWidget()) << tr("TORM: Range after : ")
245 245 << graphChildRange;
246 246 qCDebug(LOG_VisualizationZoneWidget())
247 247 << tr("TORM: child dt") << graphChildRange.m_TEnd - graphChildRange.m_TStart;
248 248 graphChild->setGraphRange(graphChildRange);
249 249 graphChild->enableAcquisition(true);
250 250 }
251 251 }
252 252 };
253 253
254 254 // connection for synchronization
255 255 connect(graphWidget, &VisualizationGraphWidget::synchronize, synchronizeZoneWidget);
256 256 connect(graphWidget, &VisualizationGraphWidget::variableAdded, this,
257 257 &VisualizationZoneWidget::onVariableAdded);
258 258 connect(graphWidget, &VisualizationGraphWidget::variableAboutToBeRemoved, this,
259 259 &VisualizationZoneWidget::onVariableAboutToBeRemoved);
260 260
261 261 auto range = SqpRange{};
262 262 if (auto firstGraph = impl->firstGraph(this)) {
263 263 // Case of a new graph in a existant zone
264 264 range = firstGraph->graphRange();
265 265 }
266 266 else {
267 267 // Case of a new graph as the first of the zone
268 268 range = variable->range();
269 269 }
270 270
271 271 this->insertGraph(index, graphWidget);
272 272
273 273 graphWidget->addVariable(variable, range);
274
275 // get y using variable range
276 if (auto dataSeries = variable->dataSeries()) {
277 dataSeries->lockRead();
278 auto valuesBounds
279 = dataSeries->valuesBounds(variable->range().m_TStart, variable->range().m_TEnd);
280 auto end = dataSeries->cend();
281 if (valuesBounds.first != end && valuesBounds.second != end) {
282 auto rangeValue = [](const auto &value) { return std::isnan(value) ? 0. : value; };
283
284 auto minValue = rangeValue(valuesBounds.first->minValue());
285 auto maxValue = rangeValue(valuesBounds.second->maxValue());
286
287 graphWidget->setYRange(SqpRange{minValue, maxValue});
288 }
289 dataSeries->unlock();
290 }
274 graphWidget->setYRange(variable);
291 275
292 276 return graphWidget;
293 277 }
294 278
295 279 VisualizationGraphWidget *
296 280 VisualizationZoneWidget::createGraph(const QList<std::shared_ptr<Variable> > variables, int index)
297 281 {
298 282 if (variables.isEmpty()) {
299 283 return nullptr;
300 284 }
301 285
302 286 auto graphWidget = createGraph(variables.first(), index);
303 287 for (auto variableIt = variables.cbegin() + 1; variableIt != variables.cend(); ++variableIt) {
304 288 graphWidget->addVariable(*variableIt, graphWidget->graphRange());
305 289 }
306 290
307 291 return graphWidget;
308 292 }
309 293
310 294 void VisualizationZoneWidget::accept(IVisualizationWidgetVisitor *visitor)
311 295 {
312 296 if (visitor) {
313 297 visitor->visitEnter(this);
314 298
315 299 // Apply visitor to graph children: widgets different from graphs are not visited (no
316 300 // action)
317 301 processGraphs(
318 302 *ui->dragDropContainer->layout(),
319 303 [visitor](VisualizationGraphWidget &graphWidget) { graphWidget.accept(visitor); });
320 304
321 305 visitor->visitLeave(this);
322 306 }
323 307 else {
324 308 qCCritical(LOG_VisualizationZoneWidget()) << tr("Can't visit widget : the visitor is null");
325 309 }
326 310 }
327 311
328 312 bool VisualizationZoneWidget::canDrop(const Variable &variable) const
329 313 {
330 314 // A tab can always accomodate a variable
331 315 Q_UNUSED(variable);
332 316 return true;
333 317 }
334 318
335 319 bool VisualizationZoneWidget::contains(const Variable &variable) const
336 320 {
337 321 Q_UNUSED(variable);
338 322 return false;
339 323 }
340 324
341 325 QString VisualizationZoneWidget::name() const
342 326 {
343 327 return ui->zoneNameLabel->text();
344 328 }
345 329
346 330 QMimeData *VisualizationZoneWidget::mimeData() const
347 331 {
348 332 auto mimeData = new QMimeData;
349 333 mimeData->setData(MIME_TYPE_ZONE, QByteArray{});
350 334
351 335 return mimeData;
352 336 }
353 337
354 338 bool VisualizationZoneWidget::isDragAllowed() const
355 339 {
356 340 return true;
357 341 }
358 342
359 343 void VisualizationZoneWidget::closeEvent(QCloseEvent *event)
360 344 {
361 345 // Closes graphs in the zone
362 346 processGraphs(*ui->dragDropContainer->layout(),
363 347 [](VisualizationGraphWidget &graphWidget) { graphWidget.close(); });
364 348
365 349 // Delete synchronization group from variable controller
366 350 QMetaObject::invokeMethod(&sqpApp->variableController(), "onRemoveSynchronizationGroupId",
367 351 Qt::QueuedConnection, Q_ARG(QUuid, impl->m_SynchronisationGroupId));
368 352
369 353 QWidget::closeEvent(event);
370 354 }
371 355
372 356 void VisualizationZoneWidget::onVariableAdded(std::shared_ptr<Variable> variable)
373 357 {
374 358 QMetaObject::invokeMethod(&sqpApp->variableController(), "onAddSynchronized",
375 359 Qt::QueuedConnection, Q_ARG(std::shared_ptr<Variable>, variable),
376 360 Q_ARG(QUuid, impl->m_SynchronisationGroupId));
377 361 }
378 362
379 363 void VisualizationZoneWidget::onVariableAboutToBeRemoved(std::shared_ptr<Variable> variable)
380 364 {
381 365 QMetaObject::invokeMethod(&sqpApp->variableController(), "desynchronize", Qt::QueuedConnection,
382 366 Q_ARG(std::shared_ptr<Variable>, variable),
383 367 Q_ARG(QUuid, impl->m_SynchronisationGroupId));
384 368 }
385 369
386 370 void VisualizationZoneWidget::dropMimeData(int index, const QMimeData *mimeData)
387 371 {
388 372 if (mimeData->hasFormat(MIME_TYPE_GRAPH)) {
389 373 impl->dropGraph(index, this);
390 374 }
391 375 else if (mimeData->hasFormat(MIME_TYPE_VARIABLE_LIST)) {
392 376 auto variables = sqpApp->variableController().variablesForMimeData(
393 377 mimeData->data(MIME_TYPE_VARIABLE_LIST));
394 378 impl->dropVariables(variables, index, this);
395 379 }
396 380 else {
397 381 qCWarning(LOG_VisualizationZoneWidget())
398 382 << tr("VisualizationZoneWidget::dropMimeData, unknown MIME data received.");
399 383 }
400 384 }
401 385
402 386 void VisualizationZoneWidget::dropMimeDataOnGraph(VisualizationDragWidget *dragWidget,
403 387 const QMimeData *mimeData)
404 388 {
405 389 auto graphWidget = qobject_cast<VisualizationGraphWidget *>(dragWidget);
406 390 if (!graphWidget) {
407 391 qCWarning(LOG_VisualizationZoneWidget())
408 392 << tr("VisualizationZoneWidget::dropMimeDataOnGraph, dropping in an unknown widget, "
409 393 "drop aborted");
410 394 Q_ASSERT(false);
411 395 return;
412 396 }
413 397
414 398 if (mimeData->hasFormat(MIME_TYPE_VARIABLE_LIST)) {
415 399 auto variables = sqpApp->variableController().variablesForMimeData(
416 400 mimeData->data(MIME_TYPE_VARIABLE_LIST));
417 401 for (const auto &var : variables) {
418 402 graphWidget->addVariable(var, graphWidget->graphRange());
419 403 }
420 404 }
421 405 else if (mimeData->hasFormat(MIME_TYPE_TIME_RANGE)) {
422 406 auto range = TimeController::timeRangeForMimeData(mimeData->data(MIME_TYPE_TIME_RANGE));
423 407 graphWidget->setGraphRange(range);
424 408 }
425 409 else {
426 410 qCWarning(LOG_VisualizationZoneWidget())
427 411 << tr("VisualizationZoneWidget::dropMimeDataOnGraph, unknown MIME data received.");
428 412 }
429 413 }
430 414
431 415 void VisualizationZoneWidget::VisualizationZoneWidgetPrivate::dropGraph(
432 416 int index, VisualizationZoneWidget *zoneWidget)
433 417 {
434 418 auto &helper = sqpApp->dragDropHelper();
435 419
436 420 auto graphWidget = qobject_cast<VisualizationGraphWidget *>(helper.getCurrentDragWidget());
437 421 if (!graphWidget) {
438 422 qCWarning(LOG_VisualizationZoneWidget())
439 423 << tr("VisualizationZoneWidget::dropGraph, drop aborted, the dropped graph is not "
440 424 "found or invalid.");
441 425 Q_ASSERT(false);
442 426 return;
443 427 }
444 428
445 429 auto parentDragDropContainer
446 430 = qobject_cast<VisualizationDragDropContainer *>(graphWidget->parentWidget());
447 431 if (!parentDragDropContainer) {
448 432 qCWarning(LOG_VisualizationZoneWidget())
449 433 << tr("VisualizationZoneWidget::dropGraph, drop aborted, the parent container of "
450 434 "the dropped graph is not found.");
451 435 Q_ASSERT(false);
452 436 return;
453 437 }
454 438
455 439 const auto &variables = graphWidget->variables();
456 440
457 441 if (parentDragDropContainer != zoneWidget->ui->dragDropContainer && !variables.isEmpty()) {
458 442 // The drop didn't occur in the same zone
459 443
460 444 // Abort the requests for the variables (if any)
461 445 // Commented, because it's not sure if it's needed or not
462 446 // for (const auto& var : variables)
463 447 //{
464 448 // sqpApp->variableController().onAbortProgressRequested(var);
465 449 //}
466 450
467 451 auto previousParentZoneWidget = graphWidget->parentZoneWidget();
468 452 auto nbGraph = parentDragDropContainer->countDragWidget();
469 453 if (nbGraph == 1) {
470 454 // This is the only graph in the previous zone, close the zone
471 455 previousParentZoneWidget->close();
472 456 }
473 457 else {
474 458 // Close the graph
475 459 graphWidget->close();
476 460 }
477 461
478 462 // Creates the new graph in the zone
479 463 zoneWidget->createGraph(variables, index);
480 464 }
481 465 else {
482 466 // The drop occurred in the same zone or the graph is empty
483 467 // Simple move of the graph, no variable operation associated
484 468 parentDragDropContainer->layout()->removeWidget(graphWidget);
485 469
486 470 if (variables.isEmpty() && parentDragDropContainer != zoneWidget->ui->dragDropContainer) {
487 471 // The graph is empty and dropped in a different zone.
488 472 // Take the range of the first graph in the zone (if existing).
489 473 auto layout = zoneWidget->ui->dragDropContainer->layout();
490 474 if (layout->count() > 0) {
491 475 if (auto visualizationGraphWidget
492 476 = qobject_cast<VisualizationGraphWidget *>(layout->itemAt(0)->widget())) {
493 477 graphWidget->setGraphRange(visualizationGraphWidget->graphRange());
494 478 }
495 479 }
496 480 }
497 481
498 482 zoneWidget->ui->dragDropContainer->insertDragWidget(index, graphWidget);
499 483 }
500 484 }
501 485
502 486 void VisualizationZoneWidget::VisualizationZoneWidgetPrivate::dropVariables(
503 487 const QList<std::shared_ptr<Variable> > &variables, int index,
504 488 VisualizationZoneWidget *zoneWidget)
505 489 {
506 490 // Note: the AcceptMimeDataFunction (set on the drop container) ensure there is a single and
507 491 // compatible variable here
508 492 if (variables.count() > 1) {
509 493 qCWarning(LOG_VisualizationZoneWidget())
510 494 << tr("VisualizationZoneWidget::dropVariables, dropping multiple variables, operation "
511 495 "aborted.");
512 496 return;
513 497 }
514 498
515 499 zoneWidget->createGraph(variables, index);
516 500 }
General Comments 0
You need to be logged in to leave comments. Login now