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

paulmthompson / WhiskerToolbox / 18389801194

09 Oct 2025 09:35PM UTC coverage: 71.943% (+0.1%) from 71.826%
18389801194

push

github

paulmthompson
add correlation matrix to filtering interface

207 of 337 new or added lines in 5 files covered. (61.42%)

867 existing lines in 31 files now uncovered.

49964 of 69449 relevant lines covered (71.94%)

1103.53 hits per line

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

71.28
/src/WhiskerToolbox/GroupManagementWidget/GroupManager.cpp
1
#include "GroupManager.hpp"
2

3
#include "Entity/EntityGroupManager.hpp"
4
#include "DataManager/DataManager.hpp"
5
#include "DataManager/Points/Point_Data.hpp"
6
#include "DataManager/Lines/Line_Data.hpp"
7

8
#include <QDebug>
9

10
QVector<QColor> const GroupManager::DEFAULT_COLORS = {
11
        QColor(31, 119, 180), // Blue
12
        QColor(255, 127, 14), // Orange
13
        QColor(44, 160, 44),  // Green
14
        QColor(214, 39, 40),  // Red
15
        QColor(148, 103, 189),// Purple
16
        QColor(140, 86, 75),  // Brown
17
        QColor(227, 119, 194),// Pink
18
        QColor(127, 127, 127),// Gray
19
        QColor(188, 189, 34), // Olive
20
        QColor(23, 190, 207)  // Cyan
21
};
22

23
GroupManager::GroupManager(EntityGroupManager * entity_group_manager, std::shared_ptr<DataManager> data_manager, QObject * parent)
46✔
24
    : QObject(parent),
25
      m_entity_group_manager(entity_group_manager),
46✔
26
      m_data_manager(std::move(data_manager)),
46✔
27
      m_next_group_id(1) {
92✔
28
    // Assert that we have valid managers
29
    Q_ASSERT(m_entity_group_manager != nullptr);
46✔
30
    Q_ASSERT(m_data_manager != nullptr);
46✔
31

32
    // Initialize known groups set
33
    auto ids = m_entity_group_manager->getAllGroupIds();
46✔
34
    for (auto gid : ids) m_known_group_ids.insert(static_cast<int>(gid));
46✔
35

36
    // Subscribe to bulk group change notifications
37
    m_groupObserverId = m_entity_group_manager->getGroupObservers().addObserver([this]() {
46✔
38
        // Ensure we update on the UI thread
39
        QMetaObject::invokeMethod(this, [this]() {
2✔
40
            // Detect newly created groups and emit groupCreated for them
41
            auto current_ids = m_entity_group_manager->getAllGroupIds();
2✔
42
            QSet<int> current_set;
2✔
43
            for (auto gid : current_ids) current_set.insert(static_cast<int>(gid));
5✔
44

45
            // New groups = current - known
46
            for (int gid : current_set) {
5✔
47
                if (!m_known_group_ids.contains(gid)) {
3✔
48
                    // Assign a default color for new groups only; don't touch existing colors
49
                    // Use the number of colors already assigned to select a palette entry
50
                    int const color_index = static_cast<int>(m_group_colors.size()) % DEFAULT_COLORS.size();
1✔
51
                    m_group_colors[gid] = DEFAULT_COLORS[color_index];
1✔
52
                    m_group_visibility[gid] = true;
1✔
53
                    emit groupCreated(gid);
1✔
54
                }
55
            }
56
            // Removed groups = known - current
57
            for (int gid : m_known_group_ids) {
4✔
58
                if (!current_set.contains(gid)) {
2✔
UNCOV
59
                    m_group_colors.remove(gid);
×
UNCOV
60
                    m_group_visibility.remove(gid);
×
UNCOV
61
                    emit groupRemoved(gid);
×
62
                }
63
            }
64
            // Update known set after emitting create/remove
65
            m_known_group_ids = current_set;
2✔
66
        }, Qt::QueuedConnection);
4✔
67
    });
2✔
68
}
92✔
69

