• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

paulmthompson / WhiskerToolbox / 18246927847

04 Oct 2025 04:44PM UTC coverage: 71.826% (+0.6%) from 71.188%
18246927847

push

github

paulmthompson
refactor out media producer consumer pipeline

0 of 120 new or added lines in 2 files covered. (0.0%)

646 existing lines in 14 files now uncovered.

48895 of 68074 relevant lines covered (71.83%)

1193.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

17.32
/src/WhiskerToolbox/Media_Widget/Media_Window/Media_Window.cpp
1
#include "Media_Window.hpp"
2

3
#include "CoreGeometry/line_geometry.hpp"
4
#include "CoreGeometry/lines.hpp"
5
#include "CoreGeometry/masks.hpp"
6
#include "DataManager/DataManager.hpp"
7
#include "DataManager/DigitalTimeSeries/Digital_Interval_Series.hpp"
8
#include "DataManager/Lines/Line_Data.hpp"
9
#include "DataManager/Masks/Mask_Data.hpp"
10
#include "DataManager/Media/Media_Data.hpp"
11
#include "DataManager/Points/Point_Data.hpp"
12
#include "ImageProcessing/OpenCVUtility.hpp"
13
#include "Media_Widget/DisplayOptions/DisplayOptions.hpp"
14
#include "Media_Widget/MediaProcessing_Widget/MediaProcessing_Widget.hpp"
15
#include "Media_Widget/MediaText_Widget/MediaText_Widget.hpp"
16
#include "Media_Widget/Media_Widget.hpp"
17
#include "TimeFrame/TimeFrame.hpp"
18
#include "GroupManagementWidget/GroupManager.hpp"
19

20
//https://stackoverflow.com/questions/72533139/libtorch-errors-when-used-with-qt-opencv-and-point-cloud-library
21
#undef slots
22
#include "DataManager/Tensors/Tensor_Data.hpp"
23
#define slots Q_SLOTS
24

25
#include <QAction>
26
#include <QElapsedTimer>
27
#include <QFont>
28
#include <QGraphicsPixmapItem>
29
#include <QGraphicsSceneContextMenuEvent>
30
#include <QGraphicsSceneMouseEvent>
31
#include <QGraphicsTextItem>
32
#include <QImage>
33
#include <QMenu>
34
#include <QPainter>
35
#include <algorithm>
36

37
#include <iostream>
38

39
/*
40

41
The Media_Window class
42

43
*/
44

45

46
Media_Window::Media_Window(std::shared_ptr<DataManager> data_manager, QObject * parent)
3✔
47
    : QGraphicsScene(parent),
48
      _data_manager{std::move(data_manager)} {
3✔
49

50
    _data_manager->addObserver([this]() {
3✔
51
        _addRemoveData();
1✔
52
    });
1✔
53

54
    _canvasImage = QImage(_canvasWidth, _canvasHeight, QImage::Format_ARGB32);
3✔
55
    _canvasPixmap = addPixmap(QPixmap::fromImage(_canvasImage));
3✔
56

57
    _createContextMenu();
3✔
58
}
3✔
59

60
Media_Window::~Media_Window() {
6✔
61
    // Clean up context menu
62
    if (_context_menu) {
3✔
63
        delete _context_menu;
3✔
64
        _context_menu = nullptr;
3✔
65
    }
66

67
    // Clear temporary line items before clearing the scene
68
    clearTemporaryLine();
3✔
69

70
    // Clear all items from the scene - this automatically removes and deletes all QGraphicsItems
71
    clear();
3✔
72

73
    // Just clear the containers since the items are already deleted by clear()
74
    _line_paths.clear();
3✔
75
    _masks.clear();
3✔
76
    _mask_bounding_boxes.clear();
3✔
77
    _mask_outlines.clear();
3✔
78
    _points.clear();
3✔
79
    _intervals.clear();
3✔
80
    _tensors.clear();
3✔
81
    _text_items.clear();
3✔
82
}
6✔
83

84
void Media_Window::addMediaDataToScene(std::string const & media_key) {
3✔
85
    auto media_config = std::make_unique<MediaDisplayOptions>();
3✔
86

87
    _media_configs[media_key] = std::move(media_config);
3✔
88

89
    UpdateCanvas();
3✔
90
}
6✔
91

92
void Media_Window::_clearMedia() {
16✔
93
    // Set to black
94
    _canvasImage.fill(Qt::black);
16✔
95
    _canvasPixmap->setPixmap(QPixmap::fromImage(_canvasImage));
16✔
96
}
16✔
97

98
void Media_Window::removeMediaDataFromScene(std::string const & media_key) {
×
99
    auto mediaItem = _media_configs.find(media_key);
×
100
    if (mediaItem != _media_configs.end()) {
×
101
        _media_configs.erase(mediaItem);
×
102
    }
103

104
    UpdateCanvas();
×
105
}
×
106

107
void Media_Window::addLineDataToScene(std::string const & line_key) {
×
108
    auto line_config = std::make_unique<LineDisplayOptions>();
×
109

110
    // Assign color based on the current number of line configs
111
    line_config->hex_color = DefaultDisplayValues::getColorForIndex(_line_configs.size());
×
112

113
    _line_configs[line_key] = std::move(line_config);
×
114

115
    UpdateCanvas();
×
116
}
×
117

118
void Media_Window::_clearLines() {
16✔
119
    for (auto pathItem: _line_paths) {
16✔
120
        removeItem(pathItem);
×
121
    }
122
    for (auto pathItem: _line_paths) {
16✔
123
        delete pathItem;
×
124
    }
125
    _line_paths.clear();
16✔
126
}
16✔
127

128
void Media_Window::removeLineDataFromScene(std::string const & line_key) {
×
129
    auto lineItem = _line_configs.find(line_key);
×
130
    if (lineItem != _line_configs.end()) {
×
131
        _line_configs.erase(lineItem);
×
132
    }
133

134
    UpdateCanvas();
×
135
}
×
136

137
void Media_Window::addMaskDataToScene(std::string const & mask_key) {
3✔
138
    auto mask_config = std::make_unique<MaskDisplayOptions>();
3✔
139

140
    // Assign color based on the current number of mask configs
141
    mask_config->hex_color = DefaultDisplayValues::getColorForIndex(_mask_configs.size());
3✔
142

143
    _mask_configs[mask_key] = std::move(mask_config);
3✔
144
    UpdateCanvas();
3✔
145
}
6✔
146

147
void Media_Window::_clearMasks() {
16✔
148
    for (auto maskItem: _masks) {
16✔
149
        removeItem(maskItem);
×
150
    }
151

152
    for (auto maskItem: _masks) {
16✔
153
        delete maskItem;
×
154
    }
155
    _masks.clear();
16✔
156
}
16✔
157

158
void Media_Window::_clearMaskBoundingBoxes() {
16✔
159
    for (auto boundingBoxItem: _mask_bounding_boxes) {
16✔
160
        removeItem(boundingBoxItem);
×
161
    }
162

163
    for (auto boundingBoxItem: _mask_bounding_boxes) {
16✔
164
        delete boundingBoxItem;
×
165
    }
166
    _mask_bounding_boxes.clear();
16✔
167
}
16✔
168

169
void Media_Window::_clearMaskOutlines() {
16✔
170
    for (auto outlineItem: _mask_outlines) {
16✔
171
        removeItem(outlineItem);
×
172
    }
173

174
    for (auto outlineItem: _mask_outlines) {
16✔
175
        delete outlineItem;
×
176
    }
177
    _mask_outlines.clear();
16✔
178
}
16✔
179

180
void Media_Window::removeMaskDataFromScene(std::string const & mask_key) {
×
181
    auto maskItem = _mask_configs.find(mask_key);
×
182
    if (maskItem != _mask_configs.end()) {
×
183
        _mask_configs.erase(maskItem);
×
184
    }
185

186
    UpdateCanvas();
×
187
}
×
188

189
void Media_Window::addPointDataToScene(std::string const & point_key) {
×
190
    auto point_config = std::make_unique<PointDisplayOptions>();
×
191

192
    // Assign color based on the current number of point configs
193
    point_config->hex_color = DefaultDisplayValues::getColorForIndex(_point_configs.size());
×
194

195
    _point_configs[point_key] = std::move(point_config);
×
196
    UpdateCanvas();
×
197
}
×
198

199
void Media_Window::_clearPoints() {
16✔
200
    if (_debug_performance) {
16✔
201
        std::cout << "CLEARING POINTS - Count before: " << _points.size() << std::endl;
×
202
    }
203

204
    for (auto pathItem: _points) {
16✔
205
        removeItem(pathItem);
×
206
    }
207
    for (auto pathItem: _points) {
16✔
208
        delete pathItem;
×
209
    }
210
    _points.clear();
16✔
211

212
    if (_debug_performance) {
16✔
213
        std::cout << "  Points cleared. Count after: " << _points.size() << std::endl;
×
214
        std::cout << "  Hover circle item still exists: " << (_hover_circle_item ? "YES" : "NO") << std::endl;
×
215
    }
216
}
16✔
217

218
void Media_Window::removePointDataFromScene(std::string const & point_key) {
×
219
    auto pointItem = _point_configs.find(point_key);
×
220
    if (pointItem != _point_configs.end()) {
×
221
        _point_configs.erase(pointItem);
×
222
    }
223

224
    UpdateCanvas();
×
225
}
×
226

227
void Media_Window::addDigitalIntervalSeries(std::string const & key) {
×
228
    auto interval_config = std::make_unique<DigitalIntervalDisplayOptions>();
×
229

230
    // Assign color based on the current number of interval configs
231
    interval_config->hex_color = DefaultDisplayValues::getColorForIndex(_interval_configs.size());
×
232

233
    _interval_configs[key] = std::move(interval_config);
×
234
    UpdateCanvas();
×
235
}
×
236

237
void Media_Window::removeDigitalIntervalSeries(std::string const & key) {
×
238
    auto item = _interval_configs.find(key);
×
239
    if (item != _interval_configs.end()) {
×
240
        _interval_configs.erase(item);
×
241
    }
242

243
    UpdateCanvas();
×
244
}
×
245

246
void Media_Window::_clearIntervals() {
16✔
247
    for (auto item: _intervals) {
16✔
248
        removeItem(item);
×
249
    }
250

251
    for (auto item: _intervals) {
16✔
252
        delete item;
×
253
    }
254
    _intervals.clear();
16✔
255
}
16✔
256

257
void Media_Window::addTensorDataToScene(std::string const & tensor_key) {
×
258
    auto tensor_config = std::make_unique<TensorDisplayOptions>();
×
259

260
    // Assign color based on the current number of tensor configs
261
    tensor_config->hex_color = DefaultDisplayValues::getColorForIndex(_tensor_configs.size());
×
262

263
    _tensor_configs[tensor_key] = std::move(tensor_config);
×
264

265
    UpdateCanvas();
×
266
}
×
267

268
void Media_Window::removeTensorDataFromScene(std::string const & tensor_key) {
×
269
    auto tensorItem = _tensor_configs.find(tensor_key);
×
270
    if (tensorItem != _tensor_configs.end()) {
×
271
        _tensor_configs.erase(tensorItem);
×
272
    }
273

274
    UpdateCanvas();
×
275
}
×
276

277
void Media_Window::_clearTensors() {
16✔
278
    for (auto item: _tensors) {
16✔
279
        removeItem(item);
×
280
    }
281

282
    for (auto item: _tensors) {
16✔
283
        delete item;
×
284
    }
285
    _tensors.clear();
16✔
286
}
16✔
287

288
void Media_Window::setTextWidget(MediaText_Widget * text_widget) {
6✔
289
    _text_widget = text_widget;
6✔
290
}
6✔
291

292
void Media_Window::setGroupManager(GroupManager * group_manager) {
×
293
    // Disconnect from previous group manager if any
294
    if (_group_manager) {
×
295
        disconnect(_group_manager, nullptr, this, nullptr);
×
296
    }
297
    
298
    _group_manager = group_manager;
×
299
    
300
    // Connect to new group manager signals if available
301
    if (_group_manager) {
×
302
        connect(_group_manager, &GroupManager::groupCreated, this, &Media_Window::onGroupChanged);
×
303
        connect(_group_manager, &GroupManager::groupRemoved, this, &Media_Window::onGroupChanged);
×
304
        connect(_group_manager, &GroupManager::groupModified, this, &Media_Window::onGroupChanged);
×
305
    }
306
}
×
307

308
void Media_Window::_plotTextOverlays() {
16✔
309
    if (!_text_widget) {
16✔
310
        return;
×
311
    }
312

313
    // Get enabled text overlays from the widget
314
    auto text_overlays = _text_widget->getEnabledTextOverlays();
16✔
315

316
    for (auto const & overlay: text_overlays) {
16✔
317
        if (!overlay.enabled) {
×
318
            continue;
×
319
        }
320

321
        // Calculate position based on relative coordinates (0.0-1.0)
322
        float const x_pos = overlay.x_position * static_cast<float>(_canvasWidth);
×
323
        float const y_pos = overlay.y_position * static_cast<float>(_canvasHeight);
×
324

325
        // Create text item
326
        auto text_item = addText(overlay.text);
×
327

328
        // Set font and size
329
        QFont font = text_item->font();
×
330
        font.setPointSize(overlay.font_size);
×
331
        text_item->setFont(font);
×
332

333
        // Set color
334
        QColor const text_color(overlay.color);
×
335
        text_item->setDefaultTextColor(text_color);
×
336

337
        // Handle orientation
338
        if (overlay.orientation == TextOrientation::Vertical) {
×
339
            text_item->setRotation(90.0);// Rotate 90 degrees for vertical text
×
340
        }
341

342
        // Set position
343
        text_item->setPos(x_pos, y_pos);
×
344

345
        // Add to our collection for cleanup
346
        _text_items.append(text_item);
×
347
    }
×
348
}
16✔
349

