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

paulmthompson / WhiskerToolbox / 18018707227

25 Sep 2025 07:35PM UTC coverage: 68.577% (-0.3%) from 68.889%
18018707227

push

github

paulmthompson
selection for lines appears to work well

11 of 59 new or added lines in 2 files covered. (18.64%)

797 existing lines in 9 files now uncovered.

42024 of 61280 relevant lines covered (68.58%)

1133.34 hits per line

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

17.31
/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 all items from the scene - this automatically removes and deletes all QGraphicsItems
68
    clear();
3✔
69

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

81
void Media_Window::addMediaDataToScene(std::string const & media_key) {
3✔
82
    auto media_config = std::make_unique<MediaDisplayOptions>();
3✔
83

84
    _media_configs[media_key] = std::move(media_config);
3✔
85

86
    UpdateCanvas();
3✔
87
}
6✔
88

89
void Media_Window::_clearMedia() {
15✔
90
    // Set to black
91
    _canvasImage.fill(Qt::black);
15✔
92
    _canvasPixmap->setPixmap(QPixmap::fromImage(_canvasImage));
15✔
93
}
15✔
94

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

101
    UpdateCanvas();
×
102
}
×
103

UNCOV
104
void Media_Window::addLineDataToScene(std::string const & line_key) {
×
UNCOV
105
    auto line_config = std::make_unique<LineDisplayOptions>();
×
106

107
    // Assign color based on the current number of line configs
UNCOV
108
    line_config->hex_color = DefaultDisplayValues::getColorForIndex(_line_configs.size());
×
109

UNCOV
110
    _line_configs[line_key] = std::move(line_config);
×
111

UNCOV
112
    UpdateCanvas();
×
UNCOV
113
}
×
114

115
void Media_Window::_clearLines() {
15✔
116
    for (auto pathItem: _line_paths) {
15✔
117
        removeItem(pathItem);
×
118
    }
119
    for (auto pathItem: _line_paths) {
15✔
120
        delete pathItem;
×
121
    }
122
    _line_paths.clear();
15✔
123
}
15✔
124

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

UNCOV
131
    UpdateCanvas();
×
UNCOV
132
}
×
133

134
void Media_Window::addMaskDataToScene(std::string const & mask_key) {
3✔
135
    auto mask_config = std::make_unique<MaskDisplayOptions>();
3✔
136

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

140
    _mask_configs[mask_key] = std::move(mask_config);
3✔
141
    UpdateCanvas();
3✔
142
}
6✔
143

144
void Media_Window::_clearMasks() {
15✔
145
    for (auto maskItem: _masks) {
15✔
146
        removeItem(maskItem);
×
147
    }
148

149
    for (auto maskItem: _masks) {
15✔
150
        delete maskItem;
×
151
    }
152
    _masks.clear();
15✔
153
}
15✔
154

155
void Media_Window::_clearMaskBoundingBoxes() {
15✔
156
    for (auto boundingBoxItem: _mask_bounding_boxes) {
15✔
157
        removeItem(boundingBoxItem);
×
158
    }
159

160
    for (auto boundingBoxItem: _mask_bounding_boxes) {
15✔
161
        delete boundingBoxItem;
×
162
    }
163
    _mask_bounding_boxes.clear();
15✔
164
}
15✔
165

166
void Media_Window::_clearMaskOutlines() {
15✔
167
    for (auto outlineItem: _mask_outlines) {
15✔
168
        removeItem(outlineItem);
×
169
    }
170

171
    for (auto outlineItem: _mask_outlines) {
15✔
172
        delete outlineItem;
×
173
    }
174
    _mask_outlines.clear();
15✔
175
}
15✔
176

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

183
    UpdateCanvas();
×
UNCOV
184
}
×
185

UNCOV
186
void Media_Window::addPointDataToScene(std::string const & point_key) {
×
187
    auto point_config = std::make_unique<PointDisplayOptions>();
×
188

189
    // Assign color based on the current number of point configs
UNCOV
190
    point_config->hex_color = DefaultDisplayValues::getColorForIndex(_point_configs.size());
×
191

UNCOV
192
    _point_configs[point_key] = std::move(point_config);
×
UNCOV
193
    UpdateCanvas();
×
194
}
×
195

196
void Media_Window::_clearPoints() {
15✔
197
    if (_debug_performance) {
15✔
UNCOV
198
        std::cout << "CLEARING POINTS - Count before: " << _points.size() << std::endl;
×
199
    }
200

201
    for (auto pathItem: _points) {
15✔
UNCOV
202
        removeItem(pathItem);
×
203
    }
204
    for (auto pathItem: _points) {
15✔
205
        delete pathItem;
×
206
    }
207
    _points.clear();
15✔
208

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

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

221
    UpdateCanvas();
×
UNCOV
222
}
×
223

224
void Media_Window::addDigitalIntervalSeries(std::string const & key) {
×
225
    auto interval_config = std::make_unique<DigitalIntervalDisplayOptions>();
×
226

227
    // Assign color based on the current number of interval configs
UNCOV
228
    interval_config->hex_color = DefaultDisplayValues::getColorForIndex(_interval_configs.size());
×
229

230
    _interval_configs[key] = std::move(interval_config);
×
UNCOV
231
    UpdateCanvas();
×
UNCOV
232
}
×
233

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

UNCOV
240
    UpdateCanvas();
×
UNCOV
241
}
×
242

243
void Media_Window::_clearIntervals() {
15✔
244
    for (auto item: _intervals) {
15✔
UNCOV
245
        removeItem(item);
×
246
    }
247

248
    for (auto item: _intervals) {
15✔
249
        delete item;
×
250
    }
251
    _intervals.clear();
15✔
252
}
15✔
253

254
void Media_Window::addTensorDataToScene(std::string const & tensor_key) {
×
255
    auto tensor_config = std::make_unique<TensorDisplayOptions>();
×
256

257
    // Assign color based on the current number of tensor configs
UNCOV
258
    tensor_config->hex_color = DefaultDisplayValues::getColorForIndex(_tensor_configs.size());
×
259

260
    _tensor_configs[tensor_key] = std::move(tensor_config);
×
261

UNCOV
262
    UpdateCanvas();
×
UNCOV
263
}
×
264

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

UNCOV
271
    UpdateCanvas();
×
UNCOV
272
}
×
273

274
void Media_Window::_clearTensors() {
15✔
275
    for (auto item: _tensors) {
15✔
UNCOV
276
        removeItem(item);
×
277
    }
278

279
    for (auto item: _tensors) {
15✔
280
        delete item;
×
281
    }
282
    _tensors.clear();
15✔
283
}
15✔
284

285
void Media_Window::setTextWidget(MediaText_Widget * text_widget) {
6✔
286
    _text_widget = text_widget;
6✔
287
}
6✔
288

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

305
void Media_Window::_plotTextOverlays() {
15✔
306
    if (!_text_widget) {
15✔
UNCOV
307
        return;
×
308
    }
309

310
    // Get enabled text overlays from the widget
311
    auto text_overlays = _text_widget->getEnabledTextOverlays();
15✔
312

313
    for (auto const & overlay: text_overlays) {
15✔
UNCOV
314
        if (!overlay.enabled) {
×
315
            continue;
×
316
        }
317

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

322
        // Create text item
UNCOV
323
        auto text_item = addText(overlay.text);
×
324

325
        // Set font and size
UNCOV
326
        QFont font = text_item->font();
×
UNCOV
327
        font.setPointSize(overlay.font_size);
×
UNCOV
328
        text_item->setFont(font);
×
329

330
        // Set color
UNCOV
331
        QColor const text_color(overlay.color);
×
332
        text_item->setDefaultTextColor(text_color);
×
333

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

339
        // Set position
UNCOV
340
        text_item->setPos(x_pos, y_pos);
×
341

342
        // Add to our collection for cleanup
UNCOV
343
        _text_items.append(text_item);
×
UNCOV
344
    }
×
345
}
15✔
346

347
void Media_Window::_clearTextOverlays() {
15✔
348
    for (auto text_item: _text_items) {
15✔
349
        removeItem(text_item);
×
350
    }
351
    for (auto text_item: _text_items) {
15✔
UNCOV
352
        delete text_item;
×
353
    }
354
    _text_items.clear();
15✔
355
}
15✔
356

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

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

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

377
    UpdateCanvas();
×
378
}
379