70
GroupManager::~GroupManager() {
66✔
71
    if (m_groupObserverId != 0) {
46✔
72
        m_entity_group_manager->getGroupObservers().removeObserver(m_groupObserverId);
46✔
73
        m_groupObserverId = 0;
46✔
74
    }
75
}
66✔
76

77
int GroupManager::createGroup(QString const & name) {
41✔
78
    QColor const color = getNextDefaultColor();
41✔
79
    return createGroup(name, color);
82✔
80
}
81

82
int GroupManager::createGroup(QString const & name, QColor const & color) {
41✔
83

84
    GroupId const entity_group_id = m_entity_group_manager->createGroup(name.toStdString());
123✔
85

86
    auto const group_id = static_cast<int>(entity_group_id);
41✔
87
    m_group_colors[group_id] = color;
41✔
88
    m_group_visibility[group_id] = true; // Groups are visible by default
41✔
89
    // Track as known to avoid duplicate create on next observer notification
90
    m_known_group_ids.insert(group_id);
41✔
91

92
    qDebug() << "GroupManager: Created group" << group_id << "with name" << name;
41✔
93

94
    emit groupCreated(group_id);
41✔
95
    return group_id;
41✔
96
}
97

98
bool GroupManager::removeGroup(int group_id) {
3✔
99
    auto entity_group_id = static_cast<GroupId>(group_id);
3✔
100

101
    if (!m_entity_group_manager->deleteGroup(entity_group_id)) {
3✔
102
        return false;
1✔
103
    }
104

105
    m_group_colors.remove(group_id);
2✔
106
    m_group_visibility.remove(group_id);
2✔
107
    m_known_group_ids.remove(group_id);
2✔
108

109
    qDebug() << "GroupManager: Removed group" << group_id;
2✔
110

111
    emit groupRemoved(group_id);
2✔
112
    return true;
2✔
113
}
114

115
std::optional<GroupManager::Group> GroupManager::getGroup(int group_id) const {
33✔
116
    auto entity_group_id = static_cast<GroupId>(group_id);
33✔
117

118
    auto descriptor = m_entity_group_manager->getGroupDescriptor(entity_group_id);
33✔
119
    if (!descriptor.has_value()) {
33✔
UNCOV
120
        return std::nullopt;
×
121
    }
122

123
    Group group(group_id,
33✔
124
                QString::fromStdString(descriptor->name),
66✔
125
                m_group_colors.value(group_id, QColor(128, 128, 128)),
66✔
126
                m_group_visibility.value(group_id, true));
132✔
127

128
    return group;
33✔
129
}
33✔
130

131
bool GroupManager::setGroupName(int group_id, QString const & name) {
12✔
132
    auto entity_group_id = static_cast<GroupId>(group_id);
12✔
133

134
    // Get current descriptor to preserve description
135
    auto descriptor = m_entity_group_manager->getGroupDescriptor(entity_group_id);
12✔
136
    if (!descriptor.has_value()) {
12✔
UNCOV
137
        return false;
×
138
    }
139

140
    // Avoid redundant updates/signals if the name is unchanged
141
    if (QString::fromStdString(descriptor->name) == name) {
12✔
142
        return true;
8✔
143
    }
144

145
    if (!m_entity_group_manager->updateGroup(entity_group_id, name.toStdString(), descriptor->description)) {
4✔
UNCOV
146
        return false;
×
147
    }
148

149
    qDebug() << "GroupManager: Updated group" << group_id << "name to" << name;
4✔
150

151
    emit groupModified(group_id);
4✔
152
    return true;
4✔
153
}
12✔
154

155
bool GroupManager::setGroupColor(int group_id, QColor const & color) {
2✔
156
    auto entity_group_id = static_cast<GroupId>(group_id);
2✔
157

158
    if (!m_entity_group_manager->hasGroup(entity_group_id)) {
2✔
UNCOV
159
        return false;
×
160
    }
161

162
    m_group_colors[group_id] = color;
2✔
163

164
    qDebug() << "GroupManager: Updated group" << group_id << "color";
2✔
165

166
    emit groupModified(group_id);
2✔
167
    return true;
2✔
168
}
169