350
void Media_Window::_clearTextOverlays() {
16✔
351
    for (auto text_item: _text_items) {
16✔
352
        removeItem(text_item);
×
353
    }
354
    for (auto text_item: _text_items) {
16✔
355
        delete text_item;
×
356
    }
357
    _text_items.clear();
16✔
358
}
16✔
359

360
void Media_Window::LoadFrame(int frame_id) {
×
361
    // Get MediaData using the active media key
362
    for (auto const & [media_key, media_config]: _media_configs) {
×
363
        if (!media_config.get()->is_visible) {
×
364
            continue;
×
365
        }
366

367
        auto media = _data_manager->getData<MediaData>(media_key);
×
368
        if (!media) {
×
369
            std::cerr << "Warning: No media data found for key '" << media_key << "'" << std::endl;
×
370
            return;
×
371
        }
372
        media->LoadFrame(frame_id);
×
373
    }
×
374

375
    // Clear any accumulated drawing points when changing frames
376
    // This ensures no cross-frame accumulation and explains why lag disappears on frame change
377
    _drawing_points.clear();
×
378
    _is_drawing = false;
×
379

380
    UpdateCanvas();
×
381
}
382

383
void Media_Window::UpdateCanvas() {
16✔
384

385
    if (_debug_performance) {
16✔
386
        std::cout << "========== Update Canvas called ==========" << std::endl;
×
387

388
        // Debug: Show current item counts before clearing
389
        std::cout << "BEFORE CLEAR - Items in scene: " << items().size() << std::endl;
×
390
        std::cout << "  Lines: " << _line_paths.size() << std::endl;
×
391
        std::cout << "  Points: " << _points.size() << std::endl;
×
392
        std::cout << "  Masks: " << _masks.size() << std::endl;
×
393
        std::cout << "  Mask bounding boxes: " << _mask_bounding_boxes.size() << std::endl;
×
394
        std::cout << "  Mask outlines: " << _mask_outlines.size() << std::endl;
×
395
        std::cout << "  Intervals: " << _intervals.size() << std::endl;
×
396
        std::cout << "  Tensors: " << _tensors.size() << std::endl;
×
397
        std::cout << "  Text items: " << _text_items.size() << std::endl;
×
398
        std::cout << "  Drawing points accumulated: " << _drawing_points.size() << std::endl;
×
399
        std::cout << "  Hover circle item exists: " << (_hover_circle_item ? "YES" : "NO") << std::endl;
×
400
    }
401

402
    _clearLines();
16✔
403
    _clearPoints();
16✔
404
    _clearMasks();
16✔
405
    _clearMaskBoundingBoxes();
16✔
406
    _clearMaskOutlines();
16✔
407
    _clearIntervals();
16✔
408
    _clearTensors();
16✔
409
    _clearTextOverlays();
16✔
410
    _clearMedia();
16✔
411

412
    _plotMediaData();
16✔
413

414
    _plotLineData();
16✔
415

416
    _plotMaskData();
16✔
417

418
    _plotPointData();
16✔
419

420
    _plotDigitalIntervalSeries();
16✔
421

422
    _plotDigitalIntervalBorders();
16✔
423

424
    _plotTensorData();
16✔
425

426
    _plotTextOverlays();
16✔
427

428
    // Note: Hover circle is now handled efficiently via _updateHoverCirclePosition()
429
    // and doesn't need to be redrawn on every UpdateCanvas() call
430

431
    if (_debug_performance) {
16✔
432
        // Debug: Show item counts after plotting
433
        std::cout << "AFTER PLOTTING - Items in scene: " << items().size() << std::endl;
×
434
        std::cout << "  Lines plotted: " << _line_paths.size() << std::endl;
×
435
        std::cout << "  Points plotted: " << _points.size() << std::endl;
×
436
        std::cout << "  Masks plotted: " << _masks.size() << std::endl;
×
437
        std::cout << "  Mask bounding boxes plotted: " << _mask_bounding_boxes.size() << std::endl;
×
438
        std::cout << "  Mask outlines plotted: " << _mask_outlines.size() << std::endl;
×
439
        std::cout << "  Intervals plotted: " << _intervals.size() << std::endl;
×
440
        std::cout << "  Tensors plotted: " << _tensors.size() << std::endl;
×
441
        std::cout << "  Text items plotted: " << _text_items.size() << std::endl;
×
442
    }
443

444
    // Save the entire QGraphicsScene as an image
445
    QImage scene_image(_canvasWidth, _canvasHeight, QImage::Format_ARGB32);
16✔
446
    scene_image.fill(Qt::transparent);// Optional: fill with transparent background
16✔
447
    QPainter painter(&scene_image);
16✔
448

449
    // Set the scene rect to match the canvas dimensions
450
    this->setSceneRect(0, 0, _canvasWidth, _canvasHeight);
16✔
451

452
    // Render the scene with proper viewport mapping
453
    this->render(&painter, QRectF(0, 0, _canvasWidth, _canvasHeight),
48✔
454
                 QRect(0, 0, _canvasWidth, _canvasHeight));
32✔
455

456
    emit canvasUpdated(scene_image);
16✔
457
}
32✔
458

459

460
QImage::Format Media_Window::_getQImageFormat(std::string const & media_key) {
×
461

462
    auto _media = _data_manager->getData<MediaData>(media_key);
×
463
    if (!_media) {
×
464
        // Return a default format if no media is available
465
        return QImage::Format_Grayscale8;
×
466
    }
467

468
    // Check bit depth for grayscale images
469
    if (_media->getFormat() == MediaData::DisplayFormat::Gray) {
×
470
        if (_media->is32Bit()) {
×
471
            return QImage::Format_Grayscale16;// Use 16-bit for higher precision
×
472
        } else {
473
            return QImage::Format_Grayscale8;// Default 8-bit
×
474
        }
475
    } else {
476
        // Color format
477
        return QImage::Format_RGBA8888;
×
478
    }
479
}
×
480

481
void Media_Window::_plotMediaData() {
16✔
482

483
    auto const current_time = _data_manager->getCurrentTime();
16✔
484

485
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
16✔
486

487
    int total_visible_media = 0;
16✔
488
    std::string active_media_key;
16✔
489
    for (auto const & [media_key, _media_config]: _media_configs) {
32✔
490
        if (!_media_config.get()->is_visible) continue;
16✔
491
        total_visible_media++;
×
492
        active_media_key = media_key;
×
493
    }
494

495
    if (total_visible_media == 0) {
16✔
496
        return;
16✔
497
    }
498

499
    QImage unscaled_image;
×
500

501
    if (total_visible_media == 1) {
×
502
        auto media = _data_manager->getData<MediaData>(active_media_key);
×
503
        if (!media) {
×
504
            std::cerr << "Warning: No media data found for key '" << active_media_key << "'" << std::endl;
×
505
            return;
×
506
        }
507

508
        if (media->getFormat() == MediaData::DisplayFormat::Gray) {
×
509
            // Handle grayscale images with potential colormap application
510
            bool apply_colormap = _media_configs[active_media_key].get()->colormap_options.active &&
×
511
                                  _media_configs[active_media_key].get()->colormap_options.colormap != ColormapType::None;
×
512

513
            if (media->is8Bit()) {
×
514
                // 8-bit grayscale processing
515
                auto unscaled_image_data_8bit = media->getProcessedData8(current_time);
×
516

517
                if (apply_colormap) {
×
518
                    auto colormap_data = ImageProcessing::apply_colormap_for_display(
×
519
                            unscaled_image_data_8bit,
520
                            media->getImageSize(),
521
                            _media_configs[active_media_key].get()->colormap_options);
×
522

523
                    // Apply colormap and get BGRA data (OpenCV returns BGRA format)
524
                    unscaled_image = QImage(colormap_data.data(),
×
525
                                            media->getWidth(),
526
                                            media->getHeight(),
527
                                            QImage::Format_ARGB32)
528
                                             .copy();
×
529
                } else {
×
530
                    // No colormap, use original 8-bit grayscale data
531
                    unscaled_image = QImage(unscaled_image_data_8bit.data(),
×
532
                                            media->getWidth(),
533
                                            media->getHeight(),
534
                                            QImage::Format_Grayscale8)
535
                                             .copy();
×
536
                }
537
            } else if (media->is32Bit()) {
×
538
                // 32-bit float processing
539
                auto unscaled_image_data_32bit = media->getProcessedData32(current_time);
×
540

541
                if (apply_colormap) {
×
542
                    // TODO: Need to implement apply_colormap_for_display for float data
543
                    // For now, convert to 8-bit and apply colormap
544
                    std::vector<uint8_t> converted_8bit;
×
545
                    converted_8bit.reserve(unscaled_image_data_32bit.size());
×
546

547
                    for (float pixel_value: unscaled_image_data_32bit) {
×
548
                        // Clamp to 0-255 range and convert to uint8_t
549
                        uint8_t byte_value = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, pixel_value)));
×
550
                        converted_8bit.push_back(byte_value);
×
551
                    }
552

553
                    auto colormap_data = ImageProcessing::apply_colormap_for_display(
×
554
                            converted_8bit,
555
                            media->getImageSize(),
556
                            _media_configs[active_media_key].get()->colormap_options);
×
557

558
                    // Apply colormap and get BGRA data - make a deep copy to avoid use-after-free
559
                    unscaled_image = QImage(colormap_data.data(),
×
560
                                            media->getWidth(),
561
                                            media->getHeight(),
562
                                            QImage::Format_ARGB32)
563
                                             .copy();
×
564
                } else {
×
565
                    // No colormap, convert 32-bit float to 16-bit for higher precision display
566
                    std::vector<uint16_t> converted_16bit;
×
567
                    converted_16bit.reserve(unscaled_image_data_32bit.size());
×
568

569
                    for (float pixel_value: unscaled_image_data_32bit) {
×
570
                        // Scale from 0-255 range to 0-65535 range
571
                        uint16_t value_16bit = static_cast<uint16_t>(std::max(0.0f, std::min(255.0f, pixel_value)) * 257.0f);
×
572
                        converted_16bit.push_back(value_16bit);
×
573
                    }
574

575
                    // Create QImage and make a deep copy to avoid use-after-free
576
                    unscaled_image = QImage(reinterpret_cast<uchar const *>(converted_16bit.data()),
×
577
                                            media->getWidth(),
578
                                            media->getHeight(),
579
                                            media->getWidth() * sizeof(uint16_t),
×
580
                                            QImage::Format_Grayscale16)
581
                                             .copy();
×
582
                }
×
583
            }
×
584
        } else {
585
            // Color image processing (always 8-bit for now)
586
            auto unscaled_image_data = media->getProcessedData8(current_time);
×
587
            unscaled_image = QImage(unscaled_image_data.data(),
×
588
                                    media->getWidth(),
589
                                    media->getHeight(),
590
                                    QImage::Format_RGBA8888);
×
591
        }
×
592
    }
×
593

594

595
    // Check for multi-channel mode (multiple enabled grayscale media)
596
    if (total_visible_media > 1) {
×
597
        // Multi-channel mode: combine multiple media with colormaps
598
        unscaled_image = _combineMultipleMedia();
×
599
    }
600

601
    auto new_image = unscaled_image.scaled(
×
602
            _canvasWidth,
603
            _canvasHeight,
604
            Qt::IgnoreAspectRatio,
605
            Qt::SmoothTransformation);
×
606

607
    std::cout << "Scaled image" << std::endl;
×
608

609
    // Check if any masks are in transparency mode
610
    bool has_transparency_mask = false;
×
611
    for (auto const & [mask_key, mask_config]: _mask_configs) {
×
612
        if (mask_config->is_visible && mask_config->use_as_transparency) {
×
613
            has_transparency_mask = true;
×
614
            break;
×
615
        }
616
    }
617

618
    // If we have transparency masks, modify the new_image
619
    if (has_transparency_mask) {
×
620
        new_image = _applyTransparencyMasks(new_image);
×
621
    }
622

623
    _canvasPixmap->setPixmap(QPixmap::fromImage(new_image));
×
624
    _canvasImage = new_image;
×
625
}
32✔
626

627