380
void Media_Window::UpdateCanvas() {
15✔
381

382
    if (_debug_performance) {
15✔
383
        std::cout << "========== Update Canvas called ==========" << std::endl;
×
384

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

399
    _clearLines();
15✔
400
    _clearPoints();
15✔
401
    _clearMasks();
15✔
402
    _clearMaskBoundingBoxes();
15✔
403
    _clearMaskOutlines();
15✔
404
    _clearIntervals();
15✔
405
    _clearTensors();
15✔
406
    _clearTextOverlays();
15✔
407
    _clearMedia();
15✔
408

409
    _plotMediaData();
15✔
410

411
    _plotLineData();
15✔
412

413
    _plotMaskData();
15✔
414

415
    _plotPointData();
15✔
416

417
    _plotDigitalIntervalSeries();
15✔
418

419
    _plotDigitalIntervalBorders();
15✔
420

421
    _plotTensorData();
15✔
422

423
    _plotTextOverlays();
15✔
424

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

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

441
    // Save the entire QGraphicsScene as an image
442
    QImage scene_image(_canvasWidth, _canvasHeight, QImage::Format_ARGB32);
15✔
443
    scene_image.fill(Qt::transparent);// Optional: fill with transparent background
15✔
444
    QPainter painter(&scene_image);
15✔
445

446
    // Set the scene rect to match the canvas dimensions
447
    this->setSceneRect(0, 0, _canvasWidth, _canvasHeight);
15✔
448

449
    // Render the scene with proper viewport mapping
450
    this->render(&painter, QRectF(0, 0, _canvasWidth, _canvasHeight),
45✔
451
                 QRect(0, 0, _canvasWidth, _canvasHeight));
30✔
452

453
    emit canvasUpdated(scene_image);
15✔
454
}
30✔
455

456

457
QImage::Format Media_Window::_getQImageFormat(std::string const & media_key) {
×
458

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

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

478
void Media_Window::_plotMediaData() {
15✔
479

480
    auto const current_time = _data_manager->getCurrentTime();
15✔
481

482
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
15✔
483

484
    int total_visible_media = 0;
15✔
485
    std::string active_media_key;
15✔
486
    for (auto const & [media_key, _media_config]: _media_configs) {
30✔
487
        if (!_media_config.get()->is_visible) continue;
15✔
488
        total_visible_media++;
×
489
        active_media_key = media_key;
×
490
    }
491

492
    if (total_visible_media == 0) {
15✔
493
        return;
15✔
494
    }
495

496
    QImage unscaled_image;
×
497

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

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

510
            if (media->is8Bit()) {
×
511
                // 8-bit grayscale processing
UNCOV
512
                auto unscaled_image_data_8bit = media->getProcessedData8(current_time);
×
513

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

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

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

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

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

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

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

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

591

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

598
    auto new_image = unscaled_image.scaled(
×
599
            _canvasWidth,
600
            _canvasHeight,
601
            Qt::IgnoreAspectRatio,
UNCOV
602
            Qt::SmoothTransformation);
×
603

UNCOV
604
    std::cout << "Scaled image" << std::endl;
×
605

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

615
    // If we have transparency masks, modify the new_image
616
    if (has_transparency_mask) {
×
UNCOV
617
        new_image = _applyTransparencyMasks(new_image);
×
618
    }
619

620
    _canvasPixmap->setPixmap(QPixmap::fromImage(new_image));
×
621
    _canvasImage = new_image;
×
622
}
30✔
623

624

UNCOV
625
QImage Media_Window::_combineMultipleMedia() {
×
626

627
    auto current_time = _data_manager->getCurrentTime();
×
628

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

634
        auto media = _data_manager->getData<MediaData>(media_key);
×
635
        if (!media) continue;
×
636

UNCOV
637
        media_sizes.push_back(media->getImageSize());
×
UNCOV
638
    }
×
639

640
    if (media_sizes.empty()) return QImage();
×
641

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

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

654
    for (auto const & [media_key, media_config]: _media_configs) {
×
UNCOV
655
        if (!media_config->is_visible) continue;
×
656

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

662
        bool apply_colormap = media_config.get()->colormap_options.active &&
×
UNCOV
663
                              media_config.get()->colormap_options.colormap != ColormapType::None;
×
664

665
        if (media->is8Bit()) {
×
666
            // Handle 8-bit media data
667
            auto media_data_8bit = media->getProcessedData8(current_time);
×
668

669
            if (apply_colormap) {
×
670
                auto colormap_data = ImageProcessing::apply_colormap_for_display(
×
671
                        media_data_8bit,
672
                        media->getImageSize(),
UNCOV
673
                        media_config.get()->colormap_options);
×
674

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

680
                        uint8_t const b = colormap_data[pixel_idx];    // Blue channel
×
UNCOV
681
                        uint8_t const g = colormap_data[pixel_idx + 1];// Green channel
×
682
                        uint8_t const r = colormap_data[pixel_idx + 2];// Red channel
×
UNCOV
683
                        uint8_t const a = colormap_data[pixel_idx + 3];// Alpha channel
×
684

685
                        // Get current pixel from combined image
UNCOV
686
                        QRgb current_pixel = combined_image.pixel(x, y);
×
687

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

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

703
                        // Get current pixel from combined image
704
                        QRgb current_pixel = combined_image.pixel(x, y);
×
705

706
                        // Additive blending
UNCOV
707
                        uint8_t const new_r = std::min(255, qRed(current_pixel) + gray_value);
×
708
                        uint8_t const new_g = std::min(255, qGreen(current_pixel) + gray_value);
×
UNCOV
709
                        uint8_t const new_b = std::min(255, qBlue(current_pixel) + gray_value);
×
710

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

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

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

729
                auto colormap_data = ImageProcessing::apply_colormap_for_display(
×
730
                        converted_8bit,
731
                        media->getImageSize(),
UNCOV
732
                        media_config.get()->colormap_options);
×
733

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

739
                        uint8_t const b = colormap_data[pixel_idx];    // Blue channel
×
UNCOV
740
                        uint8_t const g = colormap_data[pixel_idx + 1];// Green channel
×
741
                        uint8_t const r = colormap_data[pixel_idx + 2];// Red channel
×
UNCOV
742
                        uint8_t const a = colormap_data[pixel_idx + 3];// Alpha channel
×
743

744
                        // Get current pixel from combined image
UNCOV
745
                        QRgb current_pixel = combined_image.pixel(x, y);
×
746

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

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

763
                        // Get current pixel from combined image
764
                        QRgb current_pixel = combined_image.pixel(x, y);
×
765

766
                        // Additive blending
767
                        uint8_t const new_r = std::min(255, qRed(current_pixel) + gray_value);
×
768
                        uint8_t const new_g = std::min(255, qGreen(current_pixel) + gray_value);
×
UNCOV
769
                        uint8_t const new_b = std::min(255, qBlue(current_pixel) + gray_value);
×
770

771
                        combined_image.setPixel(x, y, qRgba(new_r, new_g, new_b, 255));
×
772
                    }
773
                }
774
            }
UNCOV
775
        }
×
776
    }
×
777

778
    return combined_image;
×
779
}
×
780

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

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

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

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

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

854
        // Emit legacy signals (qreal values)
UNCOV
855
        emit rightClick(event->scenePos().x(), event->scenePos().y());
×
856
        emit rightClickMedia(
×
857
                event->scenePos().x() / getXAspect(),
×
858
                event->scenePos().y() / getYAspect());
×
859

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

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

UNCOV
878
    if (event->button() == Qt::LeftButton) {
×
879
        // Always emit leftRelease signal
880
        emit leftRelease();
×
881

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

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

906
    auto pos = event->scenePos();
×
907

908
    _hover_position = pos;
×
909

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

920
    // Emit legacy signal
UNCOV
921
    emit mouseMove(event->scenePos().x(), event->scenePos().y());
×
922

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

UNCOV
928
    QGraphicsScene::mouseMoveEvent(event);
×
UNCOV
929
}
×
930

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

UNCOV
938
    _updateContextMenuActions();
×
UNCOV
939
    _showContextMenu(event->screenPos());
×
940
}
941

942
float Media_Window::getXAspect() const {
15✔
943

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

UNCOV
956
    auto _media = _data_manager->getData<MediaData>(active_media_key);
×
957
    if (!_media) {
×
UNCOV
958
        return 1.0f;// Default aspect ratio
×
959
    }
960

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

UNCOV
963
    return scale_width;
×
964
}
15✔
965

966
float Media_Window::getYAspect() const {
15✔
967

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

980
    auto _media = _data_manager->getData<MediaData>(active_media_key);
×
981
    if (!_media) {
×
UNCOV
982
        return 1.0f;// Default aspect ratio
×
983
    }
984

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

UNCOV
987
    return scale_height;
×
988
}
15✔
989