170
bool GroupManager::setGroupVisibility(int group_id, bool visible) {
14✔
171
    auto entity_group_id = static_cast<GroupId>(group_id);
14✔
172

173
    if (!m_entity_group_manager->hasGroup(entity_group_id)) {
14✔
174
        return false;
1✔
175
    }
176

177
    m_group_visibility[group_id] = visible;
13✔
178

179
    qDebug() << "GroupManager: Updated group" << group_id << "visibility to" << visible;
13✔
180

181
    emit groupModified(group_id);
13✔
182
    return true;
13✔
183
}
184

185
QMap<int, GroupManager::Group> GroupManager::getGroups() const {
41✔
186
    QMap<int, Group> result;
41✔
187

188
    auto group_ids = m_entity_group_manager->getAllGroupIds();
41✔
189
    for (GroupId const entity_group_id: group_ids) {
55✔
190
        auto descriptor = m_entity_group_manager->getGroupDescriptor(entity_group_id);
14✔
191
        if (descriptor.has_value()) {
14✔
192
            auto group_id = static_cast<int>(entity_group_id);
14✔
193
            Group const group(group_id,
14✔
194
                              QString::fromStdString(descriptor->name),
28✔
195
                              m_group_colors.value(group_id, QColor(128, 128, 128)),
28✔
196
                              m_group_visibility.value(group_id, true));
56✔
197
            result[group_id] = group;
14✔
198
        }
14✔
199
    }
14✔
200

201
    return result;
41✔
202
}
41✔
203

204
// ===== EntityId-based API =====
205
bool GroupManager::assignEntitiesToGroup(int group_id, std::unordered_set<EntityId> const & entity_ids) {
20✔
206
    auto entity_group_id = static_cast<GroupId>(group_id);
20✔
207

208
    if (!m_entity_group_manager->hasGroup(entity_group_id)) {
20✔
UNCOV
209
        return false;
×
210
    }
211

212
    std::vector<EntityId> const entity_vector(entity_ids.begin(), entity_ids.end());
60✔
213

214
    std::size_t const added_count = m_entity_group_manager->addEntitiesToGroup(entity_group_id, entity_vector);
20✔
215

216
    qDebug() << "GroupManager: Assigned" << added_count << "entities to group" << group_id;
20✔
217
    if (added_count > 0) {
20✔
218
        emit groupModified(group_id);
18✔
219
    }
220

221
    return added_count > 0;
20✔
222
}
20✔
223

224
bool GroupManager::removeEntitiesFromGroup(int group_id, std::unordered_set<EntityId> const & entity_ids) {
6✔
225
    auto entity_group_id = static_cast<GroupId>(group_id);
6✔
226

227
    if (!m_entity_group_manager->hasGroup(entity_group_id)) {
6✔
UNCOV
228
        return false;
×
229
    }
230

231
    std::vector<EntityId> const entity_vector(entity_ids.begin(), entity_ids.end());
18✔
232

233
    std::size_t const removed_count = m_entity_group_manager->removeEntitiesFromGroup(entity_group_id, entity_vector);
6✔
234

235
    if (removed_count > 0) {
6✔
236
        qDebug() << "GroupManager: Removed" << removed_count << "entities from group" << group_id;
4✔
237
        emit groupModified(group_id);
4✔
238
    }
239

240
    return removed_count > 0;
6✔
241
}
6✔
242