628
QImage Media_Window::_combineMultipleMedia() {
×
629

630
    auto current_time = _data_manager->getCurrentTime();
×
631

632
    // Loop through configs and get the largest image size
633
    std::vector<ImageSize> media_sizes;
×
634
    for (auto const & [media_key, media_config]: _media_configs) {
×
635
        if (!media_config->is_visible) continue;
×
636

637
        auto media = _data_manager->getData<MediaData>(media_key);
×
638
        if (!media) continue;
×
639

640
        media_sizes.push_back(media->getImageSize());
×
641
    }
×
642

643
    if (media_sizes.empty()) return QImage();
×
644

645
    // Find the maximum width and height
646
    int width = 0;
×
647
    int height = 0;
×
648
    for (auto const & size: media_sizes) {
×
649
        width = std::max(width, size.width);
×
650
        height = std::max(height, size.height);
×
651
    }
652

653
    // Create combined RGBA image
654
    QImage combined_image(width, height, QImage::Format_RGBA8888);
×
655
    combined_image.fill(qRgba(0, 0, 0, 255));// Start with black background
×
656

657
    for (auto const & [media_key, media_config]: _media_configs) {
×
658
        if (!media_config->is_visible) continue;
×
659

660
        auto media = _data_manager->getData<MediaData>(media_key);
×
661
        if (!media || media->getFormat() != MediaData::DisplayFormat::Gray) {
×
662
            continue;// Skip non-grayscale media
×
663
        }
664

665
        bool apply_colormap = media_config.get()->colormap_options.active &&
×
666
                              media_config.get()->colormap_options.colormap != ColormapType::None;
×
667

668
        if (media->is8Bit()) {
×
669
            // Handle 8-bit media data
670
            auto media_data_8bit = media->getProcessedData8(current_time);
×
671

672
            if (apply_colormap) {
×
673
                auto colormap_data = ImageProcessing::apply_colormap_for_display(
×
674
                        media_data_8bit,
675
                        media->getImageSize(),
676
                        media_config.get()->colormap_options);
×
677

678
                // Use colormap data (BGRA format from OpenCV)
679
                for (int y = 0; y < media->getHeight(); ++y) {
×
680
                    for (int x = 0; x < media->getWidth(); ++x) {
×
681
                        int const pixel_idx = (y * media->getWidth() + x) * 4;
×
682

683
                        uint8_t const b = colormap_data[pixel_idx];    // Blue channel
×
684
                        uint8_t const g = colormap_data[pixel_idx + 1];// Green channel
×
685
                        uint8_t const r = colormap_data[pixel_idx + 2];// Red channel
×
686
                        uint8_t const a = colormap_data[pixel_idx + 3];// Alpha channel
×
687

688
                        // Get current pixel from combined image
689
                        QRgb current_pixel = combined_image.pixel(x, y);
×
690

691
                        // Additive blending (common for multi-channel microscopy)
692
                        uint8_t const new_r = std::min(255, qRed(current_pixel) + r);
×
693
                        uint8_t const new_g = std::min(255, qGreen(current_pixel) + g);
×
694
                        uint8_t const new_b = std::min(255, qBlue(current_pixel) + b);
×
695

696
                        combined_image.setPixel(x, y, qRgba(new_r, new_g, new_b, 255));
×
697
                    }
698
                }
699
            } else {
×
700
                // Use 8-bit grayscale data directly (no colormap)
701
                for (int y = 0; y < media->getHeight(); ++y) {
×
702
                    for (int x = 0; x < media->getWidth(); ++x) {
×
703
                        int const pixel_idx = y * media->getWidth() + x;
×
704
                        uint8_t const gray_value = media_data_8bit[pixel_idx];
×
705

706
                        // Get current pixel from combined image
707
                        QRgb current_pixel = combined_image.pixel(x, y);
×
708

709
                        // Additive blending
710
                        uint8_t const new_r = std::min(255, qRed(current_pixel) + gray_value);
×
711
                        uint8_t const new_g = std::min(255, qGreen(current_pixel) + gray_value);
×
712
                        uint8_t const new_b = std::min(255, qBlue(current_pixel) + gray_value);
×
713

714
                        combined_image.setPixel(x, y, qRgba(new_r, new_g, new_b, 255));
×
715
                    }
716
                }
717
            }
718
        } else if (media->is32Bit()) {
×
719
            // Handle 32-bit float media data
720
            auto media_data_32bit = media->getProcessedData32(current_time);
×
721

722
            if (apply_colormap) {
×
723
                // Convert to 8-bit for colormap application (temporary until float colormap is implemented)
724
                std::vector<uint8_t> converted_8bit;
×
725
                converted_8bit.reserve(media_data_32bit.size());
×
726

727
                for (float pixel_value: media_data_32bit) {
×
728
                    uint8_t byte_value = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, pixel_value)));
×
729
                    converted_8bit.push_back(byte_value);
×
730
                }
731

732
                auto colormap_data = ImageProcessing::apply_colormap_for_display(
×
733
                        converted_8bit,
734
                        media->getImageSize(),
735
                        media_config.get()->colormap_options);
×
736

737
                // Use colormap data (BGRA format from OpenCV)
738
                for (int y = 0; y < media->getHeight(); ++y) {
×
739
                    for (int x = 0; x < media->getWidth(); ++x) {
×
740
                        int const pixel_idx = (y * media->getWidth() + x) * 4;
×
741

742
                        uint8_t const b = colormap_data[pixel_idx];    // Blue channel
×
743
                        uint8_t const g = colormap_data[pixel_idx + 1];// Green channel
×
744
                        uint8_t const r = colormap_data[pixel_idx + 2];// Red channel
×
745
                        uint8_t const a = colormap_data[pixel_idx + 3];// Alpha channel
×
746

747
                        // Get current pixel from combined image
748
                        QRgb current_pixel = combined_image.pixel(x, y);
×
749

750
                        // Additive blending
751
                        uint8_t const new_r = std::min(255, qRed(current_pixel) + r);
×
752
                        uint8_t const new_g = std::min(255, qGreen(current_pixel) + g);
×
753
                        uint8_t const new_b = std::min(255, qBlue(current_pixel) + b);
×
754

755
                        combined_image.setPixel(x, y, qRgba(new_r, new_g, new_b, 255));
×
756
                    }
757
                }
758
            } else {
×
759
                // Use 32-bit float data directly (no colormap)
760
                for (int y = 0; y < media->getHeight(); ++y) {
×
761
                    for (int x = 0; x < media->getWidth(); ++x) {
×
762
                        int const pixel_idx = y * media->getWidth() + x;
×
763
                        float const float_value = media_data_32bit[pixel_idx];
×
764
                        uint8_t const gray_value = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, float_value)));
×
765

766
                        // Get current pixel from combined image
767
                        QRgb current_pixel = combined_image.pixel(x, y);
×
768

769
                        // Additive blending
770
                        uint8_t const new_r = std::min(255, qRed(current_pixel) + gray_value);
×
771
                        uint8_t const new_g = std::min(255, qGreen(current_pixel) + gray_value);
×
772
                        uint8_t const new_b = std::min(255, qBlue(current_pixel) + gray_value);
×
773

774
                        combined_image.setPixel(x, y, qRgba(new_r, new_g, new_b, 255));
×
775
                    }
776
                }
777
            }
778
        }
×
779
    }
×
780

781
    return combined_image;
×
782
}
×
783

784
void Media_Window::mousePressEvent(QGraphicsSceneMouseEvent * event) {
×
785
    if (_debug_performance) {
×
786
        std::cout << "Mouse PRESS - Button: " << (event->button() == Qt::LeftButton ? "LEFT" : "RIGHT")
×
787
                  << ", Drawing mode: " << _drawing_mode << ", Current drawing points: " << _drawing_points.size() << std::endl;
×
788
    }
789

790
    if (event->button() == Qt::LeftButton) {
×
791
        if (_drawing_mode) {
×
792
            auto pos = event->scenePos();
×
793
            _drawing_points.clear();
×
794
            _drawing_points.push_back(pos);
×
795
            _is_drawing = true;
×
796
            if (_debug_performance) {
×
797
                std::cout << "  Started drawing - cleared and added first point" << std::endl;
×
798
            }
799
        } else if (_group_selection_enabled) {
×
800
            // Handle selection on left click (when not in drawing mode and group selection is enabled)
801
            std::string data_key, data_type;
×
802
            EntityId entity_id = _findEntityAtPosition(event->scenePos(), data_key, data_type);
×
803
            
804
            if (entity_id != 0) {
×
805
                // Use group-based selection for all entity types
806
                // Check if Ctrl is held for multi-selection
807
                if (event->modifiers() & Qt::ControlModifier) {
×
808
                    if (_selected_entities.count(entity_id)) {
×
809
                        _selected_entities.erase(entity_id);
×
810
                    } else {
811
                        _selected_entities.insert(entity_id);
×
812
                        _selected_data_key = data_key;
×
813
                        _selected_data_type = data_type;
×
814
                    }
815
                } else {
816
                    // Single selection
817
                    _selected_entities.clear();
×
818
                    _selected_entities.insert(entity_id);
×
819
                    _selected_data_key = data_key;
×
820
                    _selected_data_type = data_type;
×
821
                }
822
                UpdateCanvas(); // Refresh to show selection
×
823
            } else if (!(event->modifiers() & Qt::ControlModifier)) {
×
824
                // Clear selection if clicking on empty area without Ctrl
825
                clearAllSelections();
×
826
            }
827
        }
×
828

829
        // Emit legacy signals (qreal values)
830
        emit leftClick(event->scenePos().x(), event->scenePos().y());
×
831
        emit leftClickMedia(
×
832
                event->scenePos().x() / getXAspect(),
×
833
                event->scenePos().y() / getYAspect());
×
834
        
835
        // Emit media click signal with modifier information
836
        emit leftClickMediaWithEvent(
×
837
                event->scenePos().x() / getXAspect(),
×
838
                event->scenePos().y() / getYAspect(),
×
839
                event->modifiers());
840

841
        // Emit strong-typed coordinate signals
842
        CanvasCoordinates const canvas_coords(static_cast<float>(event->scenePos().x()),
×
843
                                              static_cast<float>(event->scenePos().y()));
×
844
        MediaCoordinates const media_coords(static_cast<float>(event->scenePos().x() / getXAspect()),
×
845
                                            static_cast<float>(event->scenePos().y() / getYAspect()));
×
846
        emit leftClickCanvas(canvas_coords);
×
847
        emit leftClickMediaCoords(media_coords);
×
848

849
    } else if (event->button() == Qt::RightButton) {
×
850
        if (_drawing_mode) {
×
851
            auto pos = event->scenePos();
×
852
            _drawing_points.clear();
×
853
            _drawing_points.push_back(pos);
×
854
            _is_drawing = true;
×
855
        }
856

857
        // Emit legacy signals (qreal values)
858
        emit rightClick(event->scenePos().x(), event->scenePos().y());
×
859
        emit rightClickMedia(
×
860
                event->scenePos().x() / getXAspect(),
×
861
                event->scenePos().y() / getYAspect());
×
862

863
        // Emit strong-typed coordinate signals
864
        CanvasCoordinates const canvas_coords(static_cast<float>(event->scenePos().x()),
×
865
                                              static_cast<float>(event->scenePos().y()));
×
866
        MediaCoordinates const media_coords(static_cast<float>(event->scenePos().x() / getXAspect()),
×
867
                                            static_cast<float>(event->scenePos().y() / getYAspect()));
×
868
        emit rightClickCanvas(canvas_coords);
×
869
        emit rightClickMediaCoords(media_coords);
×
870

871
    } else {
872
        QGraphicsScene::mousePressEvent(event);
×
873
    }
874
}
×
875
void Media_Window::mouseReleaseEvent(QGraphicsSceneMouseEvent * event) {
×
876
    if (_debug_performance) {
×
877
        std::cout << "Mouse RELEASE - Button: " << (event->button() == Qt::LeftButton ? "LEFT" : "RIGHT")
×
878
                  << ", Was drawing: " << _is_drawing << ", Drawing points: " << _drawing_points.size() << std::endl;
×
879
    }
880

881
    if (event->button() == Qt::LeftButton) {
×
882
        // Always emit leftRelease signal
883
        emit leftRelease();
×
884

885
        // Only emit drawing-specific signal and reset drawing state when in drawing mode
886
        if (_is_drawing) {
×
887
            _is_drawing = false;
×
888
            emit leftReleaseDrawing();
×
889
            if (_debug_performance) {
×
890
                std::cout << "  Drawing finished - emitted leftReleaseDrawing signal" << std::endl;
×
891
            }
892
        }
893
    } else if (event->button() == Qt::RightButton) {
×
894
        // Always emit rightRelease signal
895
        emit rightRelease();
×
896

897
        // Only emit drawing-specific signal and reset drawing state when in drawing mode
898
        if (_is_drawing) {
×
899
            _is_drawing = false;
×
900
            emit rightReleaseDrawing();
×
901
        }
902
    }
903
    QGraphicsScene::mouseReleaseEvent(event);
×
904
}
×
905
void Media_Window::mouseMoveEvent(QGraphicsSceneMouseEvent * event) {
×
906
    static int move_count = 0;
907
    move_count++;
×
908

909
    auto pos = event->scenePos();
×
910

911
    _hover_position = pos;
×
912

913
    if (_is_drawing) {
×
914
        _drawing_points.push_back(pos);
×
915
        if (_debug_performance && move_count % 10 == 0) {// Only print every 10th move to avoid spam
×
916
            std::cout << "Mouse MOVE #" << move_count << " - Drawing: adding point (total: "
×
917
                      << _drawing_points.size() << ")" << std::endl;
×
918
        }
919
    } else if (_debug_performance && move_count % 50 == 0) {// Print every 50th move when not drawing
×
920
        std::cout << "Mouse MOVE #" << move_count << " - Hover only" << std::endl;
×
921
    }
922

923
    // Emit legacy signal
924
    emit mouseMove(event->scenePos().x(), event->scenePos().y());
×
925

926
    // Emit strong-typed coordinate signal
927
    CanvasCoordinates const canvas_coords(static_cast<float>(event->scenePos().x()),
×
928
                                          static_cast<float>(event->scenePos().y()));
×
929
    emit mouseMoveCanvas(canvas_coords);
×
930

931
    QGraphicsScene::mouseMoveEvent(event);
×
932
}
×
933

934
void Media_Window::contextMenuEvent(QGraphicsSceneContextMenuEvent * event) {
×
935
    // Only show context menu if we have selections and a group manager
936
    if (!hasSelections() || !_group_manager) {
×
937
        QGraphicsScene::contextMenuEvent(event);
×
938
        return;
×
939
    }
940

941
    _updateContextMenuActions();
×
942
    _showContextMenu(event->screenPos());
×
943
}
944