990
void Media_Window::_plotLineData() {
15✔
991
    auto const current_time = _data_manager->getCurrentTime();
15✔
992

993
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
15✔
994

995
    auto xAspect = getXAspect();
15✔
996
    auto yAspect = getYAspect();
15✔
997

998
    for (auto const & [line_key, _line_config]: _line_configs) {
15✔
999

UNCOV
1000
        if (!_line_config.get()->is_visible) continue;
×
1001

1002
        auto plot_color = plot_color_with_alpha(_line_config.get());
×
1003

UNCOV
1004
        auto line_timeframe_key = _data_manager->getTimeKey(line_key);
×
1005
        auto line_timeframe = _data_manager->getTime(line_timeframe_key);
×
1006

1007
        auto line_data = _data_manager->getData<LineData>(line_key);
×
UNCOV
1008
        auto lineData = line_data->getAtTime(TimeFrameIndex(current_time), video_timeframe.get(), line_timeframe.get());
×
1009
        auto entityIds = line_data->getEntityIdsAtTime(TimeFrameIndex(current_time), video_timeframe.get(), line_timeframe.get());
×
1010

1011
        // Check for line-specific image size scaling
1012
        auto image_size = line_data->getImageSize();
×
1013

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

UNCOV
1019
        if (image_size.width != -1) {
×
UNCOV
1020
            auto const line_width = static_cast<float>(image_size.width);
×
UNCOV
1021
            xAspect = static_cast<float>(_canvasWidth) / line_width;
×
1022
        }
1023

1024
        if (lineData.empty()) {
×
1025
            continue;
×
1026
        }
1027

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

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

UNCOV
1035
            if (single_line.empty()) {
×
UNCOV
1036
                continue;
×
1037
            }
1038

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

1042
            // Use segment if enabled, otherwise use full line
UNCOV
1043
            Line2D line_to_plot;
×
1044
            if (_line_config.get()->show_segment) {
×
UNCOV
1045
                float const start_percentage = static_cast<float>(_line_config.get()->segment_start_percentage) / 100.0f;
×
1046
                float const end_percentage = static_cast<float>(_line_config.get()->segment_end_percentage) / 100.0f;
×
1047
                line_to_plot = get_segment_between_percentages(single_line, start_percentage, end_percentage);
×
1048

1049
                // If segment is empty (invalid percentages), skip this line
1050
                if (line_to_plot.empty()) {
×
UNCOV
1051
                    continue;
×
1052
                }
1053
            } else {
1054
                line_to_plot = single_line;
×
1055
            }
1056

UNCOV
1057
            QPainterPath path = QPainterPath();
×
1058

UNCOV
1059
            auto single_line_thres = 1000.0;
×
1060

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

UNCOV
1063
            for (size_t i = 1; i < line_to_plot.size(); i++) {
×
UNCOV
1064
                auto dx = line_to_plot[i].x - line_to_plot[i - 1].x;
×
1065
                auto dy = line_to_plot[i].y - line_to_plot[i - 1].y;
×
1066
                auto d = std::sqrt((dx * dx) + (dy * dy));
×
1067
                if (d > single_line_thres) {
×
UNCOV
1068
                    path.moveTo(QPointF(static_cast<float>(line_to_plot[i].x) * xAspect, static_cast<float>(line_to_plot[i].y) * yAspect));
×
1069
                } else {
1070
                    path.lineTo(QPointF(static_cast<float>(line_to_plot[i].x) * xAspect, static_cast<float>(line_to_plot[i].y) * yAspect));
×
1071
                }
1072
            }
1073

1074
            // Create pen with group-aware color and configurable thickness
1075
            QPen linePen;
×
NEW
1076
            linePen.setColor(line_color);
×
NEW
1077
            linePen.setWidth(_line_config.get()->line_thickness);
×
1078

1079
            auto linePath = addPath(path, linePen);
×
1080
            _line_paths.append(linePath);
×
1081

1082
            // Add dot at line base (always filled) - use group-aware color
NEW
1083
            QColor const dot_color = line_color;
×
UNCOV
1084
            auto ellipse = addEllipse(
×
UNCOV
1085
                    static_cast<float>(line_to_plot[0].x) * xAspect - 2.5,
×
UNCOV
1086
                    static_cast<float>(line_to_plot[0].y) * yAspect - 2.5,
×
1087
                    5.0, 5.0,
UNCOV
1088
                    QPen(dot_color),
×
UNCOV
1089
                    QBrush(dot_color));
×
UNCOV
1090
            _points.append(ellipse);
×
1091

1092
            // If show_points is enabled, add open circles at each point on the line
1093
            if (_line_config.get()->show_points) {
×
1094
                // Create pen and brush for open circles
1095
                QPen pointPen(dot_color);
×
1096
                pointPen.setWidth(1);
×
1097

1098
                // Empty brush for open circles
1099
                QBrush const emptyBrush(Qt::NoBrush);
×
1100

1101
                // Start from the second point (first one is already shown as filled)
1102
                for (size_t i = 1; i < line_to_plot.size(); i++) {
×
1103
                    auto ellipse = addEllipse(
×
UNCOV
1104
                            static_cast<float>(line_to_plot[i].x) * xAspect - 2.5,
×
1105
                            static_cast<float>(line_to_plot[i].y) * yAspect - 2.5,
×
1106
                            5.0, 5.0,
1107
                            pointPen,
1108
                            emptyBrush);
UNCOV
1109
                    _points.append(ellipse);
×
1110
                }
1111
            }
×
1112

1113
            // If position marker is enabled, add a marker at the specified percentage
UNCOV
1114
            if (_line_config.get()->show_position_marker) {
×
1115
                float const percentage = static_cast<float>(_line_config.get()->position_percentage) / 100.0f;
×
1116
                Point2D<float> const marker_pos = get_position_at_percentage(line_to_plot, percentage);
×
1117

UNCOV
1118
                float const marker_x = marker_pos.x * xAspect;
×
1119
                float const marker_y = marker_pos.y * yAspect;
×
1120

1121
                // Create a distinctive marker (filled circle with border)
1122
                QPen markerPen(QColor(255, 255, 255));// White border
×
UNCOV
1123
                markerPen.setWidth(2);
×
UNCOV
1124
                QBrush const markerBrush(dot_color);// Same color as line but filled
×
1125

1126
                auto marker = addEllipse(
×
1127
                        marker_x - 4.0f,
×
1128
                        marker_y - 4.0f,
×
1129
                        8.0f, 8.0f,
1130
                        markerPen,
1131
                        markerBrush);
1132
                _points.append(marker);
×
1133
            }
×
1134
        }
×
1135
    }
×
1136
}
30✔
1137

1138
void Media_Window::_plotMaskData() {
15✔
1139
    // Note: MaskData does not currently support EntityIds for group-aware coloring
1140
    // This would need to be implemented similar to PointData and LineData
1141
    auto const current_time = _data_manager->getCurrentTime();
15✔
1142

1143
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
15✔
1144

1145
    for (auto const & [mask_key, _mask_config]: _mask_configs) {
27✔
1146
        if (!_mask_config.get()->is_visible) continue;
12✔
1147

UNCOV
1148
        auto plot_color = plot_color_with_alpha(_mask_config.get());
×
1149

1150
        auto mask = _data_manager->getData<MaskData>(mask_key);
×
1151
        auto image_size = mask->getImageSize();
×
1152

1153
        auto mask_timeframe_key = _data_manager->getTimeKey(mask_key);
×
UNCOV
1154
        auto mask_timeframe = _data_manager->getTime(mask_timeframe_key);
×
1155

1156
        // Check for preview data first
1157
        std::vector<Mask2D> maskData;
×
1158
        std::vector<Mask2D> maskData2;
×
1159

UNCOV
1160
        if (_mask_preview_active && _preview_mask_data.count(mask_key) > 0) {
×
1161
            // Use preview data
1162
            maskData = _preview_mask_data[mask_key];
×
1163
            maskData2.clear();// No time -1 data for preview
×
1164
        } else {
1165
            // Use original data
1166
            maskData = mask->getAtTime(TimeFrameIndex(current_time), video_timeframe.get(), mask_timeframe.get());
×
UNCOV
1167
            maskData2 = mask->getAtTime(TimeFrameIndex(-1));
×
1168
        }
1169

UNCOV
1170
        _plotSingleMaskData(maskData, image_size, plot_color, _mask_config.get());
×
UNCOV
1171
        _plotSingleMaskData(maskData2, image_size, plot_color, _mask_config.get());
×
1172

1173
        // Plot bounding boxes if enabled
1174
        if (_mask_config.get()->show_bounding_box) {
×
1175
            // Calculate scaling factors based on mask image size, not media aspect ratio
1176
            float const xAspect = static_cast<float>(_canvasWidth) / static_cast<float>(image_size.width);
×
UNCOV
1177
            float const yAspect = static_cast<float>(_canvasHeight) / static_cast<float>(image_size.height);
×
1178

1179
            // For current time masks
1180
            for (auto const & single_mask: maskData) {
×
UNCOV
1181
                if (!single_mask.empty()) {
×
1182
                    auto bounding_box = get_bounding_box(single_mask);
×
UNCOV
1183
                    auto min_point = bounding_box.first;
×
1184
                    auto max_point = bounding_box.second;
×
1185

1186
                    // Scale coordinates to canvas using mask image size
UNCOV
1187
                    float const min_x = static_cast<float>(min_point.x) * xAspect;
×
1188
                    float const min_y = static_cast<float>(min_point.y) * yAspect;
×
UNCOV
1189
                    float const max_x = static_cast<float>(max_point.x) * xAspect;
×
UNCOV
1190
                    float const max_y = static_cast<float>(max_point.y) * yAspect;
×
1191

1192
                    // Draw bounding box rectangle (no fill, just outline)
1193
                    QPen boundingBoxPen(plot_color);
×
UNCOV
1194
                    boundingBoxPen.setWidth(2);
×
1195
                    QBrush const emptyBrush(Qt::NoBrush);
×
1196

1197
                    auto boundingBoxRect = addRect(min_x, min_y, max_x - min_x, max_y - min_y,
×
1198
                                                   boundingBoxPen, emptyBrush);
1199
                    _mask_bounding_boxes.append(boundingBoxRect);
×
UNCOV
1200
                }
×
1201
            }
1202

1203
            // For time -1 masks
1204
            for (auto const & single_mask: maskData2) {
×
UNCOV
1205
                if (!single_mask.empty()) {
×
UNCOV
1206
                    auto bounding_box = get_bounding_box(single_mask);
×
1207
                    auto min_point = bounding_box.first;
×
UNCOV
1208
                    auto max_point = bounding_box.second;
×
1209

1210
                    // Scale coordinates to canvas using mask image size
UNCOV
1211
                    float const min_x = static_cast<float>(min_point.x) * xAspect;
×
UNCOV
1212
                    float const min_y = static_cast<float>(min_point.y) * yAspect;
×
1213
                    float const max_x = static_cast<float>(max_point.x) * xAspect;
×
UNCOV
1214
                    float const max_y = static_cast<float>(max_point.y) * yAspect;
×
1215

1216
                    // Draw bounding box rectangle (no fill, just outline)
UNCOV
1217
                    QPen boundingBoxPen(plot_color);
×
1218
                    boundingBoxPen.setWidth(2);
×
1219
                    QBrush const emptyBrush(Qt::NoBrush);
×
1220

UNCOV
1221
                    auto boundingBoxRect = addRect(min_x, min_y, max_x - min_x, max_y - min_y,
×
1222
                                                   boundingBoxPen, emptyBrush);
UNCOV
1223
                    _mask_bounding_boxes.append(boundingBoxRect);
×
1224
                }
×
1225
            }
1226
        }
1227

1228
        // Plot outlines if enabled
UNCOV
1229
        if (_mask_config.get()->show_outline) {
×
1230
            // Create a slightly darker color for outlines
1231
            QRgb const outline_color = plot_color;
×
1232

1233
            // For current time masks
1234
            for (auto const & single_mask: maskData) {
×
UNCOV
1235
                if (!single_mask.empty()) {
×
1236
                    // Generate outline mask with thickness of 2 pixels
UNCOV
1237
                    auto outline_mask = generate_outline_mask(single_mask, 2, image_size.width, image_size.height);
×
1238

UNCOV
1239
                    if (!outline_mask.empty()) {
×
1240
                        // Plot the outline mask using the same approach as regular masks
1241
                        _plotSingleMaskData({outline_mask}, image_size, outline_color, _mask_config.get());
×
1242
                    }
UNCOV
1243
                }
×
1244
            }
1245

1246
            // For time -1 masks
UNCOV
1247
            for (auto const & single_mask: maskData2) {
×
UNCOV
1248
                if (!single_mask.empty()) {
×
1249
                    // Generate outline mask with thickness of 2 pixels
1250
                    auto outline_mask = generate_outline_mask(single_mask, 2, image_size.width, image_size.height);
×
1251

1252
                    if (!outline_mask.empty()) {
×
1253
                        // Plot the outline mask using the same approach as regular masks
UNCOV
1254
                        _plotSingleMaskData({outline_mask}, image_size, outline_color, _mask_config.get());
×
1255
                    }
1256
                }
×
1257
            }
1258
        }
UNCOV
1259
    }
×
1260
}
30✔
1261