243
void GroupManager::ungroupEntities(std::unordered_set<EntityId> const & entity_ids) {
1✔
244
    std::unordered_set<int> affected_groups;
1✔
245

246
    // For each entity, find which groups it belongs to and remove it
247
    for (EntityId const entity_id: entity_ids) {
2✔
248
        auto group_ids = m_entity_group_manager->getGroupsContainingEntity(entity_id);
1✔
249
        for (GroupId const entity_group_id: group_ids) {
3✔
250
            auto const group_id = static_cast<int>(entity_group_id);
2✔
251
            affected_groups.insert(group_id);
2✔
252

253
            std::vector<EntityId> const single_entity = {entity_id};
6✔
254
            m_entity_group_manager->removeEntitiesFromGroup(entity_group_id, single_entity);
2✔
255
        }
2✔
256
    }
1✔
257

258
    if (!affected_groups.empty()) {
1✔
259
        qDebug() << "GroupManager: Ungrouped" << entity_ids.size() << "entities from" << affected_groups.size() << "groups";
1✔
260
        for (int const gid: affected_groups) {
3✔
261
            emit groupModified(gid);
2✔
262
        }
263
    }
264
}
2✔
265

266
int GroupManager::getEntityGroup(EntityId id) const {
38✔
267
    auto group_ids = m_entity_group_manager->getGroupsContainingEntity(id);
38✔
268

269
    // Return the first group (assuming entities belong to at most one group for now)
270
    if (!group_ids.empty()) {
38✔
271
        return static_cast<int>(group_ids[0]);
13✔
272
    }
273

274
    return -1;// Not in any group
25✔
275
}
38✔
276

UNCOV
277
QColor GroupManager::getEntityColor(EntityId id, QColor const & default_color) const {
×
UNCOV
278
    int const group_id = getEntityGroup(id);
×
UNCOV
279
    if (group_id == -1) {
×
UNCOV
280
        return default_color;
×
281
    }
282

283
    return m_group_colors.value(group_id, default_color);
×
284
}
285

286
bool GroupManager::isEntityGroupVisible(EntityId id) const {
10✔
287
    int const group_id = getEntityGroup(id);
10✔
288
    if (group_id == -1) {
10✔
289
        return true; // Entities not in a group are always visible
1✔
290
    }
291

292
    return m_group_visibility.value(group_id, true);
9✔
293
}
294

295
int GroupManager::getGroupMemberCount(int group_id) const {
29✔
296
    auto entity_group_id = static_cast<GroupId>(group_id);
29✔
297
    return static_cast<int>(m_entity_group_manager->getGroupSize(entity_group_id));
29✔
298
}
299

300
void GroupManager::clearAllGroups() {
×
301
    qDebug() << "GroupManager: Clearing all groups";
×
302

303
    // Clear EntityGroupManager
304
    m_entity_group_manager->clear();
×
305

306
    // Clear our color and visibility mappings
UNCOV
307
    m_group_colors.clear();
×
UNCOV
308
    m_group_visibility.clear();
×
309

UNCOV
310
    m_next_group_id = 1;
×
311

312
    // Note: We don't emit specific signals here since everything is being cleared
UNCOV
313
}
×
314

315
QColor GroupManager::getNextDefaultColor() const {
41✔
316
    if (DEFAULT_COLORS.isEmpty()) {
41✔
UNCOV
317
        return {128, 128, 128};// Fallback gray
×
318
    }
319

320
    // Cycle through the default colors based on current group count
321
    auto group_ids = m_entity_group_manager->getAllGroupIds();
41✔
322
    auto color_index = static_cast<int>(group_ids.size()) % DEFAULT_COLORS.size();
41✔
323
    return DEFAULT_COLORS[color_index];
41✔
324
}
41✔
325

326
// ===== Common Group Operations for Context Menus =====
327

UNCOV
328
int GroupManager::createGroupWithEntities(std::unordered_set<EntityId> const & entity_ids) {
×
UNCOV
329
    if (entity_ids.empty()) {
×
UNCOV
330
        return -1;
×
331
    }
332

UNCOV
333
    QString group_name = QString("Group %1").arg(m_entity_group_manager->getAllGroupIds().size() + 1);
×
UNCOV
334
    int group_id = createGroup(group_name);
×
335
    
UNCOV
336
    if (group_id != -1) {
×
UNCOV
337
        assignEntitiesToGroup(group_id, entity_ids);
×
338
    }
339
    
UNCOV
340
    return group_id;
×
UNCOV
341
}
×
342