945
float Media_Window::getXAspect() const {
16✔
946

947
    std::string active_media_key;
16✔
948
    for (auto const & [config_key, config]: _media_configs) {
32✔
949
        if (config->is_visible) {
16✔
950
            active_media_key = config_key;
×
951
            break;
×
952
        }
953
    }
954
    if (active_media_key.empty()) {
16✔
955
        // No active media, return default aspect ratio
956
        return 1.0f;
16✔
957
    }
958

959
    auto _media = _data_manager->getData<MediaData>(active_media_key);
×
960
    if (!_media) {
×
961
        return 1.0f;// Default aspect ratio
×
962
    }
963

964
    float const scale_width = static_cast<float>(_canvasWidth) / static_cast<float>(_media->getWidth());
×
965

966
    return scale_width;
×
967
}
16✔
968

969
float Media_Window::getYAspect() const {
16✔
970

971
    std::string active_media_key;
16✔
972
    for (auto const & [config_key, config]: _media_configs) {
32✔
973
        if (config->is_visible) {
16✔
974
            active_media_key = config_key;
×
975
            break;
×
976
        }
977
    }
978
    if (active_media_key.empty()) {
16✔
979
        // No active media, return default aspect ratio
980
        return 1.0f;
16✔
981
    }
982

983
    auto _media = _data_manager->getData<MediaData>(active_media_key);
×
984
    if (!_media) {
×
985
        return 1.0f;// Default aspect ratio
×
986
    }
987

988
    float const scale_height = static_cast<float>(_canvasHeight) / static_cast<float>(_media->getHeight());
×
989

990
    return scale_height;
×
991
}
16✔
992

993
void Media_Window::_plotLineData() {
16✔
994
    auto const current_time = _data_manager->getCurrentTime();
16✔
995

996
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
16✔
997

998
    auto xAspect = getXAspect();
16✔
999
    auto yAspect = getYAspect();
16✔
1000

1001
    for (auto const & [line_key, _line_config]: _line_configs) {
16✔
1002

1003
        if (!_line_config.get()->is_visible) continue;
×
1004

1005
        auto plot_color = plot_color_with_alpha(_line_config.get());
×
1006

1007
        auto line_timeframe_key = _data_manager->getTimeKey(line_key);
×
1008
        auto line_timeframe = _data_manager->getTime(line_timeframe_key);
×
1009

1010
        auto line_data = _data_manager->getData<LineData>(line_key);
×
1011
        auto lineData = line_data->getAtTime(TimeFrameIndex(current_time), video_timeframe.get(), line_timeframe.get());
×
1012
        auto entityIds = line_data->getEntityIdsAtTime(TimeFrameIndex(current_time), video_timeframe.get(), line_timeframe.get());
×
1013

1014
        // Check for line-specific image size scaling
1015
        auto image_size = line_data->getImageSize();
×
1016

1017
        if (image_size.height != -1) {
×
1018
            auto const line_height = static_cast<float>(image_size.height);
×
1019
            yAspect = static_cast<float>(_canvasHeight) / line_height;
×
1020
        }
1021

1022
        if (image_size.width != -1) {
×
1023
            auto const line_width = static_cast<float>(image_size.width);
×
1024
            xAspect = static_cast<float>(_canvasWidth) / line_width;
×
1025
        }
1026

1027
        if (lineData.empty()) {
×
1028
            continue;
×
1029
        }
1030

1031
        // Ensure we have matching line data and entity IDs
1032
        size_t line_count = std::min(lineData.size(), entityIds.size());
×
1033

1034
        for (int line_idx = 0; line_idx < static_cast<int>(line_count); ++line_idx) {
×
1035
            auto const & single_line = lineData[line_idx];
×
1036
            EntityId entity_id = static_cast<size_t>(line_idx) < entityIds.size() ? entityIds[line_idx] : 0;
×
1037

1038
            if (single_line.empty()) {
×
1039
                continue;
×
1040
            }
1041

1042
            // Check if the entity's group is visible
1043
            if (!_isEntityGroupVisible(entity_id)) {
×
UNCOV
1044
                continue; // Skip rendering this entity if its group is not visible
×
1045
            }
1046

1047
            // Use group-aware color if available, otherwise use default plot color
1048
            QColor line_color = _getGroupAwareColor(entity_id, QColor::fromRgba(plot_color));
×
1049

1050
            // Use segment if enabled, otherwise use full line
UNCOV
1051
            Line2D line_to_plot;
×
UNCOV
1052
            if (_line_config.get()->show_segment) {
×
1053
                float const start_percentage = static_cast<float>(_line_config.get()->segment_start_percentage) / 100.0f;
×
1054
                float const end_percentage = static_cast<float>(_line_config.get()->segment_end_percentage) / 100.0f;
×
UNCOV
1055
                line_to_plot = get_segment_between_percentages(single_line, start_percentage, end_percentage);
×
1056

1057
                // If segment is empty (invalid percentages), skip this line
UNCOV
1058
                if (line_to_plot.empty()) {
×
UNCOV
1059
                    continue;
×
1060
                }
1061
            } else {
1062
                line_to_plot = single_line;
×
1063
            }
1064

UNCOV
1065
            QPainterPath path = QPainterPath();
×
1066

1067
            auto single_line_thres = 1000.0;
×
1068

1069
            path.moveTo(QPointF(static_cast<float>(line_to_plot[0].x) * xAspect, static_cast<float>(line_to_plot[0].y) * yAspect));
×
1070

1071
            for (size_t i = 1; i < line_to_plot.size(); i++) {
×
UNCOV
1072
                auto dx = line_to_plot[i].x - line_to_plot[i - 1].x;
×
1073
                auto dy = line_to_plot[i].y - line_to_plot[i - 1].y;
×
UNCOV
1074
                auto d = std::sqrt((dx * dx) + (dy * dy));
×
UNCOV
1075
                if (d > single_line_thres) {
×
UNCOV
1076
                    path.moveTo(QPointF(static_cast<float>(line_to_plot[i].x) * xAspect, static_cast<float>(line_to_plot[i].y) * yAspect));
×
1077
                } else {
1078
                    path.lineTo(QPointF(static_cast<float>(line_to_plot[i].x) * xAspect, static_cast<float>(line_to_plot[i].y) * yAspect));
×
1079
                }
1080
            }
1081

1082
            // Create pen with group-aware color and configurable thickness
1083
            QPen linePen;
×
UNCOV
1084
            linePen.setColor(line_color);
×
UNCOV
1085
            linePen.setWidth(_line_config.get()->line_thickness);
×
1086

1087
            auto linePath = addPath(path, linePen);
×
1088
            _line_paths.append(linePath);
×
1089

1090
            // Add dot at line base (always filled) - use group-aware color
1091
            QColor const dot_color = line_color;
×
1092
            auto ellipse = addEllipse(
×
1093
                    static_cast<float>(line_to_plot[0].x) * xAspect - 2.5,
×
UNCOV
1094
                    static_cast<float>(line_to_plot[0].y) * yAspect - 2.5,
×
1095
                    5.0, 5.0,
1096
                    QPen(dot_color),
×
UNCOV
1097
                    QBrush(dot_color));
×
1098
            _points.append(ellipse);
×
1099

1100
            // If show_points is enabled, add open circles at each point on the line
UNCOV
1101
            if (_line_config.get()->show_points) {
×
1102
                // Create pen and brush for open circles
UNCOV
1103
                QPen pointPen(dot_color);
×
UNCOV
1104
                pointPen.setWidth(1);
×
1105

1106
                // Empty brush for open circles
1107
                QBrush const emptyBrush(Qt::NoBrush);
×
1108

1109
                // Start from the second point (first one is already shown as filled)
UNCOV
1110
                for (size_t i = 1; i < line_to_plot.size(); i++) {
×
UNCOV
1111
                    auto ellipse = addEllipse(
×
1112
                            static_cast<float>(line_to_plot[i].x) * xAspect - 2.5,
×
UNCOV
1113
                            static_cast<float>(line_to_plot[i].y) * yAspect - 2.5,
×
1114
                            5.0, 5.0,
1115
                            pointPen,
1116
                            emptyBrush);
1117
                    _points.append(ellipse);
×
1118
                }
1119
            }
×
1120

1121
            // If position marker is enabled, add a marker at the specified percentage
1122
            if (_line_config.get()->show_position_marker) {
×
UNCOV
1123
                float const percentage = static_cast<float>(_line_config.get()->position_percentage) / 100.0f;
×
UNCOV
1124
                Point2D<float> const marker_pos = get_position_at_percentage(line_to_plot, percentage);
×
1125

1126
                float const marker_x = marker_pos.x * xAspect;
×
1127
                float const marker_y = marker_pos.y * yAspect;
×
1128

1129
                // Create a distinctive marker (filled circle with border)
1130
                QPen markerPen(QColor(255, 255, 255));// White border
×
1131
                markerPen.setWidth(2);
×
UNCOV
1132
                QBrush const markerBrush(dot_color);// Same color as line but filled
×
1133

UNCOV
1134
                auto marker = addEllipse(
×
1135
                        marker_x - 4.0f,
×
1136
                        marker_y - 4.0f,
×
1137
                        8.0f, 8.0f,
1138
                        markerPen,
1139
                        markerBrush);
UNCOV
1140
                _points.append(marker);
×
UNCOV
1141
            }
×
UNCOV
1142
        }
×
UNCOV
1143
    }
×
1144
}
32✔
1145

1146
void Media_Window::_plotMaskData() {
16✔
1147
    // Note: MaskData does not currently support EntityIds for group-aware coloring
1148
    // This would need to be implemented similar to PointData and LineData
1149
    auto const current_time = _data_manager->getCurrentTime();
16✔
1150

1151
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
16✔
1152

1153
    for (auto const & [mask_key, _mask_config]: _mask_configs) {
29✔
1154
        if (!_mask_config.get()->is_visible) continue;
13✔
1155

1156
        auto plot_color = plot_color_with_alpha(_mask_config.get());
×
1157

UNCOV
1158
        auto mask = _data_manager->getData<MaskData>(mask_key);
×
UNCOV
1159
        auto image_size = mask->getImageSize();
×
1160

1161
        auto mask_timeframe_key = _data_manager->getTimeKey(mask_key);
×
UNCOV
1162
        auto mask_timeframe = _data_manager->getTime(mask_timeframe_key);
×
1163

1164
        // Check for preview data first
1165
        std::vector<Mask2D> maskData;
×
1166
        std::vector<Mask2D> maskData2;
×
1167

UNCOV
1168
        if (_mask_preview_active && _preview_mask_data.count(mask_key) > 0) {
×
1169
            // Use preview data
1170
            maskData = _preview_mask_data[mask_key];
×
UNCOV
1171
            maskData2.clear();// No time -1 data for preview
×
1172
        } else {
1173
            // Use original data
1174
            maskData = mask->getAtTime(TimeFrameIndex(current_time), video_timeframe.get(), mask_timeframe.get());
×
UNCOV
1175
            maskData2 = mask->getAtTime(TimeFrameIndex(-1));
×
1176
        }
1177

UNCOV
1178
        _plotSingleMaskData(maskData, image_size, plot_color, _mask_config.get());
×
1179
        _plotSingleMaskData(maskData2, image_size, plot_color, _mask_config.get());
×
1180

1181
        // Plot bounding boxes if enabled
UNCOV
1182
        if (_mask_config.get()->show_bounding_box) {
×
1183
            // Calculate scaling factors based on mask image size, not media aspect ratio
1184
            float const xAspect = static_cast<float>(_canvasWidth) / static_cast<float>(image_size.width);
×
1185
            float const yAspect = static_cast<float>(_canvasHeight) / static_cast<float>(image_size.height);
×
1186

1187
            // For current time masks
UNCOV
1188
            for (auto const & single_mask: maskData) {
×
UNCOV
1189
                if (!single_mask.empty()) {
×
1190
                    auto bounding_box = get_bounding_box(single_mask);
×
1191
                    auto min_point = bounding_box.first;
×
1192
                    auto max_point = bounding_box.second;
×
1193

1194
                    // Scale coordinates to canvas using mask image size
UNCOV
1195
                    float const min_x = static_cast<float>(min_point.x) * xAspect;
×
1196
                    float const min_y = static_cast<float>(min_point.y) * yAspect;
×
1197
                    float const max_x = static_cast<float>(max_point.x) * xAspect;
×
1198
                    float const max_y = static_cast<float>(max_point.y) * yAspect;
×
1199

1200
                    // Draw bounding box rectangle (no fill, just outline)
UNCOV
1201
                    QPen boundingBoxPen(plot_color);
×
1202
                    boundingBoxPen.setWidth(2);
×
1203
                    QBrush const emptyBrush(Qt::NoBrush);
×
1204

UNCOV
1205
                    auto boundingBoxRect = addRect(min_x, min_y, max_x - min_x, max_y - min_y,
×
1206
                                                   boundingBoxPen, emptyBrush);
1207
                    _mask_bounding_boxes.append(boundingBoxRect);
×
1208
                }
×
1209
            }
1210

1211
            // For time -1 masks
UNCOV
1212
            for (auto const & single_mask: maskData2) {
×
UNCOV
1213
                if (!single_mask.empty()) {
×
1214
                    auto bounding_box = get_bounding_box(single_mask);
×
1215
                    auto min_point = bounding_box.first;
×
1216
                    auto max_point = bounding_box.second;
×
1217

1218
                    // Scale coordinates to canvas using mask image size
UNCOV
1219
                    float const min_x = static_cast<float>(min_point.x) * xAspect;
×
1220
                    float const min_y = static_cast<float>(min_point.y) * yAspect;
×
1221
                    float const max_x = static_cast<float>(max_point.x) * xAspect;
×
1222
                    float const max_y = static_cast<float>(max_point.y) * yAspect;
×
1223

1224
                    // Draw bounding box rectangle (no fill, just outline)
UNCOV
1225
                    QPen boundingBoxPen(plot_color);
×
1226
                    boundingBoxPen.setWidth(2);
×
1227
                    QBrush const emptyBrush(Qt::NoBrush);
×
1228

UNCOV
1229
                    auto boundingBoxRect = addRect(min_x, min_y, max_x - min_x, max_y - min_y,
×
1230
                                                   boundingBoxPen, emptyBrush);
UNCOV
1231
                    _mask_bounding_boxes.append(boundingBoxRect);
×
1232
                }
×
1233
            }
1234
        }
1235

1236
        // Plot outlines if enabled
1237
        if (_mask_config.get()->show_outline) {
×
1238
            // Create a slightly darker color for outlines
UNCOV
1239
            QRgb const outline_color = plot_color;
×
1240

1241
            // For current time masks
1242
            for (auto const & single_mask: maskData) {
×
UNCOV
1243
                if (!single_mask.empty()) {
×
1244
                    // Generate outline mask with thickness of 2 pixels
UNCOV
1245
                    auto outline_mask = generate_outline_mask(single_mask, 2, image_size.width, image_size.height);
×
1246

UNCOV
1247
                    if (!outline_mask.empty()) {
×
1248
                        // Plot the outline mask using the same approach as regular masks
UNCOV
1249
                        _plotSingleMaskData({outline_mask}, image_size, outline_color, _mask_config.get());
×
1250
                    }
1251
                }
×
1252
            }
1253

1254
            // For time -1 masks
1255
            for (auto const & single_mask: maskData2) {
×
UNCOV
1256
                if (!single_mask.empty()) {
×
1257
                    // Generate outline mask with thickness of 2 pixels
UNCOV
1258
                    auto outline_mask = generate_outline_mask(single_mask, 2, image_size.width, image_size.height);
×
1259

UNCOV
1260
                    if (!outline_mask.empty()) {
×
1261
                        // Plot the outline mask using the same approach as regular masks
1262
                        _plotSingleMaskData({outline_mask}, image_size, outline_color, _mask_config.get());
×
1263
                    }
UNCOV
1264
                }
×
1265
            }
1266
        }
1267
    }
×
1268
}
32✔
1269