UNCOV
1262
void Media_Window::_plotSingleMaskData(std::vector<Mask2D> const & maskData, ImageSize mask_size, QRgb plot_color, MaskDisplayOptions const * mask_config) {
×
1263
    // Skip transparency masks as they are handled at the media level
UNCOV
1264
    if (mask_config && mask_config->use_as_transparency) {
×
UNCOV
1265
        return;
×
1266
    }
1267

UNCOV
1268
    for (auto const & single_mask: maskData) {
×
1269
        // Normal mode: overlay mask on top of media
UNCOV
1270
        QImage unscaled_mask_image(mask_size.width, mask_size.height, QImage::Format::Format_ARGB32);
×
1271
        unscaled_mask_image.fill(0);
×
1272

UNCOV
1273
        for (auto const point: single_mask) {
×
1274
            unscaled_mask_image.setPixel(
×
1275
                    QPoint(static_cast<int>(point.x), static_cast<int>(point.y)),
×
1276
                    plot_color);
1277
        }
1278

UNCOV
1279
        auto scaled_mask_image = unscaled_mask_image.scaled(_canvasWidth, _canvasHeight);
×
UNCOV
1280
        auto maskPixmap = addPixmap(QPixmap::fromImage(scaled_mask_image));
×
UNCOV
1281
        _masks.append(maskPixmap);
×
1282
    }
×
1283
}
1284

1285
QImage Media_Window::_applyTransparencyMasks(QImage const & media_image) {
×
1286
    std::cout << "Applying transparency masks..." << std::endl;
×
1287

UNCOV
1288
    std::cout << "Media image size: " << media_image.width() << "x" << media_image.height() << std::endl;
×
1289
    std::cout << "Canvas dimensions: " << _canvasWidth << "x" << _canvasHeight << std::endl;
×
1290

UNCOV
1291
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
×
1292

1293
    QImage final_image = media_image;
×
1294

UNCOV
1295
    int transparency_mask_count = 0;
×
1296
    int const total_mask_points = 0;
×
1297

1298
    // Process all transparency masks
UNCOV
1299
    for (auto const & [mask_key, mask_config]: _mask_configs) {
×
UNCOV
1300
        if (!mask_config->is_visible || !mask_config->use_as_transparency) {
×
UNCOV
1301
            continue;
×
1302
        }
1303

UNCOV
1304
        transparency_mask_count++;
×
1305
        std::cout << "Processing transparency mask: " << mask_key << std::endl;
×
1306

UNCOV
1307
        auto mask_data = _data_manager->getData<MaskData>(mask_key);
×
UNCOV
1308
        auto image_size = mask_data->getImageSize();
×
1309

1310
        auto mask_timeframe_key = _data_manager->getTimeKey(mask_key);
×
UNCOV
1311
        auto mask_timeframe = _data_manager->getTime(mask_timeframe_key);
×
1312

UNCOV
1313
        std::cout << "Mask image size: " << image_size.width << "x" << image_size.height << std::endl;
×
1314

UNCOV
1315
        auto const current_time = _data_manager->getCurrentTime();
×
1316
        auto maskData = mask_data->getAtTime(TimeFrameIndex(current_time), video_timeframe.get(), mask_timeframe.get());
×
1317

1318
        std::cout << "Mask data size: " << maskData.size() << std::endl;
×
1319

1320
        // Calculate scaling factors
UNCOV
1321
        float const xAspect = static_cast<float>(_canvasWidth) / static_cast<float>(image_size.width);
×
1322
        float const yAspect = static_cast<float>(_canvasHeight) / static_cast<float>(image_size.height);
×
1323

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

UNCOV
1326
        QImage unscaled_mask_image(image_size.width, image_size.height, QImage::Format::Format_ARGB32);
×
1327
        unscaled_mask_image.fill(0);
×
1328
        // Add mask points to combined mask
1329
        for (auto const & single_mask: maskData) {
×
1330
            for (auto const point: single_mask) {
×
1331
                unscaled_mask_image.setPixel(
×
UNCOV
1332
                        QPoint(static_cast<int>(point.x), static_cast<int>(point.y)),
×
1333
                        qRgba(255, 255, 255, 255));
1334
            }
1335
        }
1336

UNCOV
1337
        QImage const scaled_mask_image = unscaled_mask_image.scaled(_canvasWidth, _canvasHeight);
×
1338
        // I want to copy final_image where scaled_mask_image is white, and keep the rest of the image the same
1339
        for (int y = 0; y < _canvasHeight; ++y) {
×
1340
            for (int x = 0; x < _canvasWidth; ++x) {
×
UNCOV
1341
                if (scaled_mask_image.pixel(x, y) == qRgba(255, 255, 255, 255)) {
×
UNCOV
1342
                    final_image.setPixel(x, y, final_image.pixel(x, y));
×
1343
                } else {
UNCOV
1344
                    final_image.setPixel(x, y, qRgba(0, 0, 0, 255));
×
1345
                }
1346
            }
1347
        }
1348
    }
×
1349

1350

UNCOV
1351
    return final_image;
×
1352
}
×
1353

1354
void Media_Window::_plotPointData() {
15✔
1355

1356
    auto const current_time = TimeFrameIndex(_data_manager->getCurrentTime());
15✔
1357
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
15✔
1358

1359
    if (!video_timeframe) {
15✔
1360
        std::cerr << "Error: Could not get video timeframe 'time' for point conversion" << std::endl;
×
1361
        return;
×
1362
    }
1363

1364
    for (auto const & [point_key, _point_config]: _point_configs) {
15✔
UNCOV
1365
        if (!_point_config.get()->is_visible) continue;
×
1366

1367
        auto plot_color = plot_color_with_alpha(_point_config.get());
×
1368

1369
        auto point = _data_manager->getData<PointData>(point_key);
×
1370

1371
        auto point_timeframe_key = _data_manager->getTimeKey(point_key);
×
1372
        if (point_timeframe_key.empty()) {
×
1373
            std::cerr << "Error: No timeframe found for point data: " << point_key << std::endl;
×
UNCOV
1374
            continue;
×
1375
        }
1376

1377
        auto point_timeframe = _data_manager->getTime(point_timeframe_key);
×
1378

1379
        auto xAspect = getXAspect();
×
1380
        auto yAspect = getYAspect();
×
1381

UNCOV
1382
        auto image_size = point->getImageSize();
×
1383

1384
        if (image_size.height != -1) {
×
1385
            auto const mask_height = static_cast<float>(image_size.height);
×
1386
            yAspect = static_cast<float>(_canvasHeight) / mask_height;
×
1387
        }
1388

UNCOV
1389
        if (image_size.width != -1) {
×
1390
            auto const mask_width = static_cast<float>(image_size.width);
×
1391
            xAspect = static_cast<float>(_canvasWidth) / mask_width;
×
1392
        }
1393

1394
        auto pointData = point->getAtTime(current_time, video_timeframe.get(), point_timeframe.get());
×
1395
        auto entityIds = point->getEntityIdsAtTime(current_time);
×
1396

1397
        // Get configurable point size
1398
        float const point_size = static_cast<float>(_point_config.get()->point_size);
×
1399

1400
        // Ensure we have matching point data and entity IDs
1401
        size_t count = std::min(pointData.size(), entityIds.size());
×
1402
        
UNCOV
1403
        for (size_t i = 0; i < count; ++i) {
×
1404
            auto const & single_point = pointData[i];
×
1405
            EntityId entity_id = entityIds[i];
×
1406
            
1407
            float const x_pos = single_point.x * xAspect;
×
1408
            float const y_pos = single_point.y * yAspect;
×
1409

1410
            // Use group-aware color if available, otherwise use default plot color
UNCOV
1411
            QColor point_color = _getGroupAwareColor(entity_id, QColor::fromRgba(plot_color));
×
1412
            
1413
            // Check if this point is selected to add highlight
1414
            bool is_selected = _selected_entities.count(entity_id) > 0;
×
1415
            
1416
            // Add selection highlight if point is selected
UNCOV
1417
            if (is_selected) {
×
UNCOV
1418
                QPen highlight_pen(Qt::yellow);
×
1419
                highlight_pen.setWidth(4);
×
1420
                QBrush highlight_brush(Qt::transparent);
×
1421
                auto highlight_circle = addEllipse(x_pos - point_size, y_pos - point_size,
×
1422
                                                   point_size * 2, point_size * 2, 
×
1423
                                                   highlight_pen, highlight_brush);
1424
                _points.append(highlight_circle);
×
1425
            }
×
1426

1427
            // Create the appropriate marker shape based on configuration
UNCOV
1428
            switch (_point_config.get()->marker_shape) {
×
UNCOV
1429
                case PointMarkerShape::Circle: {
×
1430
                    QPen pen(point_color);
×
1431
                    pen.setWidth(2);
×
1432
                    QBrush const brush(point_color);
×
1433
                    auto ellipse = addEllipse(x_pos - point_size / 2, y_pos - point_size / 2,
×
1434
                                              point_size, point_size, pen, brush);
1435
                    _points.append(ellipse);
×
UNCOV
1436
                    break;
×
1437
                }
×
1438
                case PointMarkerShape::Square: {
×
1439
                    QPen pen(point_color);
×
1440
                    pen.setWidth(2);
×
UNCOV
1441
                    QBrush const brush(point_color);
×
UNCOV
1442
                    auto rect = addRect(x_pos - point_size / 2, y_pos - point_size / 2,
×
1443
                                        point_size, point_size, pen, brush);
UNCOV
1444
                    _points.append(rect);
×
UNCOV
1445
                    break;
×
UNCOV
1446
                }
×
UNCOV
1447
                case PointMarkerShape::Triangle: {
×
UNCOV
1448
                    QPen pen(point_color);
×
UNCOV
1449
                    pen.setWidth(2);
×
UNCOV
1450
                    QBrush const brush(point_color);
×
1451

1452
                    // Create triangle polygon
UNCOV
1453
                    QPolygonF triangle;
×
1454
                    float const half_size = point_size / 2;
×
UNCOV
1455
                    triangle << QPointF(x_pos, y_pos - half_size)             // Top point
×
1456
                             << QPointF(x_pos - half_size, y_pos + half_size) // Bottom left
×
UNCOV
1457
                             << QPointF(x_pos + half_size, y_pos + half_size);// Bottom right
×
1458

UNCOV
1459
                    auto polygon = addPolygon(triangle, pen, brush);
×
UNCOV
1460
                    _points.append(polygon);
×
1461
                    break;
×
1462
                }
×
1463
                case PointMarkerShape::Cross: {
×
1464
                    QPen pen(point_color);
×
UNCOV
1465
                    pen.setWidth(3);
×
1466

1467
                    float const half_size = point_size / 2;
×
1468
                    // Draw horizontal line
1469
                    auto hLine = addLine(x_pos - half_size, y_pos, x_pos + half_size, y_pos, pen);
×
1470
                    _points.append(hLine);
×
1471

1472
                    // Draw vertical line
1473
                    auto vLine = addLine(x_pos, y_pos - half_size, x_pos, y_pos + half_size, pen);
×
1474
                    _points.append(vLine);
×
1475
                    break;
×
1476
                }
×
UNCOV
1477
                case PointMarkerShape::X: {
×
UNCOV
1478
                    QPen pen(point_color);
×
1479
                    pen.setWidth(3);
×
1480

UNCOV
1481
                    float const half_size = point_size / 2;
×
1482
                    // Draw diagonal line (\)
1483
                    auto dLine1 = addLine(x_pos - half_size, y_pos - half_size,
×
1484
                                          x_pos + half_size, y_pos + half_size, pen);
×
1485
                    _points.append(dLine1);
×
1486

1487
                    // Draw diagonal line (/)
1488
                    auto dLine2 = addLine(x_pos - half_size, y_pos + half_size,
×
UNCOV
1489
                                          x_pos + half_size, y_pos - half_size, pen);
×
UNCOV
1490
                    _points.append(dLine2);
×
UNCOV
1491
                    break;
×
1492
                }
×
1493
                case PointMarkerShape::Diamond: {
×
1494
                    QPen pen(point_color);
×
1495
                    pen.setWidth(2);
×
1496
                    QBrush brush(point_color);
×
1497

1498
                    // Create diamond polygon (rotated square)
1499
                    QPolygonF diamond;
×
1500
                    float const half_size = point_size / 2;
×
1501
                    diamond << QPointF(x_pos, y_pos - half_size) // Top
×
1502
                            << QPointF(x_pos + half_size, y_pos) // Right
×
1503
                            << QPointF(x_pos, y_pos + half_size) // Bottom
×
1504
                            << QPointF(x_pos - half_size, y_pos);// Left
×
1505

1506
                    auto polygon = addPolygon(diamond, pen, brush);
×
1507
                    _points.append(polygon);
×
1508
                    break;
×
UNCOV
1509
                }
×
1510
            }
1511
        }
1512
    }
×
1513
}
15✔
1514