UNCOV
343
std::vector<std::pair<int, QString>> GroupManager::getGroupsForContextMenu() const {
×
UNCOV
344
    std::vector<std::pair<int, QString>> result;
×
345
    
UNCOV
346
    auto groups = getGroups();
×
UNCOV
347
    for (auto it = groups.begin(); it != groups.end(); ++it) {
×
UNCOV
348
        result.emplace_back(it.key(), it.value().name);
×
349
    }
350
    
UNCOV
351
    return result;
×
UNCOV
352
}
×
353

354
bool GroupManager::mergeGroups(int target_group_id, std::vector<int> const & source_group_ids) {
5✔
355
    // Validate target group exists
356
    auto target_entity_group_id = static_cast<GroupId>(target_group_id);
5✔
357
    if (!m_entity_group_manager->hasGroup(target_entity_group_id)) {
5✔
358
        qDebug() << "GroupManager: Target group" << target_group_id << "does not exist";
1✔
359
        return false;
1✔
360
    }
361

362
    // Validate source groups exist and are different from target
363
    for (int source_group_id : source_group_ids) {
7✔
364
        if (source_group_id == target_group_id) {
5✔
365
            qDebug() << "GroupManager: Cannot merge group into itself:" << source_group_id;
1✔
366
            return false;
1✔
367
        }
368
        
369
        auto source_entity_group_id = static_cast<GroupId>(source_group_id);
4✔
370
        if (!m_entity_group_manager->hasGroup(source_entity_group_id)) {
4✔
371
            qDebug() << "GroupManager: Source group" << source_group_id << "does not exist";
1✔
372
            return false;
1✔
373
        }
374
    }
375

376
    // Collect all entities from source groups
377
    std::unordered_set<EntityId> entities_to_merge;
2✔
378
    for (int source_group_id : source_group_ids) {
5✔
379
        auto source_entity_group_id = static_cast<GroupId>(source_group_id);
3✔
380
        auto entities_in_group = m_entity_group_manager->getEntitiesInGroup(source_entity_group_id);
3✔
381
        
382
        for (EntityId entity_id : entities_in_group) {
10✔
383
            entities_to_merge.insert(entity_id);
7✔
384
        }
385
    }
3✔
386

387
    // Move all entities to target group
388
    if (!entities_to_merge.empty()) {
2✔
389
        std::vector<EntityId> entities_vector(entities_to_merge.begin(), entities_to_merge.end());
6✔
390
        m_entity_group_manager->addEntitiesToGroup(target_entity_group_id, entities_vector);
2✔
391
    }
2✔
392

393
    // Remove source groups
394
    for (int source_group_id : source_group_ids) {
5✔
395
        auto source_entity_group_id = static_cast<GroupId>(source_group_id);
3✔
396
        
397
        // Remove all entities from source group first
398
        auto entities_in_group = m_entity_group_manager->getEntitiesInGroup(source_entity_group_id);
3✔
399
        if (!entities_in_group.empty()) {
3✔
400
            std::vector<EntityId> entities_vector(entities_in_group.begin(), entities_in_group.end());
9✔
401
            m_entity_group_manager->removeEntitiesFromGroup(source_entity_group_id, entities_vector);
3✔
402
        }
3✔
403
        
404
        // Delete the source group
405
        m_entity_group_manager->deleteGroup(source_entity_group_id);
3✔
406
        
407
        // Clean up our mappings
408
        m_group_colors.remove(source_group_id);
3✔
409
        m_group_visibility.remove(source_group_id);
3✔
410
        
411
        qDebug() << "GroupManager: Removed source group" << source_group_id;
3✔
412
        emit groupRemoved(source_group_id);
3✔
413
    }
3✔
414

415
    qDebug() << "GroupManager: Merged" << source_group_ids.size() << "groups into group" << target_group_id;
2✔
416
    emit groupModified(target_group_id);
2✔
417
    
418
    return true;
2✔
419
}
2✔
420