UNCOV
1270
void Media_Window::_plotSingleMaskData(std::vector<Mask2D> const & maskData, ImageSize mask_size, QRgb plot_color, MaskDisplayOptions const * mask_config) {
×
1271
    // Skip transparency masks as they are handled at the media level
UNCOV
1272
    if (mask_config && mask_config->use_as_transparency) {
×
1273
        return;
×
1274
    }
1275

1276
    for (auto const & single_mask: maskData) {
×
1277
        // Normal mode: overlay mask on top of media
1278
        QImage unscaled_mask_image(mask_size.width, mask_size.height, QImage::Format::Format_ARGB32);
×
UNCOV
1279
        unscaled_mask_image.fill(0);
×
1280

UNCOV
1281
        for (auto const point: single_mask) {
×
1282
            unscaled_mask_image.setPixel(
×
1283
                    QPoint(static_cast<int>(point.x), static_cast<int>(point.y)),
×
1284
                    plot_color);
1285
        }
1286

UNCOV
1287
        auto scaled_mask_image = unscaled_mask_image.scaled(_canvasWidth, _canvasHeight);
×
1288
        auto maskPixmap = addPixmap(QPixmap::fromImage(scaled_mask_image));
×
1289
        _masks.append(maskPixmap);
×
UNCOV
1290
    }
×
1291
}
1292

UNCOV
1293
QImage Media_Window::_applyTransparencyMasks(QImage const & media_image) {
×
1294
    std::cout << "Applying transparency masks..." << std::endl;
×
1295

1296
    std::cout << "Media image size: " << media_image.width() << "x" << media_image.height() << std::endl;
×
UNCOV
1297
    std::cout << "Canvas dimensions: " << _canvasWidth << "x" << _canvasHeight << std::endl;
×
1298

1299
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
×
1300

UNCOV
1301
    QImage final_image = media_image;
×
1302

1303
    int transparency_mask_count = 0;
×
1304
    int const total_mask_points = 0;
×
1305

1306
    // Process all transparency masks
1307
    for (auto const & [mask_key, mask_config]: _mask_configs) {
×
1308
        if (!mask_config->is_visible || !mask_config->use_as_transparency) {
×
UNCOV
1309
            continue;
×
1310
        }
1311

UNCOV
1312
        transparency_mask_count++;
×
1313
        std::cout << "Processing transparency mask: " << mask_key << std::endl;
×
1314

UNCOV
1315
        auto mask_data = _data_manager->getData<MaskData>(mask_key);
×
1316
        auto image_size = mask_data->getImageSize();
×
1317

1318
        auto mask_timeframe_key = _data_manager->getTimeKey(mask_key);
×
1319
        auto mask_timeframe = _data_manager->getTime(mask_timeframe_key);
×
1320

1321
        std::cout << "Mask image size: " << image_size.width << "x" << image_size.height << std::endl;
×
1322

UNCOV
1323
        auto const current_time = _data_manager->getCurrentTime();
×
1324
        auto maskData = mask_data->getAtTime(TimeFrameIndex(current_time), video_timeframe.get(), mask_timeframe.get());
×
1325

UNCOV
1326
        std::cout << "Mask data size: " << maskData.size() << std::endl;
×
1327

1328
        // Calculate scaling factors
1329
        float const xAspect = static_cast<float>(_canvasWidth) / static_cast<float>(image_size.width);
×
1330
        float const yAspect = static_cast<float>(_canvasHeight) / static_cast<float>(image_size.height);
×
1331

1332
        std::cout << "Scaling factors: x=" << xAspect << ", y=" << yAspect << std::endl;
×
1333

1334
        QImage unscaled_mask_image(image_size.width, image_size.height, QImage::Format::Format_ARGB32);
×
1335
        unscaled_mask_image.fill(0);
×
1336
        // Add mask points to combined mask
UNCOV
1337
        for (auto const & single_mask: maskData) {
×
UNCOV
1338
            for (auto const point: single_mask) {
×
UNCOV
1339
                unscaled_mask_image.setPixel(
×
1340
                        QPoint(static_cast<int>(point.x), static_cast<int>(point.y)),
×
1341
                        qRgba(255, 255, 255, 255));
1342
            }
1343
        }
1344

1345
        QImage const scaled_mask_image = unscaled_mask_image.scaled(_canvasWidth, _canvasHeight);
×
1346
        // I want to copy final_image where scaled_mask_image is white, and keep the rest of the image the same
1347
        for (int y = 0; y < _canvasHeight; ++y) {
×
UNCOV
1348
            for (int x = 0; x < _canvasWidth; ++x) {
×
UNCOV
1349
                if (scaled_mask_image.pixel(x, y) == qRgba(255, 255, 255, 255)) {
×
UNCOV
1350
                    final_image.setPixel(x, y, final_image.pixel(x, y));
×
1351
                } else {
UNCOV
1352
                    final_image.setPixel(x, y, qRgba(0, 0, 0, 255));
×
1353
                }
1354
            }
1355
        }
UNCOV
1356
    }
×
1357

1358

UNCOV
1359
    return final_image;
×
UNCOV
1360
}
×
1361

1362
void Media_Window::_plotPointData() {
16✔
1363

1364
    auto const current_time = TimeFrameIndex(_data_manager->getCurrentTime());
16✔
1365
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
16✔
1366

1367
    if (!video_timeframe) {
16✔
1368
        std::cerr << "Error: Could not get video timeframe 'time' for point conversion" << std::endl;
×
UNCOV
1369
        return;
×
1370
    }
1371

1372
    for (auto const & [point_key, _point_config]: _point_configs) {
16✔
UNCOV
1373
        if (!_point_config.get()->is_visible) continue;
×
1374

1375
        auto plot_color = plot_color_with_alpha(_point_config.get());
×
1376

1377
        auto point = _data_manager->getData<PointData>(point_key);
×
1378

UNCOV
1379
        auto point_timeframe_key = _data_manager->getTimeKey(point_key);
×
1380
        if (point_timeframe_key.empty()) {
×
UNCOV
1381
            std::cerr << "Error: No timeframe found for point data: " << point_key << std::endl;
×
1382
            continue;
×
1383
        }
1384

1385
        auto point_timeframe = _data_manager->getTime(point_timeframe_key);
×
1386

1387
        auto xAspect = getXAspect();
×
1388
        auto yAspect = getYAspect();
×
1389

UNCOV
1390
        auto image_size = point->getImageSize();
×
1391

1392
        if (image_size.height != -1) {
×
1393
            auto const mask_height = static_cast<float>(image_size.height);
×
1394
            yAspect = static_cast<float>(_canvasHeight) / mask_height;
×
1395
        }
1396

1397
        if (image_size.width != -1) {
×
1398
            auto const mask_width = static_cast<float>(image_size.width);
×
UNCOV
1399
            xAspect = static_cast<float>(_canvasWidth) / mask_width;
×
1400
        }
1401

UNCOV
1402
        auto pointData = point->getAtTime(current_time, video_timeframe.get(), point_timeframe.get());
×
UNCOV
1403
        auto entityIds = point->getEntityIdsAtTime(current_time);
×
1404

1405
        // Get configurable point size
1406
        float const point_size = static_cast<float>(_point_config.get()->point_size);
×
1407

1408
        // Ensure we have matching point data and entity IDs
UNCOV
1409
        size_t count = std::min(pointData.size(), entityIds.size());
×
1410
        
1411
        for (size_t i = 0; i < count; ++i) {
×
UNCOV
1412
            auto const & single_point = pointData[i];
×
UNCOV
1413
            EntityId entity_id = entityIds[i];
×
1414
            
1415
            // Check if the entity's group is visible
UNCOV
1416
            if (!_isEntityGroupVisible(entity_id)) {
×
1417
                continue; // Skip rendering this entity if its group is not visible
×
1418
            }
1419
            
1420
            float const x_pos = single_point.x * xAspect;
×
1421
            float const y_pos = single_point.y * yAspect;
×
1422

1423
            // Use group-aware color if available, otherwise use default plot color
1424
            QColor point_color = _getGroupAwareColor(entity_id, QColor::fromRgba(plot_color));
×
1425
            
1426
            // Check if this point is selected to add highlight
1427
            bool is_selected = _selected_entities.count(entity_id) > 0;
×
1428
            
1429
            // Add selection highlight if point is selected
UNCOV
1430
            if (is_selected) {
×
1431
                QPen highlight_pen(Qt::yellow);
×
1432
                highlight_pen.setWidth(4);
×
1433
                QBrush highlight_brush(Qt::transparent);
×
1434
                auto highlight_circle = addEllipse(x_pos - point_size, y_pos - point_size,
×
1435
                                                   point_size * 2, point_size * 2, 
×
1436
                                                   highlight_pen, highlight_brush);
UNCOV
1437
                _points.append(highlight_circle);
×
1438
            }
×
1439

1440
            // Create the appropriate marker shape based on configuration
1441
            switch (_point_config.get()->marker_shape) {
×
1442
                case PointMarkerShape::Circle: {
×
1443
                    QPen pen(point_color);
×
1444
                    pen.setWidth(2);
×
1445
                    QBrush const brush(point_color);
×
UNCOV
1446
                    auto ellipse = addEllipse(x_pos - point_size / 2, y_pos - point_size / 2,
×
1447
                                              point_size, point_size, pen, brush);
1448
                    _points.append(ellipse);
×
1449
                    break;
×
1450
                }
×
1451
                case PointMarkerShape::Square: {
×
1452
                    QPen pen(point_color);
×
1453
                    pen.setWidth(2);
×
UNCOV
1454
                    QBrush const brush(point_color);
×
UNCOV
1455
                    auto rect = addRect(x_pos - point_size / 2, y_pos - point_size / 2,
×
1456
                                        point_size, point_size, pen, brush);
1457
                    _points.append(rect);
×
1458
                    break;
×
1459
                }
×
1460
                case PointMarkerShape::Triangle: {
×
UNCOV
1461
                    QPen pen(point_color);
×
1462
                    pen.setWidth(2);
×
1463
                    QBrush const brush(point_color);
×
1464

1465
                    // Create triangle polygon
1466
                    QPolygonF triangle;
×
1467
                    float const half_size = point_size / 2;
×
1468
                    triangle << QPointF(x_pos, y_pos - half_size)             // Top point
×
UNCOV
1469
                             << QPointF(x_pos - half_size, y_pos + half_size) // Bottom left
×
1470
                             << QPointF(x_pos + half_size, y_pos + half_size);// Bottom right
×
1471

1472
                    auto polygon = addPolygon(triangle, pen, brush);
×
1473
                    _points.append(polygon);
×
UNCOV
1474
                    break;
×
UNCOV
1475
                }
×
1476
                case PointMarkerShape::Cross: {
×
1477
                    QPen pen(point_color);
×
1478
                    pen.setWidth(3);
×
1479

1480
                    float const half_size = point_size / 2;
×
1481
                    // Draw horizontal line
1482
                    auto hLine = addLine(x_pos - half_size, y_pos, x_pos + half_size, y_pos, pen);
×
UNCOV
1483
                    _points.append(hLine);
×
1484

1485
                    // Draw vertical line
1486
                    auto vLine = addLine(x_pos, y_pos - half_size, x_pos, y_pos + half_size, pen);
×
1487
                    _points.append(vLine);
×
1488
                    break;
×
UNCOV
1489
                }
×
UNCOV
1490
                case PointMarkerShape::X: {
×
1491
                    QPen pen(point_color);
×
1492
                    pen.setWidth(3);
×
1493

1494
                    float const half_size = point_size / 2;
×
1495
                    // Draw diagonal line (\)
1496
                    auto dLine1 = addLine(x_pos - half_size, y_pos - half_size,
×
1497
                                          x_pos + half_size, y_pos + half_size, pen);
×
1498
                    _points.append(dLine1);
×
1499

1500
                    // Draw diagonal line (/)
UNCOV
1501
                    auto dLine2 = addLine(x_pos - half_size, y_pos + half_size,
×
1502
                                          x_pos + half_size, y_pos - half_size, pen);
×
1503
                    _points.append(dLine2);
×
1504
                    break;
×
1505
                }
×
1506
                case PointMarkerShape::Diamond: {
×
1507
                    QPen pen(point_color);
×
UNCOV
1508
                    pen.setWidth(2);
×
1509
                    QBrush brush(point_color);
×
1510

1511
                    // Create diamond polygon (rotated square)
1512
                    QPolygonF diamond;
×
UNCOV
1513
                    float const half_size = point_size / 2;
×
UNCOV
1514
                    diamond << QPointF(x_pos, y_pos - half_size) // Top
×
1515
                            << QPointF(x_pos + half_size, y_pos) // Right
×
UNCOV
1516
                            << QPointF(x_pos, y_pos + half_size) // Bottom
×
UNCOV
1517
                            << QPointF(x_pos - half_size, y_pos);// Left
×
1518

UNCOV
1519
                    auto polygon = addPolygon(diamond, pen, brush);
×
UNCOV
1520
                    _points.append(polygon);
×
UNCOV
1521
                    break;
×
UNCOV
1522
                }
×
1523
            }
1524
        }
UNCOV
1525
    }
×
1526
}
16✔
1527