1515
void Media_Window::_plotDigitalIntervalSeries() {
15✔
1516
    auto const current_time = _data_manager->getCurrentTime();
15✔
1517
    auto video_timeframe = _data_manager->getTime(TimeKey("time"));
15✔
1518

1519
    for (auto const & [key, _interval_config]: _interval_configs) {
15✔
UNCOV
1520
        if (!_interval_config.get()->is_visible) continue;
×
1521

1522
        // Only render if using Box plotting style
UNCOV
1523
        if (_interval_config.get()->plotting_style != IntervalPlottingStyle::Box) continue;
×
1524

UNCOV
1525
        auto plot_color = plot_color_with_alpha(_interval_config.get());
×
1526

UNCOV
1527
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(key);
×
1528

1529
        // Get the timeframes for conversion
UNCOV
1530
        auto interval_timeframe_key = _data_manager->getTimeKey(key);
×
UNCOV
1531
        if (interval_timeframe_key.empty()) {
×
UNCOV
1532
            std::cerr << "Error: No timeframe found for digital interval series: " << key << std::endl;
×
1533
            continue;
×
1534
        }
1535

UNCOV
1536
        auto interval_timeframe = _data_manager->getTime(interval_timeframe_key);
×
1537

UNCOV
1538
        if (!video_timeframe) {
×
1539
            std::cerr << "Error: Could not get video timeframe 'time' for interval conversion" << std::endl;
×
UNCOV
1540
            continue;
×
1541
        }
UNCOV
1542
        if (!interval_timeframe) {
×
UNCOV
1543
            std::cerr << "Error: Could not get interval timeframe '" << interval_timeframe_key
×
UNCOV
1544
                      << "' for series: " << key << std::endl;
×
UNCOV
1545
            continue;
×
1546
        }
1547

UNCOV
1548
        bool const needs_conversion = _needsTimeFrameConversion(video_timeframe, interval_timeframe);
×
1549

1550
        // Generate relative times based on frame range setting
1551
        std::vector<int> relative_times;
×
UNCOV
1552
        int const frame_range = _interval_config->frame_range;
×
UNCOV
1553
        for (int i = -frame_range; i <= frame_range; ++i) {
×
1554
            relative_times.push_back(i);
×
1555
        }
1556

1557
        int const square_size = _interval_config->box_size;
×
1558

1559
        // Calculate position based on location setting
1560
        int start_x, start_y;
1561
        switch (_interval_config->location) {
×
UNCOV
1562
            case IntervalLocation::TopLeft:
×
1563
                start_x = 0;
×
1564
                start_y = 0;
×
1565
                break;
×
UNCOV
1566
            case IntervalLocation::TopRight:
×
1567
                start_x = _canvasWidth - square_size * static_cast<int>(relative_times.size());
×
1568
                start_y = 0;
×
1569
                break;
×
1570
            case IntervalLocation::BottomLeft:
×
UNCOV
1571
                start_x = 0;
×
UNCOV
1572
                start_y = _canvasHeight - square_size;
×
1573
                break;
×
UNCOV
1574
            case IntervalLocation::BottomRight:
×
UNCOV
1575
                start_x = _canvasWidth - square_size * static_cast<int>(relative_times.size());
×
1576
                start_y = _canvasHeight - square_size;
×
1577
                break;
×
1578
        }
1579

1580
        for (size_t i = 0; i < relative_times.size(); ++i) {
×
1581
            int const video_time = current_time + relative_times[i];
×
UNCOV
1582
            int query_time = video_time;// Default: no conversion needed
×
1583

1584
            if (needs_conversion) {
×
1585
                // Convert from video timeframe ("time") to interval series timeframe
1586
                // 1. Convert video time index to actual time value
UNCOV
1587
                int const video_time_value = video_timeframe->getTimeAtIndex(TimeFrameIndex(video_time));
×
1588

1589
                // 2. Convert time value to index in interval series timeframe
UNCOV
1590
                query_time = interval_timeframe->getIndexAtTime(static_cast<float>(video_time_value)).getValue();
×
1591
            }
1592

UNCOV
1593
            bool const event_present = interval_series->isEventAtTime(TimeFrameIndex(query_time));
×
1594

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

UNCOV
1597
            auto intervalPixmap = addRect(
×
UNCOV
1598
                    start_x + i * square_size,
×
1599
                    start_y,
1600
                    square_size,
1601
                    square_size,
UNCOV
1602
                    QPen(Qt::black),// Black border
×
1603
                    QBrush(color)   // Fill with color if event is present
×
1604
            );
×
1605

UNCOV
1606
            _intervals.append(intervalPixmap);
×
1607
        }
1608
    }
×
1609
}
30✔
1610