UNCOV
421
bool GroupManager::deleteGroupAndEntities(int group_id) {
×
422
    auto entity_group_id = static_cast<GroupId>(group_id);
×
423

UNCOV
424
    if (!m_entity_group_manager->hasGroup(entity_group_id)) {
×
UNCOV
425
        return false;
×
426
    }
427

428
    // Get all entities in the group
429
    auto entities = m_entity_group_manager->getEntitiesInGroup(entity_group_id);
×
430
    if (entities.empty()) {
×
431
        // No entities to delete, just remove the group
432
        return removeGroup(group_id);
×
433
    }
434

UNCOV
435
    qDebug() << "GroupManager: Deleting group" << group_id << "with" << entities.size() << "entities";
×
436

437
    // Remove entities from their respective data objects
438
    for (EntityId const entity_id : entities) {
×
UNCOV
439
        removeEntityFromDataObjects(entity_id);
×
440
    }
441

442
    // Remove the group (this will also remove all entity-group associations)
UNCOV
443
    bool const group_removed = removeGroup(group_id);
×
444

UNCOV
445
    if (group_removed) {
×
UNCOV
446
        qDebug() << "GroupManager: Successfully deleted group" << group_id << "and all its entities";
×
447
    }
448

UNCOV
449
    return group_removed;
×
UNCOV
450
}
×
451

452
void GroupManager::removeEntityFromDataObjects(EntityId entity_id) {
×
453
    // Get all data keys from the DataManager
UNCOV
454
    auto const data_keys = m_data_manager->getAllKeys();
×
455
    
456
    for (std::string const & key : data_keys) {
×
UNCOV
457
        auto data_variant = m_data_manager->getDataVariant(key);
×
UNCOV
458
        if (!data_variant.has_value()) {
×
459
            continue;
×
460
        }
461

462
        // Handle different data types
UNCOV
463
        std::visit([this, &key, entity_id](auto & data_ptr) {
×
464
            if constexpr (std::is_same_v<std::decay_t<decltype(data_ptr)>, std::shared_ptr<PointData>>) {
UNCOV
465
                if (data_ptr) {
×
UNCOV
466
                    removeEntityFromPointData(data_ptr.get(), entity_id);
×
467
                }
468
            } else if constexpr (std::is_same_v<std::decay_t<decltype(data_ptr)>, std::shared_ptr<LineData>>) {
UNCOV
469
                if (data_ptr) {
×
UNCOV
470
                    removeEntityFromLineData(data_ptr.get(), entity_id);
×
471
                }
472
            }
473
            // Note: DigitalEventSeries and DigitalIntervalSeries don't have entity lookup methods
474
            // so we skip them for now
UNCOV
475
        }, data_variant.value());
×
UNCOV
476
    }
×
UNCOV
477
}
×
478

UNCOV
479
void GroupManager::removeEntityFromPointData(PointData * point_data, EntityId entity_id) {
×
UNCOV
480
    if (!point_data) return;
×
481

482
    // Find the time and index for this entity
UNCOV
483
    auto time_and_index = point_data->getTimeAndIndexByEntityId(entity_id);
×
UNCOV
484
    if (!time_and_index.has_value()) {
×
UNCOV
485
        return;
×
486
    }
487

UNCOV
488
    auto const [time, index] = time_and_index.value();
×
489
    
490
    // Remove the point at the specific time and index
UNCOV
491
    point_data->clearAtTime(time, static_cast<size_t>(index), true);
×
492
}
493

UNCOV
494
void GroupManager::removeEntityFromLineData(LineData * line_data, EntityId entity_id) {
×
UNCOV
495
    if (!line_data) return;
×
496

497
    // Find the time and index for this entity
UNCOV
498
    auto time_and_index = line_data->getTimeAndIndexByEntityId(entity_id);
×
UNCOV
499
    if (!time_and_index.has_value()) {
×
UNCOV
500
        return;
×
501
    }
502

UNCOV
503
    auto const [time, index] = time_and_index.value();
×
504
    
505
    // Remove the line at the specific time and index
UNCOV
506
    line_data->clearAtTime(time, index, true);
×
507
}
508

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