1528
void Media_Window::_plotDigitalIntervalSeries() {
16✔
1529
    auto const current_time = _data_manager->getCurrentTime();
16✔
1530
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
16✔
1531

1532
    for (auto const & [key, _interval_config]: _interval_configs) {
16✔
1533
        if (!_interval_config.get()->is_visible) continue;
×
1534

1535
        // Only render if using Box plotting style
1536
        if (_interval_config.get()->plotting_style != IntervalPlottingStyle::Box) continue;
×
1537

UNCOV
1538
        auto plot_color = plot_color_with_alpha(_interval_config.get());
×
1539

UNCOV
1540
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(key);
×
1541

1542
        // Get the timeframes for conversion
1543
        auto interval_timeframe_key = _data_manager->getTimeKey(key);
×
UNCOV
1544
        if (interval_timeframe_key.empty()) {
×
1545
            std::cerr << "Error: No timeframe found for digital interval series: " << key << std::endl;
×
1546
            continue;
×
1547
        }
1548

UNCOV
1549
        auto interval_timeframe = _data_manager->getTime(interval_timeframe_key);
×
1550

1551
        if (!video_timeframe) {
×
UNCOV
1552
            std::cerr << "Error: Could not get video timeframe 'time' for interval conversion" << std::endl;
×
UNCOV
1553
            continue;
×
1554
        }
1555
        if (!interval_timeframe) {
×
1556
            std::cerr << "Error: Could not get interval timeframe '" << interval_timeframe_key
×
1557
                      << "' for series: " << key << std::endl;
×
UNCOV
1558
            continue;
×
1559
        }
1560

UNCOV
1561
        bool const needs_conversion = _needsTimeFrameConversion(video_timeframe, interval_timeframe);
×
1562

1563
        // Generate relative times based on frame range setting
1564
        std::vector<int> relative_times;
×
1565
        int const frame_range = _interval_config->frame_range;
×
1566
        for (int i = -frame_range; i <= frame_range; ++i) {
×
1567
            relative_times.push_back(i);
×
1568
        }
1569

1570
        int const square_size = _interval_config->box_size;
×
1571

1572
        // Calculate position based on location setting
1573
        int start_x, start_y;
1574
        switch (_interval_config->location) {
×
1575
            case IntervalLocation::TopLeft:
×
1576
                start_x = 0;
×
1577
                start_y = 0;
×
1578
                break;
×
1579
            case IntervalLocation::TopRight:
×
1580
                start_x = _canvasWidth - square_size * static_cast<int>(relative_times.size());
×
UNCOV
1581
                start_y = 0;
×
UNCOV
1582
                break;
×
1583
            case IntervalLocation::BottomLeft:
×
1584
                start_x = 0;
×
1585
                start_y = _canvasHeight - square_size;
×
UNCOV
1586
                break;
×
1587
            case IntervalLocation::BottomRight:
×
UNCOV
1588
                start_x = _canvasWidth - square_size * static_cast<int>(relative_times.size());
×
UNCOV
1589
                start_y = _canvasHeight - square_size;
×
1590
                break;
×
1591
        }
1592

1593
        for (size_t i = 0; i < relative_times.size(); ++i) {
×
UNCOV
1594
            int const video_time = current_time + relative_times[i];
×
UNCOV
1595
            int query_time = video_time;// Default: no conversion needed
×
1596

UNCOV
1597
            if (needs_conversion) {
×
1598
                // Convert from video timeframe ("time") to interval series timeframe
1599
                // 1. Convert video time index to actual time value
1600
                int const video_time_value = video_timeframe->getTimeAtIndex(TimeFrameIndex(video_time));
×
1601

1602
                // 2. Convert time value to index in interval series timeframe
UNCOV
1603
                query_time = interval_timeframe->getIndexAtTime(static_cast<float>(video_time_value)).getValue();
×
1604
            }
1605

1606
            bool const event_present = interval_series->isEventAtTime(TimeFrameIndex(query_time));
×
1607

UNCOV
1608
            auto color = event_present ? plot_color : QColor(255, 255, 255, 10);// Transparent if no event
×
1609

UNCOV
1610
            auto intervalPixmap = addRect(
×
1611
                    start_x + i * square_size,
×
1612
                    start_y,
1613
                    square_size,
1614
                    square_size,
UNCOV
1615
                    QPen(Qt::black),// Black border
×
UNCOV
1616
                    QBrush(color)   // Fill with color if event is present
×
UNCOV
1617
            );
×
1618

UNCOV
1619
            _intervals.append(intervalPixmap);
×
1620
        }
1621
    }
×
1622
}
32✔
1623

1624
void Media_Window::_plotDigitalIntervalBorders() {
16✔
1625
    auto const current_time = _data_manager->getCurrentTime();
16✔
1626

1627
    for (auto const & [key, _interval_config]: _interval_configs) {
16✔
1628
        if (!_interval_config.get()->is_visible) continue;
×
1629

1630
        // Only render if using Border plotting style
UNCOV
1631
        if (_interval_config.get()->plotting_style != IntervalPlottingStyle::Border) continue;
×
1632

1633
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(key);
×
1634

1635
        // Get the timeframes for conversion
1636
        auto interval_timeframe_key = _data_manager->getTimeKey(key);
×
1637
        if (interval_timeframe_key.empty()) {
×
UNCOV
1638
            std::cerr << "Error: No timeframe found for digital interval series: " << key << std::endl;
×
1639
            continue;
×
1640
        }
1641

1642
        auto video_timeframe = _data_manager->getTime(TimeKey("time"));
×
UNCOV
1643
        auto interval_timeframe = _data_manager->getTime(interval_timeframe_key);
×
1644

1645
        if (!video_timeframe) {
×
UNCOV
1646
            std::cerr << "Error: Could not get video timeframe 'time' for interval conversion" << std::endl;
×
UNCOV
1647
            continue;
×
1648
        }
1649
        if (!interval_timeframe) {
×
UNCOV
1650
            std::cerr << "Error: Could not get interval timeframe '" << interval_timeframe_key
×
1651
                      << "' for series: " << key << std::endl;
×
1652
            continue;
×
1653
        }
1654

UNCOV
1655
        bool const needs_conversion = _needsTimeFrameConversion(video_timeframe, interval_timeframe);
×
1656

1657
        // Check if an interval is present at the current frame
UNCOV
1658
        bool interval_present = false;
×
UNCOV
1659
        if (needs_conversion) {
×
1660
            // Convert current video time to interval timeframe
1661
            auto video_time = video_timeframe->getTimeAtIndex(TimeFrameIndex(current_time));
×
UNCOV
1662
            auto interval_index = interval_timeframe->getIndexAtTime(video_time);
×
UNCOV
1663
            interval_present = interval_series->isEventAtTime(interval_index);
×
1664
        } else {
1665
            // Direct comparison (no timeframe conversion needed)
1666
            interval_present = interval_series->isEventAtTime(TimeFrameIndex(current_time));
×
1667
        }
1668

1669
        // If an interval is present, draw a border around the entire image
UNCOV
1670
        if (interval_present) {
×
1671
            auto plot_color = plot_color_with_alpha(_interval_config.get());
×
1672

1673
            // Get border thickness from config
UNCOV
1674
            int const thickness = _interval_config->border_thickness;
×
1675

1676
            QPen border_pen(plot_color);
×
UNCOV
1677
            border_pen.setWidth(thickness);
×
1678

1679
            // Draw border as 4 rectangles around the edges of the canvas
1680
            // Top border
UNCOV
1681
            auto top_border = addRect(0, 0, _canvasWidth, thickness, border_pen, QBrush(plot_color));
×
UNCOV
1682
            _intervals.append(top_border);
×
1683

1684
            // Bottom border
1685
            auto bottom_border = addRect(0, _canvasHeight - thickness, _canvasWidth, thickness, border_pen, QBrush(plot_color));
×
1686
            _intervals.append(bottom_border);
×
1687

1688
            // Left border
UNCOV
1689
            auto left_border = addRect(0, 0, thickness, _canvasHeight, border_pen, QBrush(plot_color));
×
UNCOV
1690
            _intervals.append(left_border);
×
1691

1692
            // Right border
UNCOV
1693
            auto right_border = addRect(_canvasWidth - thickness, 0, thickness, _canvasHeight, border_pen, QBrush(plot_color));
×
1694
            _intervals.append(right_border);
×
UNCOV
1695
        }
×
1696
    }
×
1697
}
16✔
1698