1611
void Media_Window::_plotDigitalIntervalBorders() {
15✔
1612
    auto const current_time = _data_manager->getCurrentTime();
15✔
1613

1614
    for (auto const & [key, _interval_config]: _interval_configs) {
15✔
UNCOV
1615
        if (!_interval_config.get()->is_visible) continue;
×
1616

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

UNCOV
1620
        auto interval_series = _data_manager->getData<DigitalIntervalSeries>(key);
×
1621

1622
        // Get the timeframes for conversion
UNCOV
1623
        auto interval_timeframe_key = _data_manager->getTimeKey(key);
×
1624
        if (interval_timeframe_key.empty()) {
×
UNCOV
1625
            std::cerr << "Error: No timeframe found for digital interval series: " << key << std::endl;
×
1626
            continue;
×
1627
        }
1628

UNCOV
1629
        auto video_timeframe = _data_manager->getTime(TimeKey("time"));
×
UNCOV
1630
        auto interval_timeframe = _data_manager->getTime(interval_timeframe_key);
×
1631

1632
        if (!video_timeframe) {
×
1633
            std::cerr << "Error: Could not get video timeframe 'time' for interval conversion" << std::endl;
×
1634
            continue;
×
1635
        }
UNCOV
1636
        if (!interval_timeframe) {
×
UNCOV
1637
            std::cerr << "Error: Could not get interval timeframe '" << interval_timeframe_key
×
1638
                      << "' for series: " << key << std::endl;
×
1639
            continue;
×
1640
        }
1641

1642
        bool const needs_conversion = _needsTimeFrameConversion(video_timeframe, interval_timeframe);
×
1643

1644
        // Check if an interval is present at the current frame
UNCOV
1645
        bool interval_present = false;
×
UNCOV
1646
        if (needs_conversion) {
×
1647
            // Convert current video time to interval timeframe
UNCOV
1648
            auto video_time = video_timeframe->getTimeAtIndex(TimeFrameIndex(current_time));
×
1649
            auto interval_index = interval_timeframe->getIndexAtTime(video_time);
×
UNCOV
1650
            interval_present = interval_series->isEventAtTime(interval_index);
×
1651
        } else {
1652
            // Direct comparison (no timeframe conversion needed)
UNCOV
1653
            interval_present = interval_series->isEventAtTime(TimeFrameIndex(current_time));
×
1654
        }
1655

1656
        // If an interval is present, draw a border around the entire image
1657
        if (interval_present) {
×
1658
            auto plot_color = plot_color_with_alpha(_interval_config.get());
×
1659

1660
            // Get border thickness from config
1661
            int const thickness = _interval_config->border_thickness;
×
1662

UNCOV
1663
            QPen border_pen(plot_color);
×
1664
            border_pen.setWidth(thickness);
×
1665

1666
            // Draw border as 4 rectangles around the edges of the canvas
1667
            // Top border
UNCOV
1668
            auto top_border = addRect(0, 0, _canvasWidth, thickness, border_pen, QBrush(plot_color));
×
1669
            _intervals.append(top_border);
×
1670

1671
            // Bottom border
1672
            auto bottom_border = addRect(0, _canvasHeight - thickness, _canvasWidth, thickness, border_pen, QBrush(plot_color));
×
1673
            _intervals.append(bottom_border);
×
1674

1675
            // Left border
UNCOV
1676
            auto left_border = addRect(0, 0, thickness, _canvasHeight, border_pen, QBrush(plot_color));
×
UNCOV
1677
            _intervals.append(left_border);
×
1678

1679
            // Right border
1680
            auto right_border = addRect(_canvasWidth - thickness, 0, thickness, _canvasHeight, border_pen, QBrush(plot_color));
×
1681
            _intervals.append(right_border);
×
UNCOV
1682
        }
×
UNCOV
1683
    }
×
1684
}
15✔
1685

1686
void Media_Window::_plotTensorData() {
15✔
1687

1688
    auto const current_time = _data_manager->getCurrentTime();
15✔
1689

1690
    for (auto const & [key, config]: _tensor_configs) {
15✔
UNCOV
1691
        if (!config.get()->is_visible) continue;
×
1692

UNCOV
1693
        auto tensor_data = _data_manager->getData<TensorData>(key);
×
1694

UNCOV
1695
        auto tensor_shape = tensor_data->getFeatureShape();
×
1696

UNCOV
1697
        auto tensor_slice = tensor_data->getChannelSlice(TimeFrameIndex(current_time), config->display_channel);
×
1698

1699
        // Create a QImage from the tensor data
UNCOV
1700
        QImage tensor_image(static_cast<int>(tensor_shape[1]), static_cast<int>(tensor_shape[0]), QImage::Format::Format_ARGB32);
×
UNCOV
1701
        for (size_t y = 0; y < tensor_shape[0]; ++y) {
×
UNCOV
1702
            for (size_t x = 0; x < tensor_shape[1]; ++x) {
×
UNCOV
1703
                float const value = tensor_slice[y * tensor_shape[1] + x];
×
1704
                //int const pixel_value = static_cast<int>(value * 255);// Assuming the tensor values are normalized between 0 and 1
1705

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

UNCOV
1711
                tensor_image.setPixel(x, y, rgb);
×
1712
            }
1713
        }
1714

1715
        // Scale the tensor image to the size of the canvas
UNCOV
1716
        QImage const scaled_tensor_image = tensor_image.scaled(_canvasWidth, _canvasHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
×
1717

UNCOV
1718
        auto tensor_pixmap = addPixmap(QPixmap::fromImage(scaled_tensor_image));
×
1719

UNCOV
1720
        _tensors.append(tensor_pixmap);
×
UNCOV
1721
    }
×
1722
}
15✔
1723

UNCOV
1724
std::vector<uint8_t> Media_Window::getDrawingMask() {
×
1725
    // Create a QImage with _canvasWidth and _canvasHeight
UNCOV
1726
    QImage maskImage(_canvasWidth, _canvasHeight, QImage::Format_Grayscale8);
×
UNCOV
1727
    maskImage.fill(0);
×
1728

UNCOV
1729
    QPainter painter(&maskImage);
×
UNCOV
1730
    painter.setPen(Qt::white);
×
UNCOV
1731
    painter.setBrush(QBrush(Qt::white));// Fill the circles with white
×
1732

UNCOV
1733
    for (auto const & point: _drawing_points) {
×
1734
        // Draw a filled circle with the current brush size (hover circle radius)
1735
        float const radius = static_cast<float>(_hover_circle_radius);
×
UNCOV
1736
        painter.drawEllipse(point, radius, radius);
×
1737
    }
UNCOV
1738
    painter.end();
×
1739

1740
    // Scale the QImage to the size of the media
1741
    auto media = _data_manager->getData<MediaData>("media");
×
1742
    int const mediaWidth = media->getWidth();
×
1743
    int const mediaHeight = media->getHeight();
×
1744
    QImage scaledMaskImage = maskImage.scaled(mediaWidth, mediaHeight);
×
1745

1746
    // Convert the QImage to a std::vector<uint8_t>
1747
    std::vector<uint8_t> mask(scaledMaskImage.bits(), scaledMaskImage.bits() + scaledMaskImage.sizeInBytes());
×
1748

UNCOV
1749
    return mask;
×
1750
}
×
1751

1752
void Media_Window::setShowHoverCircle(bool show) {
19✔
1753
    _show_hover_circle = show;
19✔
1754
    if (_show_hover_circle) {
19✔
1755
        if (_debug_performance) {
2✔
1756
            std::cout << "Hover circle enabled" << std::endl;
×
1757
        }
1758

1759
        // Create the hover circle item if it doesn't exist
1760
        if (!_hover_circle_item) {
2✔
1761
            QPen circlePen(Qt::red);
2✔
1762
            circlePen.setWidth(2);
2✔
1763
            _hover_circle_item = addEllipse(0, 0, _hover_circle_radius * 2, _hover_circle_radius * 2, circlePen);
2✔
1764
            _hover_circle_item->setVisible(false);// Initially hidden until mouse moves
2✔
1765
            // DO NOT add to _points vector - hover circle is managed separately
1766
            if (_debug_performance) {
2✔
UNCOV
1767
                std::cout << "  Created new hover circle item" << std::endl;
×
1768
            }
1769
        }
2✔
1770

1771
        // Connect mouse move to efficient hover circle update instead of full canvas update
1772
        connect(this, &Media_Window::mouseMove, this, &Media_Window::_updateHoverCirclePosition);
2✔
1773
    } else {
1774
        if (_debug_performance) {
17✔
1775
            std::cout << "Hover circle disabled" << std::endl;
×
1776
        }
1777

1778
        // Remove the hover circle item
1779
        if (_hover_circle_item) {
17✔
1780
            removeItem(_hover_circle_item);
2✔
1781
            delete _hover_circle_item;
2✔
1782
            _hover_circle_item = nullptr;
2✔
1783
            if (_debug_performance) {
2✔
UNCOV
1784
                std::cout << "  Deleted hover circle item" << std::endl;
×
1785
            }
1786
        }
1787

1788
        // Disconnect the mouse move signal
1789
        disconnect(this, &Media_Window::mouseMove, this, &Media_Window::_updateHoverCirclePosition);
17✔
1790
    }
1791
}
19✔
1792

1793
void Media_Window::setHoverCircleRadius(int radius) {
2✔
1794
    _hover_circle_radius = radius;
2✔
1795

1796
    // Update the existing hover circle item if it exists
1797
    if (_hover_circle_item && _show_hover_circle) {
2✔
1798
        qreal const x = _hover_position.x() - _hover_circle_radius;
2✔
1799
        qreal const y = _hover_position.y() - _hover_circle_radius;
2✔
1800
        _hover_circle_item->setRect(x, y, _hover_circle_radius * 2, _hover_circle_radius * 2);
2✔
1801
    }
1802
}
2✔
1803

UNCOV
1804
void Media_Window::_updateHoverCirclePosition() {
×
1805
    static int call_count = 0;
UNCOV
1806
    call_count++;
×
1807

1808
    if (_hover_circle_item && _show_hover_circle) {
×
1809
        // Update the position of the existing hover circle item
1810
        qreal const x = _hover_position.x() - _hover_circle_radius;
×
1811
        qreal const y = _hover_position.y() - _hover_circle_radius;
×
1812
        _hover_circle_item->setRect(x, y, _hover_circle_radius * 2, _hover_circle_radius * 2);
×
UNCOV
1813
        _hover_circle_item->setVisible(true);
×
1814

1815
        if (_debug_performance) {
×
UNCOV
1816
            std::cout << "Hover circle updated (call #" << call_count << ") at ("
×
UNCOV
1817
                      << _hover_position.x() << ", " << _hover_position.y() << ")" << std::endl;
×
1818
        }
1819
    } else {
×
1820
        if (_debug_performance) {
×
UNCOV
1821
            std::cout << "Hover circle update skipped (call #" << call_count << ") - item: "
×
UNCOV
1822
                      << (_hover_circle_item ? "exists" : "null") << ", show: " << _show_hover_circle << std::endl;
×
1823
        }
1824
    }
UNCOV
1825
}
×
1826

1827
void Media_Window::_addRemoveData() {
1✔
1828
    //New data key was added. This is where we may want to repopulate a custom table
1829
}
1✔
1830

UNCOV
1831
bool Media_Window::_needsTimeFrameConversion(std::shared_ptr<TimeFrame> video_timeframe,
×
1832
                                             std::shared_ptr<TimeFrame> const & interval_timeframe) {
1833
    // If either timeframe is null, no conversion is possible/needed
UNCOV
1834
    if (!video_timeframe || !interval_timeframe) {
×
UNCOV
1835
        return false;
×
1836
    }
1837

1838
    // Conversion is needed if the timeframes are different objects
UNCOV
1839
    return video_timeframe.get() != interval_timeframe.get();
×
1840
}
1841

1842

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

UNCOV
1847
    return output_color;
×
1848
}
1849

UNCOV
1850
bool Media_Window::hasPreviewMaskData(std::string const & mask_key) const {
×
UNCOV
1851
    return _mask_preview_active && _preview_mask_data.count(mask_key) > 0;
×
1852
}
1853

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

UNCOV
1856
    if (hasPreviewMaskData(mask_key)) {
×
UNCOV
1857
        return _preview_mask_data.at(mask_key);
×
1858
    }
UNCOV
1859
    return {};
×
1860
}
1861

UNCOV
1862
void Media_Window::setPreviewMaskData(std::string const & mask_key,
×
1863
                                      std::vector<std::vector<Point2D<uint32_t>>> const & preview_data,
1864
                                      bool active) {
UNCOV
1865
    if (active) {
×
UNCOV
1866
        _preview_mask_data[mask_key] = preview_data;
×
UNCOV
1867
        _mask_preview_active = true;
×
1868
    } else {
UNCOV
1869
        _preview_mask_data.erase(mask_key);
×
UNCOV
1870
        _mask_preview_active = !_preview_mask_data.empty();
×
1871
    }
UNCOV
1872
}
×
1873

UNCOV
1874
void Media_Window::onGroupChanged() {
×
1875
    // Update the canvas when group assignments or properties change
UNCOV
1876
    UpdateCanvas();
×
UNCOV
1877
}
×
1878

UNCOV
1879
QColor Media_Window::_getGroupAwareColor(EntityId entity_id, QColor const & default_color) const {
×
1880
    // Handle selection highlighting first
UNCOV
1881
    if (_selected_entities.count(entity_id) > 0) {
×
UNCOV
1882
        return QColor(255, 255, 0); // Bright yellow for selected entities
×
1883
    }
1884
    
UNCOV
1885
    if (!_group_manager || entity_id == 0) {
×
UNCOV
1886
        return default_color;
×
1887
    }
1888
    
UNCOV
1889
    return _group_manager->getEntityColor(entity_id, default_color);
×
1890
}
1891

UNCOV
1892
QRgb Media_Window::_getGroupAwareColorRgb(EntityId entity_id, QRgb default_color) const {
×
1893
    // Handle selection highlighting first
UNCOV
1894
    if (_selected_entities.count(entity_id) > 0) {
×
UNCOV
1895
        return qRgba(255, 255, 0, 255); // Bright yellow for selected entities
×
1896
    }
1897
    
UNCOV
1898
    if (!_group_manager || entity_id == 0) {
×
UNCOV
1899
        return default_color;
×
1900
    }
1901
    
UNCOV
1902
    QColor group_color = _group_manager->getEntityColor(entity_id, QColor::fromRgba(default_color));
×
UNCOV
1903
    return group_color.rgba();
×
1904
}
1905

1906
// ===== Selection and Context Menu Implementation =====
1907

1908
void Media_Window::clearAllSelections() {
3✔
1909
    if (!_selected_entities.empty()) {
3✔
UNCOV
1910
        _selected_entities.clear();
×
UNCOV
1911
        _selected_data_key.clear();
×
UNCOV
1912
        _selected_data_type.clear();
×
UNCOV
1913
        UpdateCanvas(); // Refresh to remove selection highlights
×
1914
    }
1915
}
3✔
1916

UNCOV
1917
bool Media_Window::hasSelections() const {
×
UNCOV
1918
    return !_selected_entities.empty();
×
1919
}
1920

UNCOV
1921
std::unordered_set<EntityId> Media_Window::getSelectedEntities() const {
×
UNCOV
1922
    return _selected_entities;
×
1923
}
1924

1925
void Media_Window::setGroupSelectionEnabled(bool enabled) {
3✔
1926
    _group_selection_enabled = enabled;
3✔
1927
    if (!enabled) {
3✔
1928
        // Clear any existing selections when disabling group selection
1929
        clearAllSelections();
3✔
1930
    }
1931
}
3✔
1932

NEW
1933
bool Media_Window::isGroupSelectionEnabled() const {
×
NEW
1934
    return _group_selection_enabled;
×
1935
}
1936

UNCOV
1937
EntityId Media_Window::findPointAtPosition(QPointF const & scene_pos, std::string const & point_key) {
×
UNCOV
1938
    return _findPointAtPosition(scene_pos, point_key);
×
1939
}
1940

NEW
1941
EntityId Media_Window::findEntityAtPosition(QPointF const & scene_pos, std::string & data_key, std::string & data_type) {
×
NEW
1942
    return _findEntityAtPosition(scene_pos, data_key, data_type);
×
1943
}
1944

UNCOV
1945
void Media_Window::selectEntity(EntityId entity_id, std::string const & data_key, std::string const & data_type) {
×
UNCOV
1946
    _selected_entities.clear();
×
UNCOV
1947
    _selected_entities.insert(entity_id);
×
UNCOV
1948
    _selected_data_key = data_key;
×
UNCOV
1949
    _selected_data_type = data_type;
×
UNCOV
1950
    UpdateCanvas(); // Refresh to show selection
×
UNCOV
1951
}
×
1952

UNCOV
1953
EntityId Media_Window::_findEntityAtPosition(QPointF const & scene_pos, std::string & data_key, std::string & data_type) {
×
1954
    // Convert scene coordinates to media coordinates
UNCOV
1955
    float x_media = static_cast<float>(scene_pos.x() / getXAspect());
×
UNCOV
1956
    float y_media = static_cast<float>(scene_pos.y() / getYAspect());
×
1957

1958
    // Search through lines first (as they're typically most precise)
UNCOV
1959
    for (auto const & [key, config] : _line_configs) {
×
UNCOV
1960
        if (config->is_visible) {
×
UNCOV
1961
            EntityId entity_id = _findLineAtPosition(scene_pos, key);
×
UNCOV
1962
            if (entity_id != 0) {
×
UNCOV
1963
                data_key = key;
×
UNCOV
1964
                data_type = "line";
×
UNCOV
1965
                return entity_id;
×
1966
            }
1967
        }
1968
    }
1969

1970
    // Then search points
UNCOV
1971
    for (auto const & [key, config] : _point_configs) {
×
UNCOV
1972
        if (config->is_visible) {
×
UNCOV
1973
            EntityId entity_id = _findPointAtPosition(scene_pos, key);
×
UNCOV
1974
            if (entity_id != 0) {
×
UNCOV
1975
                data_key = key;
×
UNCOV
1976
                data_type = "point";
×
UNCOV
1977
                return entity_id;
×
1978
            }
1979
        }
1980
    }
1981

1982
    // Finally search masks (usually less precise)
UNCOV
1983
    for (auto const & [key, config] : _mask_configs) {
×
UNCOV
1984
        if (config->is_visible) {
×
UNCOV
1985
            EntityId entity_id = _findMaskAtPosition(scene_pos, key);
×
UNCOV
1986
            if (entity_id != 0) {
×
UNCOV
1987
                data_key = key;
×
UNCOV
1988
                data_type = "mask";
×
UNCOV
1989
                return entity_id;
×
1990
            }
1991
        }
1992
    }
1993

UNCOV
1994
    return 0; // No entity found
×
1995
}
1996

UNCOV
1997
EntityId Media_Window::_findLineAtPosition(QPointF const & scene_pos, std::string const & line_key) {
×
UNCOV
1998
    auto line_data = _data_manager->getData<LineData>(line_key);
×
UNCOV
1999
    if (!line_data) {
×
UNCOV
2000
        return 0;
×
2001
    }
2002

UNCOV
2003
    auto current_time = _data_manager->getCurrentTime();
×
UNCOV
2004
    auto const & lines = line_data->getAtTime(TimeFrameIndex(current_time));
×
UNCOV
2005
    auto const & entity_ids = line_data->getEntityIdsAtTime(TimeFrameIndex(current_time));
×
2006

UNCOV
2007
    if (lines.size() != entity_ids.size()) {
×
UNCOV
2008
        return 0;
×
2009
    }
2010

UNCOV
2011
    float const threshold = 10.0f; // pixels
×
2012

UNCOV
2013
    for (size_t i = 0; i < lines.size(); ++i) {
×
UNCOV
2014
        auto const & line = lines[i];
×
2015
        
2016
        // Check distance from each line segment
UNCOV
2017
        for (size_t j = 0; j < line.size(); ++j) {
×
UNCOV
2018
            if (j + 1 >= line.size()) continue;
×
2019
            
UNCOV
2020
            auto const & p1 = line[j];
×
UNCOV
2021
            auto const & p2 = line[j + 1];
×
2022
            
2023
            // Convert line points to scene coordinates
UNCOV
2024
            float x1_scene = p1.x * getXAspect();
×
UNCOV
2025
            float y1_scene = p1.y * getYAspect();
×
UNCOV
2026
            float x2_scene = p2.x * getXAspect();
×
UNCOV
2027
            float y2_scene = p2.y * getYAspect();
×
2028
            
2029
            // Calculate distance from click point to line segment
UNCOV
2030
            float dist = _calculateDistanceToLineSegment(
×
UNCOV
2031
                scene_pos.x(), scene_pos.y(),
×
2032
                x1_scene, y1_scene, x2_scene, y2_scene
2033
            );
2034
            
UNCOV
2035
            if (dist <= threshold) {
×
UNCOV
2036
                return entity_ids[i];
×
2037
            }
2038
        }
2039
    }
2040

UNCOV
2041
    return 0;
×
UNCOV
2042
}
×
2043

UNCOV
2044
EntityId Media_Window::_findPointAtPosition(QPointF const & scene_pos, std::string const & point_key) {
×
UNCOV
2045
    auto point_data = _data_manager->getData<PointData>(point_key);
×
UNCOV
2046
    if (!point_data) {
×
UNCOV
2047
        return 0;
×
2048
    }
2049

UNCOV
2050
    auto current_time = _data_manager->getCurrentTime();
×
UNCOV
2051
    auto const & points = point_data->getAtTime(TimeFrameIndex(current_time));
×
UNCOV
2052
    auto const & entity_ids = point_data->getEntityIdsAtTime(TimeFrameIndex(current_time));
×
2053

UNCOV
2054
    if (points.size() != entity_ids.size()) {
×
UNCOV
2055
        return 0;
×
2056
    }
2057

UNCOV
2058
    float const threshold = 15.0f; // pixels
×
2059

UNCOV
2060
    for (size_t i = 0; i < points.size(); ++i) {
×
UNCOV
2061
        auto const & point = points[i];
×
2062
        
2063
        // Convert point to scene coordinates
UNCOV
2064
        float x_scene = point.x * getXAspect();
×
UNCOV
2065
        float y_scene = point.y * getYAspect();
×
2066
        
2067
        // Calculate distance
UNCOV
2068
        float dx = scene_pos.x() - x_scene;
×
UNCOV
2069
        float dy = scene_pos.y() - y_scene;
×
UNCOV
2070
        float distance = std::sqrt(dx * dx + dy * dy);
×
2071
        
UNCOV
2072
        if (distance <= threshold) {
×
UNCOV
2073
            return entity_ids[i];
×
2074
        }
2075
    }
2076

UNCOV
2077
    return 0;
×
UNCOV
2078
}
×
2079

UNCOV
2080
EntityId Media_Window::_findMaskAtPosition(QPointF const & scene_pos, std::string const & mask_key) {
×
UNCOV
2081
    auto mask_data = _data_manager->getData<MaskData>(mask_key);
×
UNCOV
2082
    if (!mask_data) {
×
UNCOV
2083
        return 0;
×
2084
    }
2085

UNCOV
2086
    auto current_time = _data_manager->getCurrentTime();
×
UNCOV
2087
    auto const & masks = mask_data->getAtTime(TimeFrameIndex(current_time));
×
2088

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

UNCOV
2092
    float x_media = static_cast<float>(scene_pos.x() / getXAspect());
×
UNCOV
2093
    float y_media = static_cast<float>(scene_pos.y() / getYAspect());
×
2094

UNCOV
2095
    for (size_t i = 0; i < masks.size(); ++i) {
×
UNCOV
2096
        auto const & mask = masks[i];
×
2097
        
2098
        // Check if the point is inside any of the mask's polygons
UNCOV
2099
        for (auto const & point : mask) {
×
2100
            // Simple bounding box check for now (could be improved with proper point-in-polygon)
UNCOV
2101
            if (std::abs(static_cast<float>(point.x) - x_media) < 5.0f && 
×
UNCOV
2102
                std::abs(static_cast<float>(point.y) - y_media) < 5.0f) {
×
2103
                // Return a synthetic EntityId based on position and mask index
2104
                // This is temporary until MaskData supports proper EntityIds
UNCOV
2105
                return static_cast<EntityId>(1000000 + current_time * 1000 + i);
×
2106
            }
2107
        }
2108
    }
2109

UNCOV
2110
    return 0;
×
UNCOV
2111
}
×
2112

2113
void Media_Window::_createContextMenu() {
3✔
2114
    _context_menu = new QMenu();
3✔
2115
    
2116
    // Create actions
2117
    auto * create_group_action = new QAction("Create New Group", this);
3✔
2118
    auto * ungroup_action = new QAction("Ungroup Selected", this);
3✔
2119
    auto * clear_selection_action = new QAction("Clear Selection", this);
3✔
2120
    
2121
    // Add actions to menu
2122
    _context_menu->addAction(create_group_action);
3✔
2123
    _context_menu->addSeparator();
3✔
2124
    _context_menu->addAction(ungroup_action);
3✔
2125
    _context_menu->addSeparator();
3✔
2126
    _context_menu->addAction(clear_selection_action);
3✔
2127
    
2128
    // Connect actions
2129
    connect(create_group_action, &QAction::triggered, this, &Media_Window::onCreateNewGroup);
3✔
2130
    connect(ungroup_action, &QAction::triggered, this, &Media_Window::onUngroupSelected);
3✔
2131
    connect(clear_selection_action, &QAction::triggered, this, &Media_Window::onClearSelection);
3✔
2132
}
3✔
2133

UNCOV
2134
void Media_Window::_showContextMenu(QPoint const & global_pos) {
×
UNCOV
2135
    if (_context_menu) {
×
UNCOV
2136
        _context_menu->popup(global_pos);
×
2137
    }
UNCOV
2138
}
×
2139

UNCOV
2140
void Media_Window::_updateContextMenuActions() {
×
UNCOV
2141
    if (!_context_menu || !_group_manager) {
×
UNCOV
2142
        return;
×
2143
    }
2144

2145
    // Clear all dynamic actions by removing actions after the static ones
2146
    // The static menu structure is: Create New Group, Separator, Ungroup Selected, Separator, Clear Selection
2147
    // Everything after the second separator should be removed
UNCOV
2148
    auto actions = _context_menu->actions();
×
NEW
2149
    int separator_count = 0;
×
NEW
2150
    QList<QAction*> actions_to_remove;
×
2151
    
NEW
2152
    for (QAction* action : actions) {
×
NEW
2153
        if (action->isSeparator()) {
×
NEW
2154
            separator_count++;
×
NEW
2155
            if (separator_count > 2) {
×
NEW
2156
                actions_to_remove.append(action);
×
2157
            }
NEW
2158
        } else if (separator_count >= 2) {
×
2159
            // This is a dynamic action after the second separator
NEW
2160
            actions_to_remove.append(action);
×
2161
        }
2162
    }
2163
    
2164
    // Remove and delete the dynamic actions
NEW
2165
    for (QAction* action : actions_to_remove) {
×
NEW
2166
        _context_menu->removeAction(action);
×
NEW
2167
        action->deleteLater(); // Use deleteLater() for safer cleanup
×
2168
    }
2169

2170
    // Add dynamic group assignment actions
UNCOV
2171
    auto groups = _group_manager->getGroupsForContextMenu();
×
UNCOV
2172
    if (!groups.empty()) {
×
UNCOV
2173
        _context_menu->addSeparator();
×
2174
        
UNCOV
2175
        for (auto const & [group_id, group_name] : groups) {
×
UNCOV
2176
            auto * assign_action = new QAction(QString("Assign to %1").arg(group_name), this);
×
UNCOV
2177
            _context_menu->addAction(assign_action);
×
2178
            
UNCOV
2179
            connect(assign_action, &QAction::triggered, this, [this, group_id]() {
×
UNCOV
2180
                onAssignToGroup(group_id);
×
UNCOV
2181
            });
×
2182
        }
2183
    }
UNCOV
2184
}
×
2185

UNCOV
2186
float Media_Window::_calculateDistanceToLineSegment(float px, float py, float x1, float y1, float x2, float y2) {
×
UNCOV
2187
    float dx = x2 - x1;
×
UNCOV
2188
    float dy = y2 - y1;
×
2189
    
UNCOV
2190
    if (dx == 0 && dy == 0) {
×
2191
        // Point to point distance
UNCOV
2192
        float dpx = px - x1;
×
UNCOV
2193
        float dpy = py - y1;
×
UNCOV
2194
        return std::sqrt(dpx * dpx + dpy * dpy);
×
2195
    }
2196
    
UNCOV
2197
    float t = ((px - x1) * dx + (py - y1) * dy) / (dx * dx + dy * dy);
×
UNCOV
2198
    t = std::max(0.0f, std::min(1.0f, t));
×
2199
    
UNCOV
2200
    float projection_x = x1 + t * dx;
×
UNCOV
2201
    float projection_y = y1 + t * dy;
×
2202
    
UNCOV
2203
    float dist_x = px - projection_x;
×
UNCOV
2204
    float dist_y = py - projection_y;
×
2205
    
UNCOV
2206
    return std::sqrt(dist_x * dist_x + dist_y * dist_y);
×
2207
}
2208

2209
// Context menu slot implementations
UNCOV
2210
void Media_Window::onCreateNewGroup() {
×
UNCOV
2211
    if (!_group_manager || _selected_entities.empty()) {
×
UNCOV
2212
        return;
×
2213
    }
2214
    
UNCOV
2215
    int group_id = _group_manager->createGroupWithEntities(_selected_entities);
×
UNCOV
2216
    if (group_id != -1) {
×
UNCOV
2217
        clearAllSelections();
×
2218
    }
2219
}
2220

UNCOV
2221
void Media_Window::onAssignToGroup(int group_id) {
×
UNCOV
2222
    if (!_group_manager || _selected_entities.empty()) {
×
UNCOV
2223
        return;
×
2224
    }
2225
    
UNCOV
2226
    _group_manager->assignEntitiesToGroup(group_id, _selected_entities);
×
UNCOV
2227
    clearAllSelections();
×
2228
}
2229

UNCOV
2230
void Media_Window::onUngroupSelected() {
×
UNCOV
2231
    if (!_group_manager || _selected_entities.empty()) {
×
UNCOV
2232
        return;
×
2233
    }
2234
    
UNCOV
2235
    _group_manager->ungroupEntities(_selected_entities);
×
UNCOV
2236
    clearAllSelections();
×
2237
}
2238

UNCOV
2239
void Media_Window::onClearSelection() {
×
UNCOV
2240
    clearAllSelections();
×
UNCOV
2241
}
×
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