1699
void Media_Window::_plotTensorData() {
16✔
1700

1701
    auto const current_time = _data_manager->getCurrentTime();
16✔
1702

1703
    for (auto const & [key, config]: _tensor_configs) {
16✔
1704
        if (!config.get()->is_visible) continue;
×
1705

1706
        auto tensor_data = _data_manager->getData<TensorData>(key);
×
1707

UNCOV
1708
        auto tensor_shape = tensor_data->getFeatureShape();
×
1709

1710
        auto tensor_slice = tensor_data->getChannelSlice(TimeFrameIndex(current_time), config->display_channel);
×
1711

1712
        // Create a QImage from the tensor data
UNCOV
1713
        QImage tensor_image(static_cast<int>(tensor_shape[1]), static_cast<int>(tensor_shape[0]), QImage::Format::Format_ARGB32);
×
1714
        for (size_t y = 0; y < tensor_shape[0]; ++y) {
×
UNCOV
1715
            for (size_t x = 0; x < tensor_shape[1]; ++x) {
×
UNCOV
1716
                float const value = tensor_slice[y * tensor_shape[1] + x];
×
1717
                //int const pixel_value = static_cast<int>(value * 255);// Assuming the tensor values are normalized between 0 and 1
1718

1719
                // Use the config color with alpha
UNCOV
1720
                QColor const color(QString::fromStdString(config->hex_color));
×
1721
                int const alpha = std::lround(config->alpha * 255.0f * (value > 0 ? 1.0f : 0.0f));
×
UNCOV
1722
                QRgb const rgb = qRgba(color.red(), color.green(), color.blue(), alpha);
×
1723

1724
                tensor_image.setPixel(x, y, rgb);
×
1725
            }
1726
        }
1727

1728
        // Scale the tensor image to the size of the canvas
1729
        QImage const scaled_tensor_image = tensor_image.scaled(_canvasWidth, _canvasHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
×
1730

UNCOV
1731
        auto tensor_pixmap = addPixmap(QPixmap::fromImage(scaled_tensor_image));
×
1732

1733
        _tensors.append(tensor_pixmap);
×
1734
    }
×
1735
}
16✔
1736

UNCOV
1737
std::vector<uint8_t> Media_Window::getDrawingMask() {
×
1738
    // Create a QImage with _canvasWidth and _canvasHeight
1739
    QImage maskImage(_canvasWidth, _canvasHeight, QImage::Format_Grayscale8);
×
UNCOV
1740
    maskImage.fill(0);
×
1741

UNCOV
1742
    QPainter painter(&maskImage);
×
UNCOV
1743
    painter.setPen(Qt::white);
×
1744
    painter.setBrush(QBrush(Qt::white));// Fill the circles with white
×
1745

1746
    for (auto const & point: _drawing_points) {
×
1747
        // Draw a filled circle with the current brush size (hover circle radius)
UNCOV
1748
        float const radius = static_cast<float>(_hover_circle_radius);
×
UNCOV
1749
        painter.drawEllipse(point, radius, radius);
×
1750
    }
UNCOV
1751
    painter.end();
×
1752

1753
    // Scale the QImage to the size of the media
UNCOV
1754
    auto media = _data_manager->getData<MediaData>("media");
×
UNCOV
1755
    int const mediaWidth = media->getWidth();
×
UNCOV
1756
    int const mediaHeight = media->getHeight();
×
UNCOV
1757
    QImage scaledMaskImage = maskImage.scaled(mediaWidth, mediaHeight);
×
1758

1759
    // Convert the QImage to a std::vector<uint8_t>
UNCOV
1760
    std::vector<uint8_t> mask(scaledMaskImage.bits(), scaledMaskImage.bits() + scaledMaskImage.sizeInBytes());
×
1761

UNCOV
1762
    return mask;
×
UNCOV
1763
}
×
1764

1765
void Media_Window::setShowHoverCircle(bool show) {
19✔
1766
    _show_hover_circle = show;
19✔
1767
    if (_show_hover_circle) {
19✔
1768
        if (_debug_performance) {
2✔
UNCOV
1769
            std::cout << "Hover circle enabled" << std::endl;
×
1770
        }
1771

1772
        // Create the hover circle item if it doesn't exist
1773
        if (!_hover_circle_item) {
2✔
1774
            QPen circlePen(Qt::red);
2✔
1775
            circlePen.setWidth(2);
2✔
1776
            _hover_circle_item = addEllipse(0, 0, _hover_circle_radius * 2, _hover_circle_radius * 2, circlePen);
2✔
1777
            _hover_circle_item->setVisible(false);// Initially hidden until mouse moves
2✔
1778
            // DO NOT add to _points vector - hover circle is managed separately
1779
            if (_debug_performance) {
2✔
UNCOV
1780
                std::cout << "  Created new hover circle item" << std::endl;
×
1781
            }
1782
        }
2✔
1783

1784
        // Connect mouse move to efficient hover circle update instead of full canvas update
1785
        connect(this, &Media_Window::mouseMove, this, &Media_Window::_updateHoverCirclePosition);
2✔
1786
    } else {
1787
        if (_debug_performance) {
17✔
UNCOV
1788
            std::cout << "Hover circle disabled" << std::endl;
×
1789
        }
1790

1791
        // Remove the hover circle item
1792
        if (_hover_circle_item) {
17✔
1793
            removeItem(_hover_circle_item);
2✔
1794
            delete _hover_circle_item;
2✔
1795
            _hover_circle_item = nullptr;
2✔
1796
            if (_debug_performance) {
2✔
UNCOV
1797
                std::cout << "  Deleted hover circle item" << std::endl;
×
1798
            }
1799
        }
1800

1801
        // Disconnect the mouse move signal
1802
        disconnect(this, &Media_Window::mouseMove, this, &Media_Window::_updateHoverCirclePosition);
17✔
1803
    }
1804
}
19✔
1805

1806
void Media_Window::setHoverCircleRadius(int radius) {
2✔
1807
    _hover_circle_radius = radius;
2✔
1808

1809
    // Update the existing hover circle item if it exists
1810
    if (_hover_circle_item && _show_hover_circle) {
2✔
1811
        qreal const x = _hover_position.x() - _hover_circle_radius;
2✔
1812
        qreal const y = _hover_position.y() - _hover_circle_radius;
2✔
1813
        _hover_circle_item->setRect(x, y, _hover_circle_radius * 2, _hover_circle_radius * 2);
2✔
1814
    }
1815
}
2✔
1816

UNCOV
1817
void Media_Window::_updateHoverCirclePosition() {
×
1818
    static int call_count = 0;
1819
    call_count++;
×
1820

UNCOV
1821
    if (_hover_circle_item && _show_hover_circle) {
×
1822
        // Update the position of the existing hover circle item
1823
        qreal const x = _hover_position.x() - _hover_circle_radius;
×
1824
        qreal const y = _hover_position.y() - _hover_circle_radius;
×
1825
        _hover_circle_item->setRect(x, y, _hover_circle_radius * 2, _hover_circle_radius * 2);
×
UNCOV
1826
        _hover_circle_item->setVisible(true);
×
1827

1828
        if (_debug_performance) {
×
UNCOV
1829
            std::cout << "Hover circle updated (call #" << call_count << ") at ("
×
UNCOV
1830
                      << _hover_position.x() << ", " << _hover_position.y() << ")" << std::endl;
×
1831
        }
UNCOV
1832
    } else {
×
UNCOV
1833
        if (_debug_performance) {
×
UNCOV
1834
            std::cout << "Hover circle update skipped (call #" << call_count << ") - item: "
×
UNCOV
1835
                      << (_hover_circle_item ? "exists" : "null") << ", show: " << _show_hover_circle << std::endl;
×
1836
        }
1837
    }
1838
}
×
1839

1840
void Media_Window::setShowTemporaryLine(bool show) {
3✔
1841
    _show_temporary_line = show;
3✔
1842
    if (!_show_temporary_line) {
3✔
1843
        clearTemporaryLine();
3✔
1844
    }
1845
}
3✔
1846

1847
void Media_Window::updateTemporaryLine(std::vector<Point2D<float>> const & points, std::string const & line_key) {
×
UNCOV
1848
    if (!_show_temporary_line || points.empty()) {
×
UNCOV
1849
        return;
×
1850
    }
1851

1852
    // Clear existing temporary line
1853
    clearTemporaryLine();
×
1854

1855
    // Use the same aspect ratio calculation as the target line data
1856
    auto xAspect = getXAspect();
×
UNCOV
1857
    auto yAspect = getYAspect();
×
1858
    
1859
    // If we have a line key, use the same image size scaling as that line data
UNCOV
1860
    if (!line_key.empty()) {
×
1861
        auto line_data = _data_manager->getData<LineData>(line_key);
×
1862
        if (line_data) {
×
UNCOV
1863
            auto image_size = line_data->getImageSize();
×
1864
            
1865
        }
1866
    }
×
1867

1868
    if (points.size() < 2) {
×
1869
        // If only one point, just show a marker
1870
        // Convert media coordinates to canvas coordinates
1871
        float x = points[0].x * xAspect;
×
UNCOV
1872
        float y = points[0].y * yAspect;
×
1873
        
1874
        QPen pointPen(Qt::yellow);
×
UNCOV
1875
        pointPen.setWidth(2);
×
UNCOV
1876
        QBrush pointBrush(Qt::yellow);
×
1877
        
1878
        auto pointItem = addEllipse(x - 3, y - 3, 6, 6, pointPen, pointBrush);
×
UNCOV
1879
        _temporary_line_points.push_back(pointItem);
×
UNCOV
1880
        return;
×
1881
    }
×
1882

1883
    // Create path for the line
UNCOV
1884
    QPainterPath path;
×
1885

1886

1887
    // Move to first point (convert media coordinates to canvas coordinates)
1888
    path.moveTo(QPointF(points[0].x * xAspect, points[0].y * yAspect));
×
1889

1890
    // Add lines to subsequent points
UNCOV
1891
    for (size_t i = 1; i < points.size(); ++i) {
×
UNCOV
1892
        path.lineTo(QPointF(points[i].x * xAspect, points[i].y * yAspect));
×
1893
    }
1894

1895
    // Create the line path item
UNCOV
1896
    QPen linePen(Qt::yellow);
×
1897
    linePen.setWidth(2);
×
UNCOV
1898
    linePen.setStyle(Qt::DashLine); // Dashed line to distinguish from permanent lines
×
1899
    
1900
    _temporary_line_item = addPath(path, linePen);
×
1901

1902
    // Add point markers
1903
    QPen pointPen(Qt::yellow);
×
UNCOV
1904
    pointPen.setWidth(1);
×
1905
    QBrush pointBrush(Qt::NoBrush); // Open circles
×
1906

UNCOV
1907
    for (size_t i = 0; i < points.size(); ++i) {
×
1908
        // Convert media coordinates to canvas coordinates
UNCOV
1909
        float x = points[i].x * xAspect;
×
1910
        float y = points[i].y * yAspect;
×
1911
        
1912
        auto pointItem = addEllipse(x - 2.5, y - 2.5, 5, 5, pointPen, pointBrush);
×
UNCOV
1913
        _temporary_line_points.push_back(pointItem);
×
1914
    }
UNCOV
1915
}
×
1916

1917
void Media_Window::clearTemporaryLine() {
6✔
1918
    // Remove and delete the temporary line path
1919
    if (_temporary_line_item) {
6✔
UNCOV
1920
        removeItem(_temporary_line_item);
×
UNCOV
1921
        delete _temporary_line_item;
×
UNCOV
1922
        _temporary_line_item = nullptr;
×
1923
    }
1924

1925
    // Remove and delete all temporary line point markers
1926
    for (auto pointItem : _temporary_line_points) {
6✔
UNCOV
1927
        if (pointItem) {
×
UNCOV
1928
            removeItem(pointItem);
×
1929
            delete pointItem;
×
1930
        }
1931
    }
1932
    _temporary_line_points.clear();
6✔
1933
}
6✔
1934

1935
void Media_Window::_addRemoveData() {
1✔
1936
    //New data key was added. This is where we may want to repopulate a custom table
1937
}
1✔
1938

UNCOV
1939
bool Media_Window::_needsTimeFrameConversion(std::shared_ptr<TimeFrame> video_timeframe,
×
1940
                                             std::shared_ptr<TimeFrame> const & interval_timeframe) {
1941
    // If either timeframe is null, no conversion is possible/needed
1942
    if (!video_timeframe || !interval_timeframe) {
×
1943
        return false;
×
1944
    }
1945

1946
    // Conversion is needed if the timeframes are different objects
UNCOV
1947
    return video_timeframe.get() != interval_timeframe.get();
×
1948
}
1949

1950

UNCOV
1951
QRgb plot_color_with_alpha(BaseDisplayOptions const * opts) {
×
1952
    auto color = QColor(QString::fromStdString(opts->hex_color));
×
UNCOV
1953
    auto output_color = qRgba(color.red(), color.green(), color.blue(), std::lround(opts->alpha * 255.0f));
×
1954

1955
    return output_color;
×
1956
}
1957

UNCOV
1958
bool Media_Window::hasPreviewMaskData(std::string const & mask_key) const {
×
UNCOV
1959
    return _mask_preview_active && _preview_mask_data.count(mask_key) > 0;
×
1960
}
1961

UNCOV
1962
std::vector<Mask2D> Media_Window::getPreviewMaskData(std::string const & mask_key) const {
×
1963

1964
    if (hasPreviewMaskData(mask_key)) {
×
1965
        return _preview_mask_data.at(mask_key);
×
1966
    }
1967
    return {};
×
1968
}
1969

1970
void Media_Window::setPreviewMaskData(std::string const & mask_key,
×
1971
                                      std::vector<std::vector<Point2D<uint32_t>>> const & preview_data,
1972
                                      bool active) {
UNCOV
1973
    if (active) {
×
1974
        _preview_mask_data[mask_key] = preview_data;
×
1975
        _mask_preview_active = true;
×
1976
    } else {
1977
        _preview_mask_data.erase(mask_key);
×
UNCOV
1978
        _mask_preview_active = !_preview_mask_data.empty();
×
1979
    }
1980
}
×
1981

UNCOV
1982
void Media_Window::onGroupChanged() {
×
1983
    // Update the canvas when group assignments or properties change
1984
    UpdateCanvas();
×
UNCOV
1985
}
×
1986

1987
QColor Media_Window::_getGroupAwareColor(EntityId entity_id, QColor const & default_color) const {
×
1988
    // Handle selection highlighting first
UNCOV
1989
    if (_selected_entities.count(entity_id) > 0) {
×
1990
        return QColor(255, 255, 0); // Bright yellow for selected entities
×
1991
    }
1992
    
1993
    if (!_group_manager || entity_id == 0) {
×
UNCOV
1994
        return default_color;
×
1995
    }
1996
    
1997
    return _group_manager->getEntityColor(entity_id, default_color);
×
1998
}
1999

2000
bool Media_Window::_isEntityGroupVisible(EntityId entity_id) const {
×
2001
    if (!_group_manager || entity_id == 0) {
×
UNCOV
2002
        return true; // Entities not in a group or without group manager are always visible
×
2003
    }
2004
    
UNCOV
2005
    return _group_manager->isEntityGroupVisible(entity_id);
×
2006
}
2007

2008
QRgb Media_Window::_getGroupAwareColorRgb(EntityId entity_id, QRgb default_color) const {
×
2009
    // Handle selection highlighting first
2010
    if (_selected_entities.count(entity_id) > 0) {
×
2011
        return qRgba(255, 255, 0, 255); // Bright yellow for selected entities
×
2012
    }
2013
    
UNCOV
2014
    if (!_group_manager || entity_id == 0) {
×
2015
        return default_color;
×
2016
    }
2017
    
UNCOV
2018
    QColor group_color = _group_manager->getEntityColor(entity_id, QColor::fromRgba(default_color));
×
2019
    return group_color.rgba();
×
2020
}
2021

2022
// ===== Selection and Context Menu Implementation =====
2023

2024
void Media_Window::clearAllSelections() {
3✔
2025
    if (!_selected_entities.empty()) {
3✔
UNCOV
2026
        _selected_entities.clear();
×
UNCOV
2027
        _selected_data_key.clear();
×
UNCOV
2028
        _selected_data_type.clear();
×
UNCOV
2029
        UpdateCanvas(); // Refresh to remove selection highlights
×
2030
    }
2031
}
3✔
2032

UNCOV
2033
bool Media_Window::hasSelections() const {
×
UNCOV
2034
    return !_selected_entities.empty();
×
2035
}
2036

UNCOV
2037
std::unordered_set<EntityId> Media_Window::getSelectedEntities() const {
×
UNCOV
2038
    return _selected_entities;
×
2039
}
2040

2041
void Media_Window::setGroupSelectionEnabled(bool enabled) {
3✔
2042
    _group_selection_enabled = enabled;
3✔
2043
    if (!enabled) {
3✔
2044
        // Clear any existing selections when disabling group selection
2045
        clearAllSelections();
3✔
2046
    }
2047
}
3✔
2048

2049
bool Media_Window::isGroupSelectionEnabled() const {
×
UNCOV
2050
    return _group_selection_enabled;
×
2051
}
2052

2053
EntityId Media_Window::findPointAtPosition(QPointF const & scene_pos, std::string const & point_key) {
×
2054
    return _findPointAtPosition(scene_pos, point_key);
×
2055
}
2056

2057
EntityId Media_Window::findEntityAtPosition(QPointF const & scene_pos, std::string & data_key, std::string & data_type) {
×
2058
    return _findEntityAtPosition(scene_pos, data_key, data_type);
×
2059
}
2060

2061
void Media_Window::selectEntity(EntityId entity_id, std::string const & data_key, std::string const & data_type) {
×
2062
    _selected_entities.clear();
×
2063
    _selected_entities.insert(entity_id);
×
UNCOV
2064
    _selected_data_key = data_key;
×
UNCOV
2065
    _selected_data_type = data_type;
×
UNCOV
2066
    UpdateCanvas(); // Refresh to show selection
×
UNCOV
2067
}
×
2068

2069
EntityId Media_Window::_findEntityAtPosition(QPointF const & scene_pos, std::string & data_key, std::string & data_type) {
×
2070
    // Convert scene coordinates to media coordinates
2071
    float x_media = static_cast<float>(scene_pos.x() / getXAspect());
×
2072
    float y_media = static_cast<float>(scene_pos.y() / getYAspect());
×
2073

2074
    // Search through lines first (as they're typically most precise)
2075
    for (auto const & [key, config] : _line_configs) {
×
UNCOV
2076
        if (config->is_visible) {
×
UNCOV
2077
            EntityId entity_id = _findLineAtPosition(scene_pos, key);
×
UNCOV
2078
            if (entity_id != 0) {
×
UNCOV
2079
                data_key = key;
×
UNCOV
2080
                data_type = "line";
×
2081
                return entity_id;
×
2082
            }
2083
        }
2084
    }
2085

2086
    // Then search points
2087
    for (auto const & [key, config] : _point_configs) {
×
UNCOV
2088
        if (config->is_visible) {
×
UNCOV
2089
            EntityId entity_id = _findPointAtPosition(scene_pos, key);
×
UNCOV
2090
            if (entity_id != 0) {
×
UNCOV
2091
                data_key = key;
×
2092
                data_type = "point";
×
UNCOV
2093
                return entity_id;
×
2094
            }
2095
        }
2096
    }
2097

2098
    // Finally search masks (usually less precise)
UNCOV
2099
    for (auto const & [key, config] : _mask_configs) {
×
UNCOV
2100
        if (config->is_visible) {
×
2101
            EntityId entity_id = _findMaskAtPosition(scene_pos, key);
×
2102
            if (entity_id != 0) {
×
2103
                data_key = key;
×
UNCOV
2104
                data_type = "mask";
×
2105
                return entity_id;
×
2106
            }
2107
        }
2108
    }
2109

UNCOV
2110
    return 0; // No entity found
×
2111
}
2112

UNCOV
2113
EntityId Media_Window::_findLineAtPosition(QPointF const & scene_pos, std::string const & line_key) {
×
UNCOV
2114
    auto line_data = _data_manager->getData<LineData>(line_key);
×
2115
    if (!line_data) {
×
2116
        return 0;
×
2117
    }
2118

2119
    auto current_time = _data_manager->getCurrentTime();
×
UNCOV
2120
    auto const & lines = line_data->getAtTime(TimeFrameIndex(current_time));
×
UNCOV
2121
    auto const & entity_ids = line_data->getEntityIdsAtTime(TimeFrameIndex(current_time));
×
2122

2123
    if (lines.size() != entity_ids.size()) {
×
2124
        return 0;
×
2125
    }
2126

UNCOV
2127
    float const threshold = 10.0f; // pixels
×
2128

2129
    for (size_t i = 0; i < lines.size(); ++i) {
×
UNCOV
2130
        auto const & line = lines[i];
×
2131
        
2132
        // Check distance from each line segment
2133
        for (size_t j = 0; j < line.size(); ++j) {
×
2134
            if (j + 1 >= line.size()) continue;
×
2135
            
UNCOV
2136
            auto const & p1 = line[j];
×
UNCOV
2137
            auto const & p2 = line[j + 1];
×
2138
            
2139
            // Convert line points to scene coordinates
2140
            float x1_scene = p1.x * getXAspect();
×
UNCOV
2141
            float y1_scene = p1.y * getYAspect();
×
2142
            float x2_scene = p2.x * getXAspect();
×
2143
            float y2_scene = p2.y * getYAspect();
×
2144
            
2145
            // Calculate distance from click point to line segment
UNCOV
2146
            float dist = _calculateDistanceToLineSegment(
×
UNCOV
2147
                scene_pos.x(), scene_pos.y(),
×
2148
                x1_scene, y1_scene, x2_scene, y2_scene
2149
            );
2150
            
UNCOV
2151
            if (dist <= threshold) {
×
2152
                return entity_ids[i];
×
2153
            }
2154
        }
2155
    }
2156

UNCOV
2157
    return 0;
×
2158
}
×
2159

UNCOV
2160
EntityId Media_Window::_findPointAtPosition(QPointF const & scene_pos, std::string const & point_key) {
×
UNCOV
2161
    auto point_data = _data_manager->getData<PointData>(point_key);
×
2162
    if (!point_data) {
×
2163
        return 0;
×
2164
    }
2165

2166
    auto current_time = _data_manager->getCurrentTime();
×
2167
    auto const & points = point_data->getAtTime(TimeFrameIndex(current_time));
×
2168
    auto const & entity_ids = point_data->getEntityIdsAtTime(TimeFrameIndex(current_time));
×
2169

2170
    if (points.size() != entity_ids.size()) {
×
2171
        return 0;
×
2172
    }
2173

UNCOV
2174
    float const threshold = 15.0f; // pixels
×
2175

2176
    for (size_t i = 0; i < points.size(); ++i) {
×
UNCOV
2177
        auto const & point = points[i];
×
2178
        
2179
        // Convert point to scene coordinates
2180
        float x_scene = point.x * getXAspect();
×
2181
        float y_scene = point.y * getYAspect();
×
2182
        
2183
        // Calculate distance
2184
        float dx = scene_pos.x() - x_scene;
×
2185
        float dy = scene_pos.y() - y_scene;
×
UNCOV
2186
        float distance = std::sqrt(dx * dx + dy * dy);
×
2187
        
UNCOV
2188
        if (distance <= threshold) {
×
UNCOV
2189
            return entity_ids[i];
×
2190
        }
2191
    }
2192

2193
    return 0;
×
2194
}
×
2195

UNCOV
2196
EntityId Media_Window::_findMaskAtPosition(QPointF const & scene_pos, std::string const & mask_key) {
×
2197
    auto mask_data = _data_manager->getData<MaskData>(mask_key);
×
UNCOV
2198
    if (!mask_data) {
×
2199
        return 0;
×
2200
    }
2201

UNCOV
2202
    auto current_time = _data_manager->getCurrentTime();
×
2203
    auto const & masks = mask_data->getAtTime(TimeFrameIndex(current_time));
×
2204

2205
    // MaskData doesn't currently support EntityIds, so we'll use position-based indices for now
2206
    // This is a simplified implementation that can be improved when MaskData gets EntityId support
2207

2208
    float x_media = static_cast<float>(scene_pos.x() / getXAspect());
×
2209
    float y_media = static_cast<float>(scene_pos.y() / getYAspect());
×
2210

UNCOV
2211
    for (size_t i = 0; i < masks.size(); ++i) {
×
UNCOV
2212
        auto const & mask = masks[i];
×
2213
        
2214
        // Check if the point is inside any of the mask's polygons
UNCOV
2215
        for (auto const & point : mask) {
×
2216
            // Simple bounding box check for now (could be improved with proper point-in-polygon)
UNCOV
2217
            if (std::abs(static_cast<float>(point.x) - x_media) < 5.0f && 
×
UNCOV
2218
                std::abs(static_cast<float>(point.y) - y_media) < 5.0f) {
×
2219
                // Return a synthetic EntityId based on position and mask index
2220
                // This is temporary until MaskData supports proper EntityIds
UNCOV
2221
                return static_cast<EntityId>(1000000 + current_time * 1000 + i);
×
2222
            }
2223
        }
2224
    }
2225

UNCOV
2226
    return 0;
×
UNCOV
2227
}
×
2228

2229
void Media_Window::_createContextMenu() {
3✔
2230
    _context_menu = new QMenu();
3✔
2231
    
2232
    // Create actions
2233
    auto * create_group_action = new QAction("Create New Group", this);
3✔
2234
    auto * ungroup_action = new QAction("Ungroup Selected", this);
3✔
2235
    auto * clear_selection_action = new QAction("Clear Selection", this);
3✔
2236
    
2237
    // Add actions to menu
2238
    _context_menu->addAction(create_group_action);
3✔
2239
    _context_menu->addSeparator();
3✔
2240
    _context_menu->addAction(ungroup_action);
3✔
2241
    _context_menu->addSeparator();
3✔
2242
    _context_menu->addAction(clear_selection_action);
3✔
2243
    
2244
    // Connect actions
2245
    connect(create_group_action, &QAction::triggered, this, &Media_Window::onCreateNewGroup);
3✔
2246
    connect(ungroup_action, &QAction::triggered, this, &Media_Window::onUngroupSelected);
3✔
2247
    connect(clear_selection_action, &QAction::triggered, this, &Media_Window::onClearSelection);
3✔
2248
}
3✔
2249

2250
void Media_Window::_showContextMenu(QPoint const & global_pos) {
×
2251
    if (_context_menu) {
×
2252
        _context_menu->popup(global_pos);
×
2253
    }
2254
}
×
2255

2256
void Media_Window::_updateContextMenuActions() {
×
UNCOV
2257
    if (!_context_menu || !_group_manager) {
×
2258
        return;
×
2259
    }
2260

2261
    // Clear all dynamic actions by removing actions after the static ones
2262
    // The static menu structure is: Create New Group, Separator, Ungroup Selected, Separator, Clear Selection
2263
    // Everything after the second separator should be removed
2264
    auto actions = _context_menu->actions();
×
2265
    int separator_count = 0;
×
UNCOV
2266
    QList<QAction*> actions_to_remove;
×
2267
    
UNCOV
2268
    for (QAction* action : actions) {
×
2269
        if (action->isSeparator()) {
×
2270
            separator_count++;
×
2271
            if (separator_count > 2) {
×
UNCOV
2272
                actions_to_remove.append(action);
×
2273
            }
2274
        } else if (separator_count >= 2) {
×
2275
            // This is a dynamic action after the second separator
UNCOV
2276
            actions_to_remove.append(action);
×
2277
        }
2278
    }
2279
    
2280
    // Remove and delete the dynamic actions
UNCOV
2281
    for (QAction* action : actions_to_remove) {
×
2282
        _context_menu->removeAction(action);
×
UNCOV
2283
        action->deleteLater(); // Use deleteLater() for safer cleanup
×
2284
    }
2285

2286
    // Add dynamic group assignment actions
UNCOV
2287
    auto groups = _group_manager->getGroupsForContextMenu();
×
2288
    if (!groups.empty()) {
×
UNCOV
2289
        _context_menu->addSeparator();
×
2290
        
2291
        for (auto const & [group_id, group_name] : groups) {
×
2292
            auto * assign_action = new QAction(QString("Assign to %1").arg(group_name), this);
×
UNCOV
2293
            _context_menu->addAction(assign_action);
×
2294
            
2295
            connect(assign_action, &QAction::triggered, this, [this, group_id]() {
×
2296
                onAssignToGroup(group_id);
×
UNCOV
2297
            });
×
2298
        }
2299
    }
UNCOV
2300
}
×
2301

2302
float Media_Window::_calculateDistanceToLineSegment(float px, float py, float x1, float y1, float x2, float y2) {
×
UNCOV
2303
    float dx = x2 - x1;
×
2304
    float dy = y2 - y1;
×
2305
    
UNCOV
2306
    if (dx == 0 && dy == 0) {
×
2307
        // Point to point distance
2308
        float dpx = px - x1;
×
2309
        float dpy = py - y1;
×
2310
        return std::sqrt(dpx * dpx + dpy * dpy);
×
2311
    }
2312
    
2313
    float t = ((px - x1) * dx + (py - y1) * dy) / (dx * dx + dy * dy);
×
2314
    t = std::max(0.0f, std::min(1.0f, t));
×
2315
    
UNCOV
2316
    float projection_x = x1 + t * dx;
×
UNCOV
2317
    float projection_y = y1 + t * dy;
×
2318
    
2319
    float dist_x = px - projection_x;
×
2320
    float dist_y = py - projection_y;
×
2321
    
UNCOV
2322
    return std::sqrt(dist_x * dist_x + dist_y * dist_y);
×
2323
}
2324

2325
// Context menu slot implementations
UNCOV
2326
void Media_Window::onCreateNewGroup() {
×
UNCOV
2327
    if (!_group_manager || _selected_entities.empty()) {
×
2328
        return;
×
2329
    }
2330
    
UNCOV
2331
    int group_id = _group_manager->createGroupWithEntities(_selected_entities);
×
UNCOV
2332
    if (group_id != -1) {
×
2333
        clearAllSelections();
×
2334
    }
2335
}
2336

2337
void Media_Window::onAssignToGroup(int group_id) {
×
2338
    if (!_group_manager || _selected_entities.empty()) {
×
2339
        return;
×
2340
    }
2341
    
UNCOV
2342
    _group_manager->assignEntitiesToGroup(group_id, _selected_entities);
×
UNCOV
2343
    clearAllSelections();
×
2344
}
2345

UNCOV
2346
void Media_Window::onUngroupSelected() {
×
UNCOV
2347
    if (!_group_manager || _selected_entities.empty()) {
×
UNCOV
2348
        return;
×
2349
    }
2350
    
UNCOV
2351
    _group_manager->ungroupEntities(_selected_entities);
×
UNCOV
2352
    clearAllSelections();
×
2353
}
2354

UNCOV
2355
void Media_Window::onClearSelection() {
×
UNCOV
2356
    clearAllSelections();
×
UNCOV
2357
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc