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

paulmthompson / WhiskerToolbox / 18762825348

23 Oct 2025 09:42PM UTC coverage: 72.822% (+0.3%) from 72.522%
18762825348

push

github

paulmthompson
add boolean digital interval logic test

693 of 711 new or added lines in 5 files covered. (97.47%)

718 existing lines in 10 files now uncovered.

54997 of 75522 relevant lines covered (72.82%)

45740.9 hits per line

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

30.74
/src/WhiskerToolbox/DataViewer_Widget/OpenGLWidget.cpp
1
#include "OpenGLWidget.hpp"
2

3
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
4
#include "DataManager/utils/color.hpp"
5
#include "DataViewer/AnalogTimeSeries/AnalogTimeSeriesDisplayOptions.hpp"
6
#include "DataViewer/AnalogTimeSeries/MVP_AnalogTimeSeries.hpp"
7
#include "DataViewer/DigitalEvent/DigitalEventSeriesDisplayOptions.hpp"
8
#include "DataViewer/DigitalEvent/MVP_DigitalEvent.hpp"
9
#include "DataViewer/DigitalInterval/DigitalIntervalSeriesDisplayOptions.hpp"
10
#include "DataViewer/DigitalInterval/MVP_DigitalInterval.hpp"
11
#include "DataViewer/PlottingManager/PlottingManager.hpp"
12
#include "DataViewer_Widget.hpp"
13
#include "DigitalTimeSeries/Digital_Event_Series.hpp"
14
#include "DigitalTimeSeries/Digital_Interval_Series.hpp"
15
#include "ShaderManager/ShaderManager.hpp"
16
#include "TimeFrame/TimeFrame.hpp"
17

18
#include <QMouseEvent>
19
#include <QOpenGLContext>
20
#include <QOpenGLFunctions>
21
#include <QOpenGLShader>
22
#include <QPainter>
23
#include <glm/glm.hpp>
24
#include <glm/gtc/matrix_transform.hpp>
25
//#include <glm/gtx/transform.hpp>
26

27
#include <algorithm>
28
#include <chrono>
29
#include <cstdlib>
30
#include <ctime>
31
#include <iostream>
32
#include <ranges>
33

34

35
// This was a helpful resource for making a dashed line:
36
//https://stackoverflow.com/questions/52928678/dashed-line-in-opengl3
37

38
/*
39

40
Currently this is pretty hacky OpenGL code that will be refactored to take advantage of
41
OpenGL.
42

43
I will establish a Model-View-Projection matrix framework.
44

45
1) model matrix (object to world space)
46
    - can be used to shift data up or down?
47
2) view matrix (world space to camera space)
48
    - Camera panning
49
3) projection matrix (camera space to clip space)
50
    - orthographic projection
51
    - zooming and visible area definition
52

53
I will also only select the data that is present
54

55

56
*/
57

58
OpenGLWidget::OpenGLWidget(QWidget * parent)
20✔
59
    : QOpenGLWidget(parent) {
100✔
60
    setMouseTracking(true);// Enable mouse tracking for hover events
20✔
61
    
62
    // Initialize tooltip timer
63
    _tooltip_timer = new QTimer(this);
20✔
64
    _tooltip_timer->setSingleShot(true);
20✔
65
    _tooltip_timer->setInterval(TOOLTIP_DELAY_MS);
20✔
66
    connect(_tooltip_timer, &QTimer::timeout, this, [this]() {
20✔
UNCOV
67
        showSeriesInfoTooltip(_tooltip_hover_pos);
×
UNCOV
68
    });
×
69
}
20✔
70

71
OpenGLWidget::~OpenGLWidget() {
40✔
72
    cleanup();
20✔
73
}
40✔
74

75
void OpenGLWidget::updateCanvas(int time) {
200✔
76
    _time = time;
200✔
77
    //std::cout << "Redrawing at " << _time << std::endl;
78
    update();
200✔
79
}
200✔
80

81
// Add these implementations:
82
void OpenGLWidget::mousePressEvent(QMouseEvent * event) {
×
UNCOV
83
    if (event->button() == Qt::LeftButton) {
×
84
        // Check if we're clicking near an interval edge for dragging
85
        auto edge_info = findIntervalEdgeAtPosition(static_cast<float>(event->pos().x()), static_cast<float>(event->pos().y()));
×
UNCOV
86
        if (edge_info.has_value()) {
×
UNCOV
87
            auto const [series_key, is_left_edge] = edge_info.value();
×
88
            startIntervalDrag(series_key, is_left_edge, event->pos());
×
89
            return;// Don't start panning when dragging intervals
×
UNCOV
90
        }
×
91

92
        _isPanning = true;
×
UNCOV
93
        _lastMousePos = event->pos();
×
94

95
        // Emit click coordinates for interval selection
96
        float const canvas_x = static_cast<float>(event->pos().x());
×
97
        float const canvas_y = static_cast<float>(event->pos().y());
×
98

99
        // Convert canvas X to time coordinate
100
        float const time_coord = canvasXToTime(canvas_x);
×
101

102
        // Find the closest analog series for Y coordinate conversion (similar to mouseMoveEvent)
UNCOV
103
        QString series_info = "";
×
UNCOV
104
        if (!_analog_series.empty()) {
×
UNCOV
105
            for (auto const & [key, data]: _analog_series) {
×
106
                if (data.display_options->is_visible) {
×
107
                    float const analog_value = canvasYToAnalogValue(canvas_y, key);
×
108
                    series_info = QString("Series: %1, Value: %2").arg(QString::fromStdString(key)).arg(analog_value, 0, 'f', 3);
×
UNCOV
109
                    break;
×
110
                }
111
            }
112
        }
113

114
        emit mouseClick(time_coord, canvas_y, series_info);
×
115
    }
×
UNCOV
116
    QOpenGLWidget::mousePressEvent(event);
×
117
}
118

UNCOV
119
void OpenGLWidget::mouseMoveEvent(QMouseEvent * event) {
×
120
    if (_is_dragging_interval) {
×
121
        // Update interval drag
UNCOV
122
        updateIntervalDrag(event->pos());
×
UNCOV
123
        cancelTooltipTimer(); // Cancel tooltip during drag
×
124
        return;// Don't do other mouse move processing while dragging
×
125
    }
126

UNCOV
127
    if (_is_creating_new_interval) {
×
128
        // Update new interval creation
UNCOV
129
        updateNewIntervalCreation(event->pos());
×
130
        cancelTooltipTimer(); // Cancel tooltip during interval creation
×
UNCOV
131
        return;// Don't do other mouse move processing while creating
×
132
    }
133

UNCOV
134
    if (_isPanning) {
×
135
        // Calculate vertical movement in pixels
136
        int const deltaY = event->pos().y() - _lastMousePos.y();
×
137

138
        // Convert to normalized device coordinates
139
        // A positive deltaY (moving down) should move the view up
140
        float const normalizedDeltaY = -1.0f * static_cast<float>(deltaY) / static_cast<float>(height()) * 2.0f;
×
141

142
        // Adjust vertical offset based on movement
143
        _verticalPanOffset += normalizedDeltaY;
×
144

145
        _lastMousePos = event->pos();
×
UNCOV
146
        update();// Request redraw
×
UNCOV
147
        cancelTooltipTimer(); // Cancel tooltip during panning
×
148
    } else {
149
        // Check for cursor changes when hovering near interval edges
UNCOV
150
        auto edge_info = findIntervalEdgeAtPosition(static_cast<float>(event->pos().x()), static_cast<float>(event->pos().y()));
×
UNCOV
151
        if (edge_info.has_value()) {
×
152
            setCursor(Qt::SizeHorCursor);
×
UNCOV
153
            cancelTooltipTimer(); // Don't show tooltip when hovering over interval edges
×
154
        } else {
155
            setCursor(Qt::ArrowCursor);
×
156
            // Start tooltip timer for series info
UNCOV
157
            startTooltipTimer(event->pos());
×
158
        }
159
    }
×
160

161
    // Emit hover coordinates for coordinate display
162
    float const canvas_x = static_cast<float>(event->pos().x());
×
UNCOV
163
    float const canvas_y = static_cast<float>(event->pos().y());
×
164

165
    // Convert canvas X to time coordinate
UNCOV
166
    float const time_coord = canvasXToTime(canvas_x);
×
167

168
    // Find the closest analog series for Y coordinate conversion
169
    QString series_info = "";
×
170
    if (!_analog_series.empty()) {
×
171
        // For now, use the first visible analog series for Y coordinate conversion
172
        for (auto const & [key, data]: _analog_series) {
×
173
            if (data.display_options->is_visible) {
×
174
                float const analog_value = canvasYToAnalogValue(canvas_y, key);
×
175
                series_info = QString("Series: %1, Value: %2").arg(QString::fromStdString(key)).arg(analog_value, 0, 'f', 3);
×
176
                break;
×
177
            }
178
        }
179
    }
180

UNCOV
181
    emit mouseHover(time_coord, canvas_y, series_info);
×
182

183
    QOpenGLWidget::mouseMoveEvent(event);
×
UNCOV
184
}
×
185

186
void OpenGLWidget::mouseReleaseEvent(QMouseEvent * event) {
×
187
    if (event->button() == Qt::LeftButton) {
×
188
        if (_is_dragging_interval) {
×
UNCOV
189
            finishIntervalDrag();
×
190
        } else if (_is_creating_new_interval) {
×
191
            finishNewIntervalCreation();
×
192
        } else {
193
            _isPanning = false;
×
194
        }
195
    }
196
    QOpenGLWidget::mouseReleaseEvent(event);
×
UNCOV
197
}
×
198

199
void OpenGLWidget::leaveEvent(QEvent * event) {
2✔
200
    // Cancel tooltip when mouse leaves the widget
201
    cancelTooltipTimer();
2✔
202
    QOpenGLWidget::leaveEvent(event);
2✔
203
}
2✔
204

UNCOV
205
void OpenGLWidget::setBackgroundColor(std::string const & hexColor) {
×
UNCOV
206
    m_background_color = hexColor;
×
UNCOV
207
    updateCanvas(_time);
×
UNCOV
208
}
×
209

UNCOV
210
void OpenGLWidget::setPlotTheme(PlotTheme theme) {
×
UNCOV
211
    _plot_theme = theme;
×
212

UNCOV
213
    if (theme == PlotTheme::Dark) {
×
214
        // Dark theme: black background, white axes
215
        m_background_color = "#000000";
×
UNCOV
216
        m_axis_color = "#FFFFFF";
×
217
    } else {
218
        // Light theme: white background, dark axes
UNCOV
219
        m_background_color = "#FFFFFF";
×
UNCOV
220
        m_axis_color = "#333333";
×
221
    }
222

223
    updateCanvas(_time);
×
UNCOV
224
}
×
225

226
void OpenGLWidget::cleanup() {
38✔
227
    // Avoid re-entrancy or cleanup without a valid context
228
    if (!_gl_initialized) {
38✔
229
        return;
20✔
230
    }
231

232
    // Guard: QOpenGLContext may already be gone during teardown
233
    if (QOpenGLContext::currentContext() == nullptr && context() == nullptr) {
18✔
UNCOV
234
        _gl_initialized = false;
×
UNCOV
235
        return;
×
236
    }
237

238
    // Safe to release our GL resources
239
    makeCurrent();
18✔
240

241
    if (m_program) {
18✔
242
        delete m_program;
×
UNCOV
243
        m_program = nullptr;
×
244
    }
245
    if (m_dashedProgram) {
18✔
246
        delete m_dashedProgram;
×
UNCOV
247
        m_dashedProgram = nullptr;
×
248
    }
249

250
    m_vbo.destroy();
18✔
251
    m_vao.destroy();
18✔
252

253
    doneCurrent();
18✔
254

255
    _gl_initialized = false;
18✔
256
}
257

UNCOV
258
QOpenGLShaderProgram * create_shader_program(char const * vertexShaderSource,
×
259
                                             char const * fragmentShaderSource) {
UNCOV
260
    auto prog = new QOpenGLShaderProgram;
×
UNCOV
261
    prog->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource);
×
UNCOV
262
    prog->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource);
×
263

UNCOV
264
    prog->link();
×
265

UNCOV
266
    return prog;
×
267
}
268

269
// Replace the old create_shader_program with a new version that loads from resource files for the dashed shader
UNCOV
270
QOpenGLShaderProgram * create_shader_program_from_resource(QString const & vertexResource, QString const & fragmentResource) {
×
UNCOV
271
    auto prog = new QOpenGLShaderProgram;
×
UNCOV
272
    prog->addShaderFromSourceFile(QOpenGLShader::Vertex, vertexResource);
×
UNCOV
273
    prog->addShaderFromSourceFile(QOpenGLShader::Fragment, fragmentResource);
×
UNCOV
274
    prog->link();
×
UNCOV
275
    return prog;
×
276
}
277

278
void OpenGLWidget::initializeGL() {
18✔
279
    // Ensure QOpenGLFunctions is initialized
280
    initializeOpenGLFunctions();
18✔
281

282
    // Track GL init and connect context destruction
283
    _gl_initialized = true;
18✔
284
    if (context()) {
18✔
285
        // Disconnect any previous connection to avoid duplicates
286
        if (_ctxAboutToBeDestroyedConn) {
18✔
UNCOV
287
            disconnect(_ctxAboutToBeDestroyedConn);
×
288
        }
289
        _ctxAboutToBeDestroyedConn = connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, [this]() {
36✔
290
            cleanup();
18✔
291
        });
18✔
292
    }
293

294
    auto fmt = format();
18✔
295
    std::cout << "OpenGL major version: " << fmt.majorVersion() << std::endl;
18✔
296
    std::cout << "OpenGL minor version: " << fmt.minorVersion() << std::endl;
18✔
297
    int r, g, b;
18✔
298
    hexToRGB(m_background_color, r, g, b);
18✔
299
    glClearColor(
18✔
300
            static_cast<float>(r) / 255.0f,
18✔
301
            static_cast<float>(g) / 255.0f,
18✔
302
            static_cast<float>(b) / 255.0f,
18✔
303
            1.0f);
304
    glEnable(GL_BLEND);
18✔
305
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
18✔
306
    // Load axes shader
307
    ShaderManager::instance().loadProgram(
162✔
308
            "axes",
309
            m_shaderSourceType == ShaderSourceType::Resource ? ":/shaders/colored_vertex.vert" : "src/WhiskerToolbox/shaders/colored_vertex.vert",
18✔
310
            m_shaderSourceType == ShaderSourceType::Resource ? ":/shaders/colored_vertex.frag" : "src/WhiskerToolbox/shaders/colored_vertex.frag",
18✔
311
            "",
312
            m_shaderSourceType);
313
    // Load dashed line shader
314
    ShaderManager::instance().loadProgram(
162✔
315
            "dashed_line",
316
            m_shaderSourceType == ShaderSourceType::Resource ? ":/shaders/dashed_line.vert" : "src/WhiskerToolbox/shaders/dashed_line.vert",
18✔
317
            m_shaderSourceType == ShaderSourceType::Resource ? ":/shaders/dashed_line.frag" : "src/WhiskerToolbox/shaders/dashed_line.frag",
18✔
318
            "",
319
            m_shaderSourceType);
320

321
    // Get uniform locations for axes shader
322
    auto axesProgram = ShaderManager::instance().getProgram("axes");
54✔
323
    if (axesProgram) {
18✔
324
        auto nativeProgram = axesProgram->getNativeProgram();
18✔
325
        if (nativeProgram) {
18✔
326
            m_projMatrixLoc = nativeProgram->uniformLocation("projMatrix");
18✔
327
            m_viewMatrixLoc = nativeProgram->uniformLocation("viewMatrix");
18✔
328
            m_modelMatrixLoc = nativeProgram->uniformLocation("modelMatrix");
18✔
329
            m_colorLoc = nativeProgram->uniformLocation("u_color");
18✔
330
            m_alphaLoc = nativeProgram->uniformLocation("u_alpha");
18✔
331
        }
332
    }
333

334
    // Get uniform locations for dashed line shader
335
    auto dashedProgram = ShaderManager::instance().getProgram("dashed_line");
54✔
336
    if (dashedProgram) {
18✔
337
        auto nativeProgram = dashedProgram->getNativeProgram();
18✔
338
        if (nativeProgram) {
18✔
339
            m_dashedProjMatrixLoc = nativeProgram->uniformLocation("u_mvp");
18✔
340
            m_dashedResolutionLoc = nativeProgram->uniformLocation("u_resolution");
18✔
341
            m_dashedDashSizeLoc = nativeProgram->uniformLocation("u_dashSize");
18✔
342
            m_dashedGapSizeLoc = nativeProgram->uniformLocation("u_gapSize");
18✔
343
        }
344
    }
345

346
    // Connect reload signal to redraw
347
    connect(&ShaderManager::instance(), &ShaderManager::shaderReloaded, this, [this](std::string const &) { update(); });
18✔
348
    m_vao.create();
18✔
349
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
18✔
350
    m_vbo.create();
18✔
351
    m_vbo.bind();
18✔
352
    m_vbo.setUsagePattern(QOpenGLBuffer::StaticDraw);
18✔
353
    setupVertexAttribs();
18✔
354
}
36✔
355

356
void OpenGLWidget::setupVertexAttribs() {
294✔
357

358
    m_vbo.bind();                     // glBindBuffer(GL_ARRAY_BUFFER, m_vbo.bufferId());
294✔
359
    int const vertex_argument_num = 4;// Position (x, y, 0, 1) for axes shader
294✔
360

361
    // Attribute 0: vertex positions (x, y, 0, 1) for axes shader
362
    glEnableVertexAttribArray(0);
294✔
363
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, vertex_argument_num * sizeof(GLfloat), nullptr);
294✔
364

365
    // Disable unused vertex attributes
366
    glDisableVertexAttribArray(1);
294✔
367
    glDisableVertexAttribArray(2);
294✔
368

369
    m_vbo.release();
294✔
370
}
294✔
371

372
///////////////////////////////////////////////////////////////////////////////
373

374
/**
375
 * @brief OpenGLWidget::drawDigitalEventSeries
376
 *
377
 * Each event is specified by a single time point.
378
 * We can find which of the time points are within the visible time frame
379
 * After those are found, we will draw a vertical line at that time point
380
 * 
381
 * Now supports two display modes:
382
 * - Stacked: Events are positioned in separate horizontal lanes with configurable spacing
383
 * - Full Canvas: Events stretch from top to bottom of the canvas (original behavior)
384
 */
385
void OpenGLWidget::drawDigitalEventSeries() {
69✔
386
    int r, g, b;
69✔
387
    auto const start_time = _xAxis.getStart();
69✔
388
    auto const end_time = _xAxis.getEnd();
69✔
389
    auto axesProgram = ShaderManager::instance().getProgram("axes");
207✔
390
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
69✔
391

392
    auto const min_y = _yMin;
69✔
393
    auto const max_y = _yMax;
69✔
394

395
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);// glBindVertexArray
69✔
396
    setupVertexAttribs();
69✔
397

398
    // Count visible event series for stacked positioning
399
    int visible_event_count = 0;
69✔
400
    for (auto const & [key, event_data]: _digital_event_series) {
95✔
401
        if (event_data.display_options->is_visible) {
26✔
402
            visible_event_count++;
26✔
403
        }
404
    }
405

406
    if (visible_event_count == 0 || !_master_time_frame) {
69✔
407
        glUseProgram(0);
57✔
408
        return;
57✔
409
    }
410

411
    // Calculate center coordinate for stacked mode (similar to analog series)
412
    //float const center_coord = -0.5f * 0.1f * (static_cast<float>(visible_event_count - 1));// Use default spacing for center calculation
413

414
    int visible_series_index = 0;// counts all visible event series (for iteration)
12✔
415
    int stacked_series_index = 0;// index among stacked-mode event series only
12✔
416
    int const total_analog_visible = static_cast<int>(_plotting_manager->getVisibleAnalogSeriesKeys().size());
12✔
417
    // Count stacked-mode events (exclude FullCanvas from stackable count)
418
    int stacked_event_count = 0;
12✔
419
    for (auto const & [key, event_data]: _digital_event_series) {
38✔
420
        if (event_data.display_options->is_visible &&
52✔
421
            event_data.display_options->display_mode == EventDisplayMode::Stacked) {
26✔
422
            stacked_event_count++;
25✔
423
        }
424
    }
425
    int const total_stackable_series = total_analog_visible + stacked_event_count;
12✔
426

427
    for (auto const & [key, event_data]: _digital_event_series) {
38✔
428
        auto const & series = event_data.series;
26✔
429
        auto const & time_frame = event_data.time_frame;
26✔
430
        auto const & display_options = event_data.display_options;
26✔
431

432
        if (!display_options->is_visible) continue;
26✔
433

434
        hexToRGB(display_options->hex_color, r, g, b);
26✔
435
        float const rNorm = static_cast<float>(r) / 255.0f;
26✔
436
        float const gNorm = static_cast<float>(g) / 255.0f;
26✔
437
        float const bNorm = static_cast<float>(b) / 255.0f;
26✔
438
        float const alpha = display_options->alpha;
26✔
439

440
        auto visible_events = series->getEventsInRange(TimeFrameIndex(start_time),
26✔
441
                                                       TimeFrameIndex(end_time),
442
                                                       _master_time_frame.get(),
26✔
443
                                                       time_frame.get());
52✔
444

445
        // === MVP MATRIX SETUP ===
446

447
        // We need to check if we have a PlottingManager reference
448
        if (!_plotting_manager) {
26✔
UNCOV
449
            std::cerr << "Warning: PlottingManager not set in OpenGLWidget" << std::endl;
×
UNCOV
450
            continue;
×
451
        }
452

453
        // Determine plotting mode and allocate accordingly
454
        display_options->plotting_mode = (display_options->display_mode == EventDisplayMode::Stacked)
52✔
455
                                                 ? EventPlottingMode::Stacked
26✔
456
                                                 : EventPlottingMode::FullCanvas;
457

458
        float allocated_y_center = 0.0f;
26✔
459
        float allocated_height = 0.0f;
26✔
460
        if (display_options->plotting_mode == EventPlottingMode::Stacked) {
26✔
461
            // Use global stacked allocation across analog + stacked events
462
            _plotting_manager->calculateGlobalStackedAllocation(-1, stacked_series_index, total_stackable_series,
25✔
463
                                                                allocated_y_center, allocated_height);
464
            stacked_series_index++;
25✔
465
        } else {
466
            // Full canvas allocation
467
            allocated_y_center = (_plotting_manager->viewport_y_min + _plotting_manager->viewport_y_max) * 0.5f;
1✔
468
            allocated_height = _plotting_manager->viewport_y_max - _plotting_manager->viewport_y_min;
1✔
469
        }
470

471
        display_options->allocated_y_center = allocated_y_center;
26✔
472
        display_options->allocated_height = allocated_height;
26✔
473

474
        // Apply PlottingManager pan offset
475
        _plotting_manager->setPanOffset(_verticalPanOffset);
26✔
476

477
        auto Model = new_getEventModelMat(*display_options, *_plotting_manager);
26✔
478
        auto View = new_getEventViewMat(*display_options, *_plotting_manager);
26✔
479
        auto Projection = new_getEventProjectionMat(start_time, end_time, _yMin, _yMax, *_plotting_manager);
26✔
480

481
        glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
26✔
482
        glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
26✔
483
        glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
26✔
484

485
        // Set color and alpha uniforms
486
        glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
26✔
487
        glUniform1f(m_alphaLoc, alpha);
26✔
488

489
        // Set line thickness from display options
490
        glLineWidth(static_cast<float>(display_options->line_thickness));
26✔
491

492
        for (auto const & event: visible_events) {
26✔
493
            // Calculate X position in master time frame coordinates for consistent rendering
494
            float xCanvasPos;
UNCOV
495
            if (time_frame.get() == _master_time_frame.get()) {
×
496
                // Same time frame - event is already in correct coordinates
UNCOV
497
                xCanvasPos = event;
×
498
            } else {
499
                // Different time frames - convert event index to time, then to master time frame
UNCOV
500
                float event_time = static_cast<float>(time_frame->getTimeAtIndex(TimeFrameIndex(static_cast<int>(event))));
×
UNCOV
501
                xCanvasPos = event_time;// This should work if both time frames use the same time units
×
502
            }
503

504
            // Provide local [-1, 1] vertical endpoints; Model handles placement/scale
UNCOV
505
            std::array<GLfloat, 8> vertices = {
×
506
                    xCanvasPos, -1.0f, 0.0f, 1.0f,
UNCOV
507
                    xCanvasPos,  1.0f, 0.0f, 1.0f};
×
508

UNCOV
509
            glBindBuffer(GL_ARRAY_BUFFER, m_vbo.bufferId());
×
UNCOV
510
            m_vbo.allocate(vertices.data(), vertices.size() * sizeof(GLfloat));
×
511

UNCOV
512
            GLint const first = 0;  // Starting index of enabled array
×
UNCOV
513
            GLsizei const count = 2;// number of vertices to render
×
UNCOV
514
            glDrawArrays(GL_LINES, first, count);
×
515
        }
516

517
        visible_series_index++;
26✔
518
    }
519

520
    // Reset line width to default
521
    glLineWidth(1.0f);
12✔
522
    glUseProgram(0);
12✔
523
}
69✔
524

525
///////////////////////////////////////////////////////////////////////////////
526

527
void OpenGLWidget::drawDigitalIntervalSeries() {
69✔
528
    int r, g, b;
69✔
529
    auto const start_time = static_cast<float>(_xAxis.getStart());
69✔
530
    auto const end_time = static_cast<float>(_xAxis.getEnd());
69✔
531

532
    //auto const min_y = _yMin;
533
    //auto const max_y = _yMax;
534

535
    auto axesProgram = ShaderManager::instance().getProgram("axes");
207✔
536
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
69✔
537

538
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);// glBindVertexArray
69✔
539
    setupVertexAttribs();
69✔
540

541
    if (!_master_time_frame) {
69✔
542
        glUseProgram(0);
×
543
        return;
×
544
    }
545

546
    for (auto const & [key, interval_data]: _digital_interval_series) {
69✔
UNCOV
547
        auto const & series = interval_data.series;
×
UNCOV
548
        auto const & time_frame = interval_data.time_frame;
×
UNCOV
549
        auto const & display_options = interval_data.display_options;
×
550

551
        if (!display_options->is_visible) continue;
×
552

553
        // Get only the intervals that overlap with the visible range
554
        // These will be
UNCOV
555
        auto visible_intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(
×
556
                TimeFrameIndex(static_cast<int64_t>(start_time)),
557
                TimeFrameIndex(static_cast<int64_t>(end_time)),
558
                _master_time_frame.get(),
×
UNCOV
559
                time_frame.get());
×
560

561
        hexToRGB(display_options->hex_color, r, g, b);
×
UNCOV
562
        float const rNorm = static_cast<float>(r) / 255.0f;
×
UNCOV
563
        float const gNorm = static_cast<float>(g) / 255.0f;
×
564
        float const bNorm = static_cast<float>(b) / 255.0f;
×
UNCOV
565
        float const alpha = display_options->alpha;
×
566

567
        // === MVP MATRIX SETUP ===
568

569
        // We need to check if we have a PlottingManager reference
570
        if (!_plotting_manager) {
×
571
            std::cerr << "Warning: PlottingManager not set in OpenGLWidget" << std::endl;
×
572
            continue;
×
573
        }
574

575
        // Calculate coordinate allocation from PlottingManager
576
        // Digital intervals typically use full canvas allocation
UNCOV
577
        float allocated_y_center, allocated_height;
×
578
        _plotting_manager->calculateDigitalIntervalSeriesAllocation(0, allocated_y_center, allocated_height);
×
579

580
        display_options->allocated_y_center = allocated_y_center;
×
UNCOV
581
        display_options->allocated_height = allocated_height;
×
582

583
        // Apply PlottingManager pan offset
UNCOV
584
        _plotting_manager->setPanOffset(_verticalPanOffset);
×
585

586
        auto Model = new_getIntervalModelMat(*display_options, *_plotting_manager);
×
587
        auto View = new_getIntervalViewMat(*_plotting_manager);
×
UNCOV
588
        auto Projection = new_getIntervalProjectionMat(start_time, end_time, _yMin, _yMax, *_plotting_manager);
×
589

590
        glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
×
UNCOV
591
        glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
×
UNCOV
592
        glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
×
593

594
        // Set color and alpha uniforms
595
        glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
×
UNCOV
596
        glUniform1f(m_alphaLoc, alpha);
×
597

598
        for (auto const & interval: visible_intervals) {
×
599

UNCOV
600
            std::cout << "interval.start:" << interval.start << "interval.end:" << interval.end << std::endl;
×
601

602
            auto start = static_cast<float>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.start)));
×
UNCOV
603
            auto end = static_cast<float>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.end)));
×
604

605
            //Clip the interval to the visible range
606
            start = std::max(start, start_time);
×
UNCOV
607
            end = std::min(end, end_time);
×
608

609
            float const xStart = start;
×
610
            float const xEnd = end;
×
611

612
            // Use normalized coordinates for intervals
613
            // The Model matrix will handle positioning and scaling
614
            float const interval_y_min = -1.0f;// Bottom of interval in local coordinates
×
615
            float const interval_y_max = +1.0f;// Top of interval in local coordinates
×
616

617
            // Create 4D vertices (x, y, 0, 1) to match the shader expectations
UNCOV
618
            std::array<GLfloat, 16> vertices = {
×
619
                    xStart, interval_y_min, 0.0f, 1.0f,
620
                    xEnd, interval_y_min, 0.0f, 1.0f,
621
                    xEnd, interval_y_max, 0.0f, 1.0f,
622
                    xStart, interval_y_max, 0.0f, 1.0f};
×
623

UNCOV
624
            m_vbo.bind();
×
UNCOV
625
            m_vbo.allocate(vertices.data(), vertices.size() * sizeof(GLfloat));
×
626
            m_vbo.release();
×
627

628
            GLint const first = 0;  // Starting index of enabled array
×
UNCOV
629
            GLsizei const count = 4;// number of indexes to render
×
UNCOV
630
            glDrawArrays(GL_TRIANGLE_FAN, first, count);
×
631
        }
632

633
        // Draw highlighting for selected intervals
UNCOV
634
        auto selected_interval = getSelectedInterval(key);
×
635
        if (selected_interval.has_value() && !(_is_dragging_interval && _dragging_series_key == key)) {
×
636
            auto const [sel_start_time, sel_end_time] = selected_interval.value();
×
637

638
            // Check if the selected interval overlaps with visible range
639
            if (sel_end_time >= static_cast<int64_t>(start_time) && sel_start_time <= static_cast<int64_t>(end_time)) {
×
640
                // Clip the selected interval to the visible range
641
                float const highlighted_start = std::max(static_cast<float>(sel_start_time), start_time);
×
642
                float const highlighted_end = std::min(static_cast<float>(sel_end_time), end_time);
×
643

644
                // Draw a thick border around the selected interval
645
                // Use a brighter version of the same color for highlighting
UNCOV
646
                float const highlight_rNorm = std::min(1.0f, rNorm + 0.3f);
×
UNCOV
647
                float const highlight_gNorm = std::min(1.0f, gNorm + 0.3f);
×
648
                float const highlight_bNorm = std::min(1.0f, bNorm + 0.3f);
×
649

650
                // Set line width for highlighting
651
                glLineWidth(4.0f);
×
652

653
                // Draw the four border lines of the rectangle
654
                // Set highlight color uniforms
UNCOV
655
                glUniform3f(m_colorLoc, highlight_rNorm, highlight_gNorm, highlight_bNorm);
×
UNCOV
656
                glUniform1f(m_alphaLoc, 1.0f);
×
657

658
                // Bottom edge
659
                std::array<GLfloat, 8> bottom_edge = {
×
660
                        highlighted_start, -1.0f, 0.0f, 1.0f,
661
                        highlighted_end, -1.0f, 0.0f, 1.0f};
×
662
                m_vbo.bind();
×
663
                m_vbo.allocate(bottom_edge.data(), bottom_edge.size() * sizeof(GLfloat));
×
UNCOV
664
                m_vbo.release();
×
UNCOV
665
                glDrawArrays(GL_LINES, 0, 2);
×
666

667
                // Top edge
668
                std::array<GLfloat, 8> top_edge = {
×
669
                        highlighted_start, 1.0f, 0.0f, 1.0f,
670
                        highlighted_end, 1.0f, 0.0f, 1.0f};
×
671
                m_vbo.bind();
×
672
                m_vbo.allocate(top_edge.data(), top_edge.size() * sizeof(GLfloat));
×
UNCOV
673
                m_vbo.release();
×
UNCOV
674
                glDrawArrays(GL_LINES, 0, 2);
×
675

676
                // Left edge
UNCOV
677
                std::array<GLfloat, 8> left_edge = {
×
678
                        highlighted_start, -1.0f, 0.0f, 1.0f,
UNCOV
679
                        highlighted_start, 1.0f, 0.0f, 1.0f};
×
UNCOV
680
                m_vbo.bind();
×
UNCOV
681
                m_vbo.allocate(left_edge.data(), left_edge.size() * sizeof(GLfloat));
×
UNCOV
682
                m_vbo.release();
×
UNCOV
683
                glDrawArrays(GL_LINES, 0, 2);
×
684

685
                // Right edge
UNCOV
686
                std::array<GLfloat, 8> right_edge = {
×
687
                        highlighted_end, -1.0f, 0.0f, 1.0f,
UNCOV
688
                        highlighted_end, 1.0f, 0.0f, 1.0f};
×
UNCOV
689
                m_vbo.bind();
×
UNCOV
690
                m_vbo.allocate(right_edge.data(), right_edge.size() * sizeof(GLfloat));
×
UNCOV
691
                m_vbo.release();
×
UNCOV
692
                glDrawArrays(GL_LINES, 0, 2);
×
693

694
                // Reset line width
UNCOV
695
                glLineWidth(1.0f);
×
696
            }
697
        }
698
    }
699

700
    glUseProgram(0);
69✔
701
}
69✔
702

703
///////////////////////////////////////////////////////////////////////////////
704

705
void OpenGLWidget::drawAnalogSeries() {
69✔
706
    int r, g, b;
69✔
707

708
    auto const start_time = TimeFrameIndex(_xAxis.getStart());
69✔
709
    auto const end_time = TimeFrameIndex(_xAxis.getEnd());
69✔
710

711
    auto axesProgram = ShaderManager::instance().getProgram("axes");
207✔
712
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
69✔
713

714
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
69✔
715
    setupVertexAttribs();
69✔
716

717
    if (!_master_time_frame) {
69✔
UNCOV
718
        glUseProgram(0);
×
UNCOV
719
        return;
×
720
    }
721

722
    int i = 0;
69✔
723

724
    for (auto const & [key, analog_data]: _analog_series) {
128✔
725
        auto const & series = analog_data.series;
59✔
726
        auto const & data = series->getAnalogTimeSeries();
59✔
727
        //if (!series->hasTimeFrameV2()) {
728
        //    continue;
729
        //}
730
        //auto time_frame = series->getTimeFrameV2().value();
731
        auto time_frame = analog_data.time_frame;
59✔
732

733
        auto const & display_options = analog_data.display_options;
59✔
734

735
        if (!display_options->is_visible) continue;
59✔
736

737
        // Calculate coordinate allocation from PlottingManager
738
        // For now, we'll use the analog series index to allocate coordinates
739
        // This is a temporary bridge until we fully migrate series management to PlottingManager
740
        if (!_plotting_manager) {
59✔
UNCOV
741
            std::cerr << "Warning: PlottingManager not set in OpenGLWidget" << std::endl;
×
UNCOV
742
            continue;
×
743
        }
744

745
        // Compute global stacked allocation so analog shares space with stacked events
746
        float allocated_y_center = 0.0f;
59✔
747
        float allocated_height = 0.0f;
59✔
748

749
        // Determine analog index among visible analogs using current analog-only allocation
750
        int const analog_visible_count = static_cast<int>(_plotting_manager->getVisibleAnalogSeriesKeys().size());
59✔
751
        int analog_index = i;
59✔
752
        float tmp_center = 0.0f;
59✔
753
        float tmp_height = 0.0f;
59✔
754
        if (_plotting_manager->getAnalogSeriesAllocationForKey(key, tmp_center, tmp_height) && tmp_height > 0.0f) {
59✔
755
            float const idxf = (tmp_center - _plotting_manager->viewport_y_min) / tmp_height - 0.5f;
59✔
756
            analog_index = std::clamp(static_cast<int>(std::round(idxf)), 0, std::max(0, analog_visible_count - 1));
59✔
757
        }
758

759
        // Count stacked-mode events (exclude FullCanvas)
760
        int stacked_event_count = 0;
59✔
761
        for (auto const & [ekey, edata]: _digital_event_series) {
73✔
762
            if (edata.display_options->is_visible && edata.display_options->display_mode == EventDisplayMode::Stacked) {
14✔
763
                stacked_event_count++;
13✔
764
            }
765
        }
766
        int const total_stackable_series = analog_visible_count + stacked_event_count;
59✔
767

768
        if (total_stackable_series > 0) {
59✔
769
            _plotting_manager->calculateGlobalStackedAllocation(analog_index, -1, total_stackable_series,
59✔
770
                                                                allocated_y_center, allocated_height);
771
        } else {
772
            // Fallback to analog-only allocation
UNCOV
773
            if (!_plotting_manager->getAnalogSeriesAllocationForKey(key, allocated_y_center, allocated_height)) {
×
UNCOV
774
                _plotting_manager->calculateAnalogSeriesAllocation(i, allocated_y_center, allocated_height);
×
775
            }
776
        }
777

778
        display_options->allocated_y_center = allocated_y_center;
59✔
779
        display_options->allocated_height = allocated_height;
59✔
780

781
        // Set the color for the current series
782
        hexToRGB(display_options->hex_color, r, g, b);
59✔
783
        float const rNorm = static_cast<float>(r) / 255.0f;
59✔
784
        float const gNorm = static_cast<float>(g) / 255.0f;
59✔
785
        float const bNorm = static_cast<float>(b) / 255.0f;
59✔
786

787
        m_vertices.clear();
59✔
788

789
        auto series_start_index = getTimeIndexForSeries(start_time, _master_time_frame.get(), time_frame.get());
59✔
790
        auto series_end_index = getTimeIndexForSeries(end_time, _master_time_frame.get(), time_frame.get());
59✔
791

792
        // === MVP MATRIX SETUP ===
793

794
        // Apply PlottingManager pan offset
795
        _plotting_manager->setPanOffset(_verticalPanOffset);
59✔
796

797
        auto Model = new_getAnalogModelMat(*display_options, display_options->cached_std_dev, display_options->cached_mean, *_plotting_manager);
59✔
798
        auto View = new_getAnalogViewMat(*_plotting_manager);
59✔
799
        auto Projection = new_getAnalogProjectionMat(start_time, end_time, _yMin, _yMax, *_plotting_manager);
59✔
800

801
        glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
59✔
802
        glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
59✔
803
        glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
59✔
804

805
        // Set color and alpha uniforms
806
        glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
59✔
807
        glUniform1f(m_alphaLoc, 1.0f);
59✔
808

809
        auto analog_range = series->getTimeValueSpanInTimeFrameIndexRange(series_start_index, series_end_index);
59✔
810

811
        if (analog_range.values.empty()) {
59✔
812
            // Instead of returning early (which stops rendering ALL series),
813
            // continue to the next series. This allows other series to still be rendered
814
            // even if this particular series has no data in the current visible range.
815
            i++;
×
816
            continue;
×
817
        }
818

819
        if (display_options->gap_handling == AnalogGapHandling::AlwaysConnect) {
59✔
820

821
            auto time_begin = analog_range.time_indices.begin();
×
822

UNCOV
823
            for (size_t i = 0; i < analog_range.values.size(); i++) {
×
UNCOV
824
                auto const xCanvasPos = time_frame->getTimeAtIndex(**time_begin);
×
825
                //auto const xCanvasPos = point.time_frame_index.getValue();
UNCOV
826
                auto const yCanvasPos = analog_range.values[i];
×
827

UNCOV
828
                m_vertices.push_back(xCanvasPos);
×
UNCOV
829
                m_vertices.push_back(yCanvasPos);
×
830
                m_vertices.push_back(0.0f);// z coordinate
×
UNCOV
831
                m_vertices.push_back(1.0f);// w coordinate
×
832

UNCOV
833
                ++(*time_begin);
×
834
            }
UNCOV
835
            m_vbo.bind();
×
UNCOV
836
            m_vbo.allocate(m_vertices.data(), static_cast<int>(m_vertices.size() * sizeof(GLfloat)));
×
UNCOV
837
            m_vbo.release();
×
838

839
            // Set line thickness from display options
UNCOV
840
            glLineWidth(static_cast<float>(display_options->line_thickness));
×
UNCOV
841
            glDrawArrays(GL_LINE_STRIP, 0, static_cast<int>(m_vertices.size() / 4));
×
842

843
        } else if (display_options->gap_handling == AnalogGapHandling::DetectGaps) {
59✔
844
            // Draw multiple line segments, breaking at gaps
845
            // Set line thickness before drawing segments
846
            glLineWidth(static_cast<float>(display_options->line_thickness));
59✔
847
            _drawAnalogSeriesWithGapDetection(data, time_frame, analog_range,
59✔
848
                                              display_options->gap_threshold);
59✔
849

UNCOV
850
        } else if (display_options->gap_handling == AnalogGapHandling::ShowMarkers) {
×
851
            // Draw individual markers instead of lines
UNCOV
852
            _drawAnalogSeriesAsMarkers(data, time_frame, analog_range);
×
853
        }
854

855

856
        i++;
59✔
857
    }
59✔
858

859
    // Reset line width to default
860
    glLineWidth(1.0f);
69✔
861
    glUseProgram(0);
69✔
862
}
69✔
863

864
void OpenGLWidget::_drawAnalogSeriesWithGapDetection(std::vector<float> const & data,
59✔
865
                                                     std::shared_ptr<TimeFrame> const & time_frame,
866
                                                     AnalogTimeSeries::TimeValueSpanPair analog_range,
867
                                                     float gap_threshold) {
868

869
    std::vector<GLfloat> segment_vertices;
59✔
870
    auto prev_index = 0;
59✔
871

872
    auto time_begin = analog_range.time_indices.begin();
59✔
873

874
    for (size_t i = 0; i < analog_range.values.size(); i++) {
7,816✔
875
        auto const xCanvasPos = time_frame->getTimeAtIndex(**time_begin);
7,757✔
876
        auto const yCanvasPos = analog_range.values[i];
7,757✔
877

878
        // Check for gap if this isn't the first point
879
        if (prev_index != 0) {
7,757✔
880

881
            float const time_gap = (**time_begin).getValue() - prev_index;
7,639✔
882

883
            if (time_gap > gap_threshold) {
7,639✔
884
                // Draw current segment if it has points
UNCOV
885
                if (segment_vertices.size() >= 4) {// At least 2 points (2 floats each)
×
UNCOV
886
                    m_vbo.bind();
×
UNCOV
887
                    m_vbo.allocate(segment_vertices.data(), static_cast<int>(segment_vertices.size() * sizeof(GLfloat)));
×
UNCOV
888
                    m_vbo.release();
×
UNCOV
889
                    glDrawArrays(GL_LINE_STRIP, 0, static_cast<int>(segment_vertices.size() / 4));
×
890
                }
891

892
                // Start new segment
UNCOV
893
                segment_vertices.clear();
×
894
            }
895
        }
896

897
        // Add current point to segment (4D coordinates: x, y, 0, 1)
898
        segment_vertices.push_back(xCanvasPos);
7,757✔
899
        segment_vertices.push_back(yCanvasPos);
7,757✔
900
        segment_vertices.push_back(0.0f);// z coordinate
7,757✔
901
        segment_vertices.push_back(1.0f);// w coordinate
7,757✔
902

903
        prev_index = (**time_begin).getValue();
7,757✔
904
        ++(*time_begin);
7,757✔
905
    }
906

907
    // Draw final segment
908
    if (segment_vertices.size() >= 4) {// At least 1 point (4 floats)
59✔
909
        m_vbo.bind();
59✔
910
        m_vbo.allocate(segment_vertices.data(), static_cast<int>(segment_vertices.size() * sizeof(GLfloat)));
59✔
911
        m_vbo.release();
59✔
912

913
        if (segment_vertices.size() >= 8) {// At least 2 points (8 floats)
59✔
914
            glDrawArrays(GL_LINE_STRIP, 0, static_cast<int>(segment_vertices.size() / 4));
59✔
915
        } else {
916
            // Single point - draw as a small marker
917
            glDrawArrays(GL_POINTS, 0, static_cast<int>(segment_vertices.size() / 4));
×
918
        }
919
    }
920
}
118✔
921

922
void OpenGLWidget::_drawAnalogSeriesAsMarkers(std::vector<float> const & data,
×
923
                                              std::shared_ptr<TimeFrame> const & time_frame,
924
                                              AnalogTimeSeries::TimeValueSpanPair analog_range) {
925
    m_vertices.clear();
×
926

UNCOV
927
    auto time_begin = analog_range.time_indices.begin();
×
928

929
    for (size_t i = 0; i < analog_range.values.size(); i++) {
×
930

UNCOV
931
        auto const xCanvasPos = time_frame->getTimeAtIndex(**time_begin);
×
932
        auto const yCanvasPos = analog_range.values[i];
×
933

UNCOV
934
        m_vertices.push_back(xCanvasPos);
×
UNCOV
935
        m_vertices.push_back(yCanvasPos);
×
UNCOV
936
        m_vertices.push_back(0.0f);// z coordinate
×
UNCOV
937
        m_vertices.push_back(1.0f);// w coordinate
×
938

UNCOV
939
        ++(*time_begin);
×
940
    }
941

UNCOV
942
    if (!m_vertices.empty()) {
×
UNCOV
943
        m_vbo.bind();
×
UNCOV
944
        m_vbo.allocate(m_vertices.data(), static_cast<int>(m_vertices.size() * sizeof(GLfloat)));
×
UNCOV
945
        m_vbo.release();
×
946

947
        // Set point size for better visibility
948
        //glPointSize(3.0f);
UNCOV
949
        glDrawArrays(GL_POINTS, 0, static_cast<int>(m_vertices.size() / 4));
×
950
        //glPointSize(1.0f); // Reset to default
951
    }
UNCOV
952
}
×
953

954
///////////////////////////////////////////////////////////////////////////////
955

956
void OpenGLWidget::paintGL() {
69✔
957
    int r, g, b;
69✔
958
    hexToRGB(m_background_color, r, g, b);
69✔
959
    glClearColor(
69✔
960
            static_cast<float>(r) / 255.0f,
69✔
961
            static_cast<float>(g) / 255.0f,
69✔
962
            static_cast<float>(b) / 255.0f, 1.0f);
69✔
963
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
69✔
964

965
    //This has been converted to master coordinates
966
    int const currentTime = _time;
69✔
967

968
    int64_t const zoom = _xAxis.getEnd() - _xAxis.getStart();
69✔
969
    _xAxis.setCenterAndZoom(currentTime, zoom);
69✔
970

971
    // Update Y boundaries based on pan and zoom
972
    _updateYViewBoundaries();
69✔
973

974
    // Draw the series
975
    drawDigitalEventSeries();
69✔
976
    drawDigitalIntervalSeries();
69✔
977
    drawAnalogSeries();
69✔
978

979
    drawAxis();
69✔
980

981
    drawGridLines();
69✔
982

983
    drawDraggedInterval();
69✔
984
    drawNewIntervalBeingCreated();
69✔
985
}
69✔
986

987
void OpenGLWidget::resizeGL(int w, int h) {
18✔
988

989
    // Set the viewport to match the widget dimensions
990
    // This is crucial for proper scaling - it tells OpenGL the actual pixel dimensions
991
    glViewport(0, 0, w, h);
18✔
992

993
    // Store the new dimensions
994
    // Note: width() and height() will return the new values after this call
995

996
    // For 2D plotting, we should use orthographic projection
997
    m_proj.setToIdentity();
18✔
998
    m_proj.ortho(-1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f);// Use orthographic projection for 2D plotting
18✔
999

1000
    m_view.setToIdentity();
18✔
1001
    m_view.translate(0, 0, -1);// Move slightly back for orthographic view
18✔
1002

1003
    // Trigger a repaint with the new dimensions
1004
    update();
18✔
1005
}
18✔
1006

1007
void OpenGLWidget::drawAxis() {
69✔
1008

1009
    auto axesProgram = ShaderManager::instance().getProgram("axes");
207✔
1010
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
69✔
1011

1012
    glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, m_proj.constData());
69✔
1013
    glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, m_view.constData());
69✔
1014
    glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, m_model.constData());
69✔
1015

1016
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
69✔
1017
    setupVertexAttribs();
69✔
1018

1019
    // Get axis color from theme and set uniforms
1020
    float r, g, b;
69✔
1021
    hexToRGB(m_axis_color, r, g, b);
69✔
1022
    glUniform3f(m_colorLoc, r, g, b);
69✔
1023
    glUniform1f(m_alphaLoc, 1.0f);
69✔
1024

1025
    // Draw horizontal line at x=0 with 4D coordinates (x, y, 0, 1)
1026
    std::array<GLfloat, 8> lineVertices = {
69✔
1027
            0.0f, _yMin, 0.0f, 1.0f,
69✔
1028
            0.0f, _yMax, 0.0f, 1.0f};
69✔
1029

1030
    m_vbo.bind();
69✔
1031
    m_vbo.allocate(lineVertices.data(), lineVertices.size() * sizeof(GLfloat));
207✔
1032
    m_vbo.release();
69✔
1033
    glDrawArrays(GL_LINES, 0, 2);
69✔
1034
    glUseProgram(0);
69✔
1035
}
138✔
1036

1037
void OpenGLWidget::addAnalogTimeSeries(
22✔
1038
        std::string const & key,
1039
        std::shared_ptr<AnalogTimeSeries> series,
1040
        std::shared_ptr<TimeFrame> time_frame,
1041
        std::string const & color) {
1042

1043
    auto display_options = std::make_unique<NewAnalogTimeSeriesDisplayOptions>();
22✔
1044

1045
    // Set color
1046
    display_options->hex_color = color.empty() ? TimeSeriesDefaultValues::getColorForIndex(_analog_series.size()) : color;
22✔
1047
    display_options->is_visible = true;
22✔
1048

1049
    // Calculate scale factor based on standard deviation
1050
    auto start_time = std::chrono::high_resolution_clock::now();
22✔
1051
    setAnalogIntrinsicProperties(series.get(), *display_options);
22✔
1052
    auto end_time = std::chrono::high_resolution_clock::now();
22✔
1053
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
22✔
1054
    std::cout << "Standard deviation calculation took " << duration.count() << " milliseconds" << std::endl;
22✔
1055
    display_options->scale_factor = display_options->cached_std_dev * 5.0f;
22✔
1056
    display_options->user_scale_factor = 1.0f;// Default user scale
22✔
1057

1058
    if (time_frame->getTotalFrameCount() / 5 > series->getNumSamples()) {
22✔
1059
        display_options->gap_handling = AnalogGapHandling::AlwaysConnect;
×
UNCOV
1060
        display_options->enable_gap_detection = false;
×
1061

1062
    } else {
1063
        display_options->enable_gap_detection = true;
22✔
1064
        display_options->gap_handling = AnalogGapHandling::DetectGaps;
22✔
1065
        display_options->gap_threshold = time_frame->getTotalFrameCount() / 1000;
22✔
1066
    }
1067

1068
    _analog_series[key] = AnalogSeriesData{
110✔
1069
            std::move(series),
22✔
1070
            std::move(time_frame),
22✔
1071
            std::move(display_options)};
44✔
1072

1073
    updateCanvas(_time);
22✔
1074
}
44✔
1075

UNCOV
1076
void OpenGLWidget::removeAnalogTimeSeries(std::string const & key) {
×
UNCOV
1077
    auto item = _analog_series.find(key);
×
UNCOV
1078
    if (item != _analog_series.end()) {
×
UNCOV
1079
        _analog_series.erase(item);
×
1080
    }
UNCOV
1081
    updateCanvas(_time);
×
UNCOV
1082
}
×
1083

1084
void OpenGLWidget::addDigitalEventSeries(
11✔
1085
        std::string const & key,
1086
        std::shared_ptr<DigitalEventSeries> series,
1087
        std::shared_ptr<TimeFrame> time_frame,
1088
        std::string const & color) {
1089

1090
    auto display_options = std::make_unique<NewDigitalEventSeriesDisplayOptions>();
11✔
1091

1092
    // Set color
1093
    display_options->hex_color = color.empty() ? TimeSeriesDefaultValues::getColorForIndex(_digital_event_series.size()) : color;
11✔
1094
    display_options->is_visible = true;
11✔
1095

1096
    _digital_event_series[key] = DigitalEventSeriesData{
55✔
1097
            std::move(series),
11✔
1098
            std::move(time_frame),
11✔
1099
            std::move(display_options)};
22✔
1100

1101
    updateCanvas(_time);
11✔
1102
}
22✔
1103

1104
void OpenGLWidget::removeDigitalEventSeries(std::string const & key) {
×
1105
    auto item = _digital_event_series.find(key);
×
1106
    if (item != _digital_event_series.end()) {
×
1107
        _digital_event_series.erase(item);
×
1108
    }
1109
    updateCanvas(_time);
×
1110
}
×
1111

1112
void OpenGLWidget::addDigitalIntervalSeries(
×
1113
        std::string const & key,
1114
        std::shared_ptr<DigitalIntervalSeries> series,
1115
        std::shared_ptr<TimeFrame> time_frame,
1116
        std::string const & color) {
1117

1118
    auto display_options = std::make_unique<NewDigitalIntervalSeriesDisplayOptions>();
×
1119

1120
    // Set color
UNCOV
1121
    display_options->hex_color = color.empty() ? TimeSeriesDefaultValues::getColorForIndex(_digital_interval_series.size()) : color;
×
UNCOV
1122
    display_options->is_visible = true;
×
1123

UNCOV
1124
    _digital_interval_series[key] = DigitalIntervalSeriesData{
×
1125
            std::move(series),
×
1126
            std::move(time_frame),
×
1127
            std::move(display_options)};
×
1128

UNCOV
1129
    updateCanvas(_time);
×
1130
}
×
1131

1132
void OpenGLWidget::removeDigitalIntervalSeries(std::string const & key) {
×
1133
    auto item = _digital_interval_series.find(key);
×
1134
    if (item != _digital_interval_series.end()) {
×
1135
        _digital_interval_series.erase(item);
×
1136
    }
1137
    updateCanvas(_time);
×
UNCOV
1138
}
×
1139

1140
void OpenGLWidget::_addSeries(std::string const & key) {
×
1141
    static_cast<void>(key);
1142
    //auto item = _series_y_position.find(key);
1143
}
×
1144

1145
void OpenGLWidget::_removeSeries(std::string const & key) {
×
1146
    auto item = _series_y_position.find(key);
×
UNCOV
1147
    if (item != _series_y_position.end()) {
×
1148
        _series_y_position.erase(item);
×
1149
    }
UNCOV
1150
}
×
1151

1152
void OpenGLWidget::clearSeries() {
×
1153
    _analog_series.clear();
×
UNCOV
1154
    updateCanvas(_time);
×
1155
}
×
1156

UNCOV
1157
void OpenGLWidget::drawDashedLine(LineParameters const & params) {
×
1158

1159
    auto dashedProgram = ShaderManager::instance().getProgram("dashed_line");
×
UNCOV
1160
    if (dashedProgram) glUseProgram(dashedProgram->getProgramId());
×
1161

UNCOV
1162
    glUniformMatrix4fv(m_dashedProjMatrixLoc, 1, GL_FALSE, (m_proj * m_view * m_model).constData());
×
1163
    std::array<GLfloat, 2> hw = {static_cast<float>(width()), static_cast<float>(height())};
×
UNCOV
1164
    glUniform2fv(m_dashedResolutionLoc, 1, hw.data());
×
1165
    glUniform1f(m_dashedDashSizeLoc, params.dashLength);
×
1166
    glUniform1f(m_dashedGapSizeLoc, params.gapLength);
×
1167

UNCOV
1168
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
×
1169

1170
    // Pass 3D coordinates (z=0) to match the vertex shader input format
UNCOV
1171
    std::array<float, 6> vertices = {
×
UNCOV
1172
            params.xStart, params.yStart, 0.0f,
×
1173
            params.xEnd, params.yEnd, 0.0f};
×
1174

UNCOV
1175
    m_vbo.bind();
×
UNCOV
1176
    m_vbo.allocate(vertices.data(), vertices.size() * sizeof(float));
×
1177

UNCOV
1178
    glEnableVertexAttribArray(0);
×
UNCOV
1179
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), nullptr);
×
1180

1181
    glDrawArrays(GL_LINES, 0, 2);
×
1182

UNCOV
1183
    m_vbo.release();
×
1184

UNCOV
1185
    glUseProgram(0);
×
1186
}
×
1187

1188
void OpenGLWidget::drawGridLines() {
69✔
1189
    if (!_grid_lines_enabled) {
69✔
1190
        return;// Grid lines are disabled
69✔
1191
    }
1192

UNCOV
1193
    auto const start_time = _xAxis.getStart();
×
1194
    auto const end_time = _xAxis.getEnd();
×
1195

1196
    // Calculate the range of time values
1197
    auto const time_range = end_time - start_time;
×
1198

1199
    // Avoid drawing grid lines if the range is too small or invalid
UNCOV
1200
    if (time_range <= 0 || _grid_spacing <= 0) {
×
1201
        return;
×
1202
    }
1203

1204
    // Find the first grid line position that's >= start_time
1205
    // Use integer division to ensure proper alignment
1206
    int64_t first_grid_time = ((start_time / _grid_spacing) * _grid_spacing);
×
1207
    if (first_grid_time < start_time) {
×
UNCOV
1208
        first_grid_time += _grid_spacing;
×
1209
    }
1210

1211
    // Draw vertical grid lines at regular intervals
UNCOV
1212
    for (int64_t grid_time = first_grid_time; grid_time <= end_time; grid_time += _grid_spacing) {
×
1213
        // Convert time coordinate to normalized device coordinate (-1 to 1)
UNCOV
1214
        float const normalized_x = 2.0f * static_cast<float>(grid_time - start_time) / static_cast<float>(time_range) - 1.0f;
×
1215

1216
        // Skip grid lines that are outside the visible range due to floating point precision
UNCOV
1217
        if (normalized_x < -1.0f || normalized_x > 1.0f) {
×
UNCOV
1218
            continue;
×
1219
        }
1220

UNCOV
1221
        LineParameters gridLine;
×
UNCOV
1222
        gridLine.xStart = normalized_x;
×
UNCOV
1223
        gridLine.xEnd = normalized_x;
×
UNCOV
1224
        gridLine.yStart = _yMin;
×
UNCOV
1225
        gridLine.yEnd = _yMax;
×
1226
        gridLine.dashLength = 3.0f;// Shorter dashes for grid lines
×
UNCOV
1227
        gridLine.gapLength = 3.0f; // Shorter gaps for grid lines
×
1228

1229
        drawDashedLine(gridLine);
×
1230
    }
1231
}
1232

1233
void OpenGLWidget::_updateYViewBoundaries() {
69✔
1234
    /*
1235
    float viewHeight = 2.0f;
1236

1237
    // Calculate center point (adjusted by vertical pan)
1238
    float centerY = _verticalPanOffset;
1239
    
1240
    // Calculate min and max values
1241
    _yMin = centerY - (viewHeight / 2.0f);
1242
    _yMax = centerY + (viewHeight / 2.0f);
1243
     */
1244
}
69✔
1245

1246
float OpenGLWidget::canvasXToTime(float canvas_x) const {
×
1247
    // Convert canvas pixel coordinate to time coordinate
UNCOV
1248
    float const canvas_width = static_cast<float>(width());
×
UNCOV
1249
    float const normalized_x = canvas_x / canvas_width;// 0.0 to 1.0
×
1250

1251
    auto const start_time = static_cast<float>(_xAxis.getStart());
×
1252
    auto const end_time = static_cast<float>(_xAxis.getEnd());
×
1253

UNCOV
1254
    return start_time + normalized_x * (end_time - start_time);
×
1255
}
1256

UNCOV
1257
float OpenGLWidget::canvasYToAnalogValue(float canvas_y, std::string const & series_key) const {
×
1258
    // Get canvas dimensions
UNCOV
1259
    auto [canvas_width, canvas_height] = getCanvasSize();
×
1260

1261
    // Convert canvas Y to normalized coordinates [0, 1] where 0 is bottom, 1 is top
UNCOV
1262
    float const normalized_y = 1.0f - (canvas_y / static_cast<float>(canvas_height));
×
1263

1264
    // Convert to view coordinates using current viewport bounds (accounting for pan offset)
1265
    float const dynamic_min_y = _yMin + _verticalPanOffset;
×
UNCOV
1266
    float const dynamic_max_y = _yMax + _verticalPanOffset;
×
UNCOV
1267
    float const view_y = dynamic_min_y + normalized_y * (dynamic_max_y - dynamic_min_y);
×
1268

1269
    // Find the series configuration to get positioning info
1270
    auto const analog_it = _analog_series.find(series_key);
×
1271
    if (analog_it == _analog_series.end()) {
×
UNCOV
1272
        return 0.0f;// Series not found
×
1273
    }
1274

1275
    auto const & display_options = analog_it->second.display_options;
×
1276

1277
    // Check if this series uses VerticalSpaceManager positioning
UNCOV
1278
    if (display_options->y_offset != 0.0f && display_options->allocated_height > 0.0f) {
×
1279
        // VerticalSpaceManager mode: series is positioned at y_offset with its own scaling
1280
        float const series_local_y = view_y - display_options->y_offset;
×
1281

1282
        // Apply inverse of the scaling used in rendering to get actual analog value
1283
        auto const series = analog_it->second.series;
×
UNCOV
1284
        auto const stdDev = getCachedStdDev(*series, *display_options);
×
1285
        auto const user_scale_combined = display_options->user_scale_factor * _global_zoom;
×
1286

1287
        // Calculate the same scaling factors used in rendering
UNCOV
1288
        float const usable_height = display_options->allocated_height * 0.8f;
×
1289
        float const base_amplitude_scale = 1.0f / stdDev;
×
UNCOV
1290
        float const height_scale = usable_height / 1.0f;
×
UNCOV
1291
        float const amplitude_scale = base_amplitude_scale * height_scale * user_scale_combined;
×
1292

1293
        // Apply inverse scaling to get actual data value
1294
        return series_local_y / amplitude_scale;
×
1295
    } else {
×
1296
        // Legacy mode: use corrected calculation
1297
        float const adjusted_y = view_y;// No pan offset adjustment needed since it's in projection
×
1298

1299
        // Calculate series center and scaling for legacy mode
UNCOV
1300
        auto series_pos_it = _series_y_position.find(series_key);
×
UNCOV
1301
        if (series_pos_it == _series_y_position.end()) {
×
1302
            // Series not found in position map, return 0 as fallback
1303
            return 0.0f;
×
1304
        }
1305
        int const series_index = series_pos_it->second;
×
UNCOV
1306
        float const center_coord = -0.5f * _ySpacing * (static_cast<float>(_analog_series.size() - 1));
×
1307
        float const series_y_offset = static_cast<float>(series_index) * _ySpacing + center_coord;
×
1308

1309
        float const relative_y = adjusted_y - series_y_offset;
×
1310

1311
        // Use corrected scaling calculation
UNCOV
1312
        auto const series = analog_it->second.series;
×
1313
        auto const stdDev = getCachedStdDev(*series, *display_options);
×
UNCOV
1314
        auto const user_scale_combined = display_options->user_scale_factor * _global_zoom;
×
1315
        float const legacy_amplitude_scale = (1.0f / stdDev) * user_scale_combined;
×
1316

1317
        return relative_y / legacy_amplitude_scale;
×
1318
    }
×
1319
}
1320

1321
// Interval selection methods
UNCOV
1322
void OpenGLWidget::setSelectedInterval(std::string const & series_key, int64_t start_time, int64_t end_time) {
×
1323
    _selected_intervals[series_key] = std::make_pair(start_time, end_time);
×
1324
    updateCanvas(_time);
×
1325
}
×
1326

UNCOV
1327
void OpenGLWidget::clearSelectedInterval(std::string const & series_key) {
×
UNCOV
1328
    auto it = _selected_intervals.find(series_key);
×
1329
    if (it != _selected_intervals.end()) {
×
1330
        _selected_intervals.erase(it);
×
1331
        updateCanvas(_time);
×
1332
    }
UNCOV
1333
}
×
1334

1335
std::optional<std::pair<int64_t, int64_t>> OpenGLWidget::getSelectedInterval(std::string const & series_key) const {
×
UNCOV
1336
    auto it = _selected_intervals.find(series_key);
×
1337
    if (it != _selected_intervals.end()) {
×
UNCOV
1338
        return it->second;
×
1339
    }
1340
    return std::nullopt;
×
1341
}
1342

UNCOV
1343
std::optional<std::pair<int64_t, int64_t>> OpenGLWidget::findIntervalAtTime(std::string const & series_key, float time_coord) const {
×
1344
    auto it = _digital_interval_series.find(series_key);
×
UNCOV
1345
    if (it == _digital_interval_series.end()) {
×
1346
        return std::nullopt;
×
1347
    }
1348

1349
    auto const & interval_data = it->second;
×
UNCOV
1350
    auto const & series = interval_data.series;
×
1351
    auto const & time_frame = interval_data.time_frame;
×
1352

1353
    // Convert time coordinate from master time frame to series time frame
1354
    int64_t query_time_index;
UNCOV
1355
    if (time_frame.get() == _master_time_frame.get()) {
×
1356
        // Same time frame - use time coordinate directly
1357
        query_time_index = static_cast<int64_t>(std::round(time_coord));
×
1358
    } else {
1359
        // Different time frame - convert master time to series time frame index
UNCOV
1360
        query_time_index = time_frame->getIndexAtTime(time_coord).getValue();
×
1361
    }
1362

1363
    // Find all intervals that contain this time point in the series' time frame
1364
    auto intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(query_time_index, query_time_index);
×
1365

UNCOV
1366
    if (!intervals.empty()) {
×
1367
        // Return the first interval found, converted to master time frame coordinates
1368
        auto const & interval = intervals.front();
×
UNCOV
1369
        int64_t interval_start_master, interval_end_master;
×
1370

UNCOV
1371
        if (time_frame.get() == _master_time_frame.get()) {
×
1372
            // Same time frame - use indices directly as time coordinates
1373
            interval_start_master = interval.start;
×
UNCOV
1374
            interval_end_master = interval.end;
×
1375
        } else {
1376
            // Convert series indices to master time frame coordinates
1377
            interval_start_master = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.start)));
×
UNCOV
1378
            interval_end_master = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.end)));
×
1379
        }
1380

1381
        return std::make_pair(interval_start_master, interval_end_master);
×
1382
    }
1383

1384
    return std::nullopt;
×
1385
}
1386

1387
// Interval edge dragging methods
1388
std::optional<std::pair<std::string, bool>> OpenGLWidget::findIntervalEdgeAtPosition(float canvas_x, float canvas_y) const {
×
1389

1390
    static_cast<void>(canvas_y);
1391

UNCOV
1392
    float const time_coord = canvasXToTime(canvas_x);
×
UNCOV
1393
    constexpr float EDGE_TOLERANCE_PX = 10.0f;
×
1394

1395
    // Only check selected intervals
UNCOV
1396
    for (auto const & [series_key, interval_bounds]: _selected_intervals) {
×
UNCOV
1397
        auto const [start_time, end_time] = interval_bounds;
×
1398

1399
        // Convert interval bounds to canvas coordinates
1400
        auto const start_time_f = static_cast<float>(start_time);
×
UNCOV
1401
        auto const end_time_f = static_cast<float>(end_time);
×
1402

1403
        // Check if we're within the interval's time range (with some tolerance)
UNCOV
1404
        if (time_coord >= start_time_f - EDGE_TOLERANCE_PX && time_coord <= end_time_f + EDGE_TOLERANCE_PX) {
×
1405
            // Convert time coordinates to canvas X positions for pixel-based tolerance
UNCOV
1406
            float const canvas_width = static_cast<float>(width());
×
UNCOV
1407
            auto const start_time_canvas = static_cast<float>(_xAxis.getStart());
×
1408
            auto const end_time_canvas = static_cast<float>(_xAxis.getEnd());
×
1409

1410
            float const start_canvas_x = (start_time_f - start_time_canvas) / (end_time_canvas - start_time_canvas) * canvas_width;
×
1411
            float const end_canvas_x = (end_time_f - start_time_canvas) / (end_time_canvas - start_time_canvas) * canvas_width;
×
1412

1413
            // Check if we're close to the left edge
1414
            if (std::abs(canvas_x - start_canvas_x) <= EDGE_TOLERANCE_PX) {
×
UNCOV
1415
                return std::make_pair(series_key, true);// true = left edge
×
1416
            }
1417

1418
            // Check if we're close to the right edge
1419
            if (std::abs(canvas_x - end_canvas_x) <= EDGE_TOLERANCE_PX) {
×
1420
                return std::make_pair(series_key, false);// false = right edge
×
1421
            }
1422
        }
1423
    }
1424

UNCOV
1425
    return std::nullopt;
×
1426
}
1427

UNCOV
1428
void OpenGLWidget::startIntervalDrag(std::string const & series_key, bool is_left_edge, QPoint const & start_pos) {
×
1429
    auto selected_interval = getSelectedInterval(series_key);
×
UNCOV
1430
    if (!selected_interval.has_value()) {
×
UNCOV
1431
        return;
×
1432
    }
1433

1434
    auto const [start_time, end_time] = selected_interval.value();
×
1435

UNCOV
1436
    _is_dragging_interval = true;
×
UNCOV
1437
    _dragging_series_key = series_key;
×
1438
    _dragging_left_edge = is_left_edge;
×
UNCOV
1439
    _original_start_time = start_time;
×
UNCOV
1440
    _original_end_time = end_time;
×
1441
    _dragged_start_time = start_time;
×
1442
    _dragged_end_time = end_time;
×
UNCOV
1443
    _drag_start_pos = start_pos;
×
1444

1445
    // Disable normal mouse interactions during drag
UNCOV
1446
    setCursor(Qt::SizeHorCursor);
×
1447

1448
    std::cout << "Started dragging " << (is_left_edge ? "left" : "right")
1449
              << " edge of interval [" << start_time << ", " << end_time << "]" << std::endl;
×
1450
}
1451

1452
void OpenGLWidget::updateIntervalDrag(QPoint const & current_pos) {
×
1453
    if (!_is_dragging_interval) {
×
UNCOV
1454
        return;
×
1455
    }
1456

1457
    // Convert mouse position to time coordinate (in master time frame)
1458
    float const current_time_master = canvasXToTime(static_cast<float>(current_pos.x()));
×
1459

1460
    // Get the series data
UNCOV
1461
    auto it = _digital_interval_series.find(_dragging_series_key);
×
UNCOV
1462
    if (it == _digital_interval_series.end()) {
×
1463
        // Series not found - abort drag
UNCOV
1464
        cancelIntervalDrag();
×
1465
        return;
×
1466
    }
1467

UNCOV
1468
    auto const & series = it->second.series;
×
1469
    auto const & time_frame = it->second.time_frame;
×
1470

1471
    // Convert master time coordinate to series time frame index
UNCOV
1472
    int64_t current_time_series_index;
×
UNCOV
1473
    if (time_frame.get() == _master_time_frame.get()) {
×
1474
        // Same time frame - use time coordinate directly
UNCOV
1475
        current_time_series_index = static_cast<int64_t>(std::round(current_time_master));
×
1476
    } else {
1477
        // Different time frame - convert master time to series time frame index
1478
        current_time_series_index = time_frame->getIndexAtTime(current_time_master).getValue();
×
1479
    }
1480

1481
    // Convert original interval bounds to series time frame for constraints
1482
    int64_t original_start_series, original_end_series;
1483
    if (time_frame.get() == _master_time_frame.get()) {
×
1484
        // Same time frame
1485
        original_start_series = _original_start_time;
×
UNCOV
1486
        original_end_series = _original_end_time;
×
1487
    } else {
1488
        // Convert master time coordinates to series time frame indices
UNCOV
1489
        original_start_series = time_frame->getIndexAtTime(static_cast<float>(_original_start_time)).getValue();
×
UNCOV
1490
        original_end_series = time_frame->getIndexAtTime(static_cast<float>(_original_end_time)).getValue();
×
1491
    }
1492

1493
    // Perform dragging logic in series time frame
1494
    int64_t new_start_series, new_end_series;
1495

UNCOV
1496
    if (_dragging_left_edge) {
×
1497
        // Dragging left edge - constrain to not pass right edge
UNCOV
1498
        new_start_series = std::min(current_time_series_index, original_end_series - 1);
×
1499
        new_end_series = original_end_series;
×
1500

1501
        // Check for collision with other intervals in series time frame
UNCOV
1502
        auto overlapping_intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(
×
1503
                new_start_series, new_start_series);
×
1504

UNCOV
1505
        for (auto const & interval: overlapping_intervals) {
×
1506
            // Skip the interval we're currently editing
UNCOV
1507
            if (interval.start == original_start_series && interval.end == original_end_series) {
×
1508
                continue;
×
1509
            }
1510

1511
            // If we would overlap with another interval, stop 1 index after it
UNCOV
1512
            if (new_start_series <= interval.end) {
×
1513
                new_start_series = interval.end + 1;
×
1514
                break;
×
1515
            }
1516
        }
1517
    } else {
1518
        // Dragging right edge - constrain to not pass left edge
UNCOV
1519
        new_start_series = original_start_series;
×
UNCOV
1520
        new_end_series = std::max(current_time_series_index, original_start_series + 1);
×
1521

1522
        // Check for collision with other intervals in series time frame
1523
        auto overlapping_intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(
×
UNCOV
1524
                new_end_series, new_end_series);
×
1525

UNCOV
1526
        for (auto const & interval: overlapping_intervals) {
×
1527
            // Skip the interval we're currently editing
UNCOV
1528
            if (interval.start == original_start_series && interval.end == original_end_series) {
×
1529
                continue;
×
1530
            }
1531

1532
            // If we would overlap with another interval, stop 1 index before it
UNCOV
1533
            if (new_end_series >= interval.start) {
×
1534
                new_end_series = interval.start - 1;
×
1535
                break;
×
1536
            }
1537
        }
1538
    }
1539

1540
    // Validate the new interval bounds
UNCOV
1541
    if (new_start_series >= new_end_series) {
×
1542
        // Invalid bounds - keep current drag state
UNCOV
1543
        return;
×
1544
    }
1545

1546
    // Convert back to master time frame for display
1547
    if (time_frame.get() == _master_time_frame.get()) {
×
1548
        // Same time frame
1549
        _dragged_start_time = new_start_series;
×
UNCOV
1550
        _dragged_end_time = new_end_series;
×
1551
    } else {
1552
        // Convert series indices back to master time coordinates
1553
        try {
1554
            _dragged_start_time = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(new_start_series)));
×
UNCOV
1555
            _dragged_end_time = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(new_end_series)));
×
1556
        } catch (...) {
×
1557
            // Conversion failed - abort drag
UNCOV
1558
            cancelIntervalDrag();
×
UNCOV
1559
            return;
×
1560
        }
×
1561
    }
1562

1563
    // Trigger redraw to show the dragged interval
UNCOV
1564
    updateCanvas(_time);
×
1565
}
1566

1567
void OpenGLWidget::finishIntervalDrag() {
×
UNCOV
1568
    if (!_is_dragging_interval) {
×
1569
        return;
×
1570
    }
1571

1572
    // Get the series data
UNCOV
1573
    auto it = _digital_interval_series.find(_dragging_series_key);
×
UNCOV
1574
    if (it == _digital_interval_series.end()) {
×
1575
        // Series not found - abort drag
1576
        cancelIntervalDrag();
×
1577
        return;
×
1578
    }
1579

UNCOV
1580
    auto const & series = it->second.series;
×
UNCOV
1581
    auto const & time_frame = it->second.time_frame;
×
1582

1583
    try {
1584
        // Convert all coordinates to series time frame for data operations
1585
        int64_t original_start_series, original_end_series, new_start_series, new_end_series;
1586

UNCOV
1587
        if (time_frame.get() == _master_time_frame.get()) {
×
1588
            // Same time frame - use coordinates directly
1589
            original_start_series = _original_start_time;
×
1590
            original_end_series = _original_end_time;
×
UNCOV
1591
            new_start_series = _dragged_start_time;
×
UNCOV
1592
            new_end_series = _dragged_end_time;
×
1593
        } else {
1594
            // Convert master time coordinates to series time frame indices
UNCOV
1595
            original_start_series = time_frame->getIndexAtTime(static_cast<float>(_original_start_time)).getValue();
×
UNCOV
1596
            original_end_series = time_frame->getIndexAtTime(static_cast<float>(_original_end_time)).getValue();
×
1597
            new_start_series = time_frame->getIndexAtTime(static_cast<float>(_dragged_start_time)).getValue();
×
UNCOV
1598
            new_end_series = time_frame->getIndexAtTime(static_cast<float>(_dragged_end_time)).getValue();
×
1599
        }
1600

1601
        // Validate converted coordinates
UNCOV
1602
        if (new_start_series >= new_end_series ||
×
1603
            new_start_series < 0 || new_end_series < 0) {
×
UNCOV
1604
            throw std::runtime_error("Invalid interval bounds after conversion");
×
1605
        }
1606

1607
        // Update the interval data in the series' native time frame
1608
        // First, remove the original interval completely
UNCOV
1609
        for (int64_t time = original_start_series; time <= original_end_series; ++time) {
×
UNCOV
1610
            series->setEventAtTime(TimeFrameIndex(time), false);
×
1611
        }
1612

1613
        // Add the new interval
UNCOV
1614
        series->addEvent(TimeFrameIndex(new_start_series), TimeFrameIndex(new_end_series));
×
1615

1616
        // Update the selection to the new interval (stored in master time frame coordinates)
UNCOV
1617
        setSelectedInterval(_dragging_series_key, _dragged_start_time, _dragged_end_time);
×
1618

1619
        std::cout << "Finished dragging interval. Original: ["
×
1620
                  << _original_start_time << ", " << _original_end_time
×
1621
                  << "] -> New: [" << _dragged_start_time << ", " << _dragged_end_time << "]" << std::endl;
×
1622

UNCOV
1623
    } catch (...) {
×
1624
        // Error occurred during conversion or data update - abort drag
UNCOV
1625
        std::cout << "Error during interval drag completion - keeping original interval" << std::endl;
×
UNCOV
1626
        cancelIntervalDrag();
×
1627
        return;
×
1628
    }
×
1629

1630
    // Reset drag state
UNCOV
1631
    _is_dragging_interval = false;
×
1632
    _dragging_series_key.clear();
×
UNCOV
1633
    setCursor(Qt::ArrowCursor);
×
1634

1635
    // Trigger final redraw
UNCOV
1636
    updateCanvas(_time);
×
1637
}
1638

UNCOV
1639
void OpenGLWidget::cancelIntervalDrag() {
×
UNCOV
1640
    if (!_is_dragging_interval) {
×
1641
        return;
×
1642
    }
1643

UNCOV
1644
    std::cout << "Cancelled interval drag" << std::endl;
×
1645

1646
    // Reset drag state without applying changes
UNCOV
1647
    _is_dragging_interval = false;
×
1648
    _dragging_series_key.clear();
×
1649
    setCursor(Qt::ArrowCursor);
×
1650

1651
    // Trigger redraw to remove the dragged interval visualization
UNCOV
1652
    updateCanvas(_time);
×
1653
}
1654

1655
void OpenGLWidget::drawDraggedInterval() {
69✔
1656
    if (!_is_dragging_interval) {
69✔
1657
        return;
69✔
1658
    }
1659

1660
    // Get the series data for rendering
1661
    auto it = _digital_interval_series.find(_dragging_series_key);
×
1662
    if (it == _digital_interval_series.end()) {
×
UNCOV
1663
        return;
×
1664
    }
1665

1666
    auto const & display_options = it->second.display_options;
×
1667

UNCOV
1668
    auto const start_time = static_cast<float>(_xAxis.getStart());
×
1669
    auto const end_time = static_cast<float>(_xAxis.getEnd());
×
1670
    auto const min_y = _yMin;
×
1671
    auto const max_y = _yMax;
×
1672

1673
    // Check if the dragged interval is visible
1674
    if (_dragged_end_time < static_cast<int64_t>(start_time) || _dragged_start_time > static_cast<int64_t>(end_time)) {
×
1675
        return;
×
1676
    }
1677

1678
    auto axesProgram = ShaderManager::instance().getProgram("axes");
×
UNCOV
1679
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
×
1680

1681
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
×
1682
    setupVertexAttribs();
×
1683

1684
    // Set up matrices (same as normal interval rendering)
1685
    auto Model = glm::mat4(1.0f);
×
1686
    auto View = glm::mat4(1.0f);
×
UNCOV
1687
    auto Projection = glm::ortho(start_time, end_time, min_y, max_y);
×
1688

1689
    glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
×
1690
    glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
×
UNCOV
1691
    glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
×
1692

1693
    // Get colors
UNCOV
1694
    int r, g, b;
×
UNCOV
1695
    hexToRGB(display_options->hex_color, r, g, b);
×
UNCOV
1696
    float const rNorm = static_cast<float>(r) / 255.0f;
×
1697
    float const gNorm = static_cast<float>(g) / 255.0f;
×
UNCOV
1698
    float const bNorm = static_cast<float>(b) / 255.0f;
×
1699

1700
    // Clip the dragged interval to visible range
1701
    float const dragged_start = std::max(static_cast<float>(_dragged_start_time), start_time);
×
1702
    float const dragged_end = std::min(static_cast<float>(_dragged_end_time), end_time);
×
1703

1704
    // Draw the original interval dimmed (alpha = 0.2)
1705
    float const original_start = std::max(static_cast<float>(_original_start_time), start_time);
×
1706
    float const original_end = std::min(static_cast<float>(_original_end_time), end_time);
×
1707

1708
    // Set color and alpha uniforms for original interval (dimmed)
1709
    glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
×
UNCOV
1710
    glUniform1f(m_alphaLoc, 0.2f);
×
1711

1712
    // Create 4D vertices (x, y, 0, 1) to match the shader expectations
1713
    std::array<GLfloat, 16> original_vertices = {
×
1714
            original_start, min_y, 0.0f, 1.0f,
1715
            original_end, min_y, 0.0f, 1.0f,
1716
            original_end, max_y, 0.0f, 1.0f,
1717
            original_start, max_y, 0.0f, 1.0f};
×
1718

UNCOV
1719
    m_vbo.bind();
×
1720
    m_vbo.allocate(original_vertices.data(), original_vertices.size() * sizeof(GLfloat));
×
1721
    m_vbo.release();
×
UNCOV
1722
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
×
1723

1724
    // Set color and alpha uniforms for dragged interval (semi-transparent)
1725
    glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
×
1726
    glUniform1f(m_alphaLoc, 0.8f);
×
1727

1728
    // Create 4D vertices (x, y, 0, 1) to match the shader expectations
UNCOV
1729
    std::array<GLfloat, 16> dragged_vertices = {
×
1730
            dragged_start, min_y, 0.0f, 1.0f,
1731
            dragged_end, min_y, 0.0f, 1.0f,
1732
            dragged_end, max_y, 0.0f, 1.0f,
1733
            dragged_start, max_y, 0.0f, 1.0f};
×
1734

UNCOV
1735
    m_vbo.bind();
×
UNCOV
1736
    m_vbo.allocate(dragged_vertices.data(), dragged_vertices.size() * sizeof(GLfloat));
×
UNCOV
1737
    m_vbo.release();
×
UNCOV
1738
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
×
1739

UNCOV
1740
    glUseProgram(0);
×
UNCOV
1741
}
×
1742

1743
namespace TimeSeriesDefaultValues {
1744
std::string getColorForIndex(size_t index) {
×
1745
    if (index < DEFAULT_COLORS.size()) {
×
UNCOV
1746
        return DEFAULT_COLORS[index];
×
1747
    } else {
UNCOV
1748
        return generateRandomColor();
×
1749
    }
1750
}
1751
}// namespace TimeSeriesDefaultValues
1752

1753
void OpenGLWidget::mouseDoubleClickEvent(QMouseEvent * event) {
×
UNCOV
1754
    if (event->button() == Qt::LeftButton) {
×
1755
        // Check if we're double-clicking over a digital interval series
1756
        //float const canvas_x = static_cast<float>(event->pos().x());
1757
        //float const canvas_y = static_cast<float>(event->pos().y());
1758

1759
        // Find which digital interval series (if any) is at this Y position
1760
        // For now, use the first visible digital interval series
1761
        // TODO: Improve this to detect which series based on Y coordinate
1762
        for (auto const & [series_key, data]: _digital_interval_series) {
×
UNCOV
1763
            if (data.display_options->is_visible) {
×
UNCOV
1764
                startNewIntervalCreation(series_key, event->pos());
×
1765
                return;
×
1766
            }
1767
        }
1768
    }
1769

1770
    QOpenGLWidget::mouseDoubleClickEvent(event);
×
1771
}
1772

UNCOV
1773
void OpenGLWidget::startNewIntervalCreation(std::string const & series_key, QPoint const & start_pos) {
×
1774
    // Don't start if we're already dragging an interval
1775
    if (_is_dragging_interval || _is_creating_new_interval) {
×
UNCOV
1776
        return;
×
1777
    }
1778

1779
    // Check if the series exists
UNCOV
1780
    auto it = _digital_interval_series.find(series_key);
×
1781
    if (it == _digital_interval_series.end()) {
×
UNCOV
1782
        return;
×
1783
    }
1784

1785
    _is_creating_new_interval = true;
×
1786
    _new_interval_series_key = series_key;
×
UNCOV
1787
    _new_interval_click_pos = start_pos;
×
1788

1789
    // Convert click position to time coordinate (in master time frame)
1790
    float const click_time_master = canvasXToTime(static_cast<float>(start_pos.x()));
×
1791
    _new_interval_click_time = static_cast<int64_t>(std::round(click_time_master));
×
1792

1793
    // Initialize start and end to the click position
1794
    _new_interval_start_time = _new_interval_click_time;
×
1795
    _new_interval_end_time = _new_interval_click_time;
×
1796

1797
    // Set cursor to indicate creation mode
1798
    setCursor(Qt::SizeHorCursor);
×
1799

1800
    std::cout << "Started new interval creation for series " << series_key
1801
              << " at time " << _new_interval_click_time << std::endl;
×
1802
}
1803

UNCOV
1804
void OpenGLWidget::updateNewIntervalCreation(QPoint const & current_pos) {
×
1805
    if (!_is_creating_new_interval) {
×
1806
        return;
×
1807
    }
1808

1809
    // Convert current mouse position to time coordinate (in master time frame)
UNCOV
1810
    float const current_time_master = canvasXToTime(static_cast<float>(current_pos.x()));
×
UNCOV
1811
    int64_t const current_time_coord = static_cast<int64_t>(std::round(current_time_master));
×
1812

1813
    // Get the series data for constraint checking
UNCOV
1814
    auto it = _digital_interval_series.find(_new_interval_series_key);
×
UNCOV
1815
    if (it == _digital_interval_series.end()) {
×
1816
        // Series not found - abort creation
1817
        cancelNewIntervalCreation();
×
1818
        return;
×
1819
    }
1820

1821
    auto const & series = it->second.series;
×
1822
    auto const & time_frame = it->second.time_frame;
×
1823

1824
    // Convert coordinates to series time frame for collision detection
1825
    int64_t click_time_series, current_time_series;
×
UNCOV
1826
    if (time_frame.get() == _master_time_frame.get()) {
×
1827
        // Same time frame - use coordinates directly
UNCOV
1828
        click_time_series = _new_interval_click_time;
×
UNCOV
1829
        current_time_series = current_time_coord;
×
1830
    } else {
1831
        // Convert master time coordinates to series time frame indices
UNCOV
1832
        click_time_series = time_frame->getIndexAtTime(static_cast<float>(_new_interval_click_time)).getValue();
×
UNCOV
1833
        current_time_series = time_frame->getIndexAtTime(static_cast<float>(current_time_coord)).getValue();
×
1834
    }
1835

1836
    // Determine interval bounds (always ensure start < end)
1837
    int64_t new_start_series = std::min(click_time_series, current_time_series);
×
1838
    int64_t new_end_series = std::max(click_time_series, current_time_series);
×
1839

1840
    // Ensure minimum interval size of 1
UNCOV
1841
    if (new_start_series == new_end_series) {
×
1842
        if (current_time_series >= click_time_series) {
×
UNCOV
1843
            new_end_series = new_start_series + 1;
×
1844
        } else {
UNCOV
1845
            new_start_series = new_end_series - 1;
×
1846
        }
1847
    }
1848

1849
    // Check for collision with existing intervals in series time frame
UNCOV
1850
    auto overlapping_intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(
×
1851
            new_start_series, new_end_series);
×
1852

1853
    // If there are overlapping intervals, constrain the new interval
UNCOV
1854
    for (auto const & interval: overlapping_intervals) {
×
UNCOV
1855
        if (current_time_series >= click_time_series) {
×
1856
            // Dragging right - stop before the first overlapping interval
1857
            if (new_end_series >= interval.start) {
×
UNCOV
1858
                new_end_series = interval.start - 1;
×
1859
                if (new_end_series <= new_start_series) {
×
1860
                    new_end_series = new_start_series + 1;
×
1861
                }
UNCOV
1862
                break;
×
1863
            }
1864
        } else {
1865
            // Dragging left - stop after the last overlapping interval
1866
            if (new_start_series <= interval.end) {
×
UNCOV
1867
                new_start_series = interval.end + 1;
×
1868
                if (new_start_series >= new_end_series) {
×
1869
                    new_start_series = new_end_series - 1;
×
1870
                }
UNCOV
1871
                break;
×
1872
            }
1873
        }
1874
    }
1875

1876
    // Convert back to master time frame for display
1877
    if (time_frame.get() == _master_time_frame.get()) {
×
1878
        // Same time frame
1879
        _new_interval_start_time = new_start_series;
×
UNCOV
1880
        _new_interval_end_time = new_end_series;
×
1881
    } else {
1882
        // Convert series indices back to master time coordinates
1883
        try {
1884
            _new_interval_start_time = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(new_start_series)));
×
UNCOV
1885
            _new_interval_end_time = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(new_end_series)));
×
1886
        } catch (...) {
×
1887
            // Conversion failed - abort creation
UNCOV
1888
            cancelNewIntervalCreation();
×
UNCOV
1889
            return;
×
1890
        }
×
1891
    }
1892

1893
    // Trigger redraw to show the new interval being created
UNCOV
1894
    updateCanvas(_time);
×
1895
}
1896

1897
void OpenGLWidget::finishNewIntervalCreation() {
×
UNCOV
1898
    if (!_is_creating_new_interval) {
×
1899
        return;
×
1900
    }
1901

1902
    // Get the series data
1903
    auto it = _digital_interval_series.find(_new_interval_series_key);
×
1904
    if (it == _digital_interval_series.end()) {
×
1905
        // Series not found - abort creation
UNCOV
1906
        cancelNewIntervalCreation();
×
UNCOV
1907
        return;
×
1908
    }
1909

UNCOV
1910
    auto const & series = it->second.series;
×
UNCOV
1911
    auto const & time_frame = it->second.time_frame;
×
1912

1913
    try {
1914
        // Convert coordinates to series time frame for data operations
1915
        int64_t new_start_series, new_end_series;
1916

UNCOV
1917
        if (time_frame.get() == _master_time_frame.get()) {
×
1918
            // Same time frame - use coordinates directly
1919
            new_start_series = _new_interval_start_time;
×
1920
            new_end_series = _new_interval_end_time;
×
1921
        } else {
1922
            // Convert master time coordinates to series time frame indices
UNCOV
1923
            new_start_series = time_frame->getIndexAtTime(static_cast<float>(_new_interval_start_time)).getValue();
×
1924
            new_end_series = time_frame->getIndexAtTime(static_cast<float>(_new_interval_end_time)).getValue();
×
1925
        }
1926

1927
        // Validate converted coordinates
UNCOV
1928
        if (new_start_series >= new_end_series || new_start_series < 0 || new_end_series < 0) {
×
UNCOV
1929
            throw std::runtime_error("Invalid interval bounds after conversion");
×
1930
        }
1931

1932
        // Add the new interval to the series
UNCOV
1933
        series->addEvent(TimeFrameIndex(new_start_series), TimeFrameIndex(new_end_series));
×
1934

1935
        // Set the new interval as selected (stored in master time frame coordinates)
UNCOV
1936
        setSelectedInterval(_new_interval_series_key, _new_interval_start_time, _new_interval_end_time);
×
1937

1938
        std::cout << "Created new interval [" << _new_interval_start_time
×
1939
                  << ", " << _new_interval_end_time << "] for series "
×
1940
                  << _new_interval_series_key << std::endl;
×
1941

UNCOV
1942
    } catch (...) {
×
1943
        // Error occurred during conversion or data update - abort creation
UNCOV
1944
        std::cout << "Error during new interval creation" << std::endl;
×
UNCOV
1945
        cancelNewIntervalCreation();
×
1946
        return;
×
1947
    }
×
1948

1949
    // Reset creation state
UNCOV
1950
    _is_creating_new_interval = false;
×
1951
    _new_interval_series_key.clear();
×
UNCOV
1952
    setCursor(Qt::ArrowCursor);
×
1953

1954
    // Trigger final redraw
UNCOV
1955
    updateCanvas(_time);
×
1956
}
1957

UNCOV
1958
void OpenGLWidget::cancelNewIntervalCreation() {
×
UNCOV
1959
    if (!_is_creating_new_interval) {
×
1960
        return;
×
1961
    }
1962

UNCOV
1963
    std::cout << "Cancelled new interval creation" << std::endl;
×
1964

1965
    // Reset creation state without applying changes
UNCOV
1966
    _is_creating_new_interval = false;
×
1967
    _new_interval_series_key.clear();
×
1968
    setCursor(Qt::ArrowCursor);
×
1969

1970
    // Trigger redraw to remove the new interval visualization
UNCOV
1971
    updateCanvas(_time);
×
1972
}
1973

1974
void OpenGLWidget::drawNewIntervalBeingCreated() {
69✔
1975
    if (!_is_creating_new_interval) {
69✔
1976
        return;
69✔
1977
    }
1978

1979
    // Get the series data for rendering
1980
    auto it = _digital_interval_series.find(_new_interval_series_key);
×
1981
    if (it == _digital_interval_series.end()) {
×
UNCOV
1982
        return;
×
1983
    }
1984

1985
    auto const & display_options = it->second.display_options;
×
1986

UNCOV
1987
    auto const start_time = static_cast<float>(_xAxis.getStart());
×
1988
    auto const end_time = static_cast<float>(_xAxis.getEnd());
×
1989
    auto const min_y = _yMin;
×
1990
    auto const max_y = _yMax;
×
1991

1992
    // Check if the new interval is visible
1993
    if (_new_interval_end_time < static_cast<int64_t>(start_time) || _new_interval_start_time > static_cast<int64_t>(end_time)) {
×
1994
        return;
×
1995
    }
1996

1997
    auto axesProgram = ShaderManager::instance().getProgram("axes");
×
UNCOV
1998
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
×
1999

2000
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
×
2001
    setupVertexAttribs();
×
2002

2003
    // Set up matrices (same as normal interval rendering)
2004
    auto Model = glm::mat4(1.0f);
×
2005
    auto View = glm::mat4(1.0f);
×
UNCOV
2006
    auto Projection = glm::ortho(start_time, end_time, min_y, max_y);
×
2007

UNCOV
2008
    glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
×
2009
    glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
×
UNCOV
2010
    glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
×
2011

2012
    // Get colors
2013
    int r, g, b;
×
UNCOV
2014
    hexToRGB(display_options->hex_color, r, g, b);
×
2015
    float const rNorm = static_cast<float>(r) / 255.0f;
×
2016
    float const gNorm = static_cast<float>(g) / 255.0f;
×
2017
    float const bNorm = static_cast<float>(b) / 255.0f;
×
2018

2019
    // Set color and alpha uniforms for new interval (50% transparency)
2020
    glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
×
2021
    glUniform1f(m_alphaLoc, 0.5f);
×
2022

2023
    // Clip the new interval to visible range
UNCOV
2024
    float const new_start = std::max(static_cast<float>(_new_interval_start_time), start_time);
×
UNCOV
2025
    float const new_end = std::min(static_cast<float>(_new_interval_end_time), end_time);
×
2026

2027
    // Draw the new interval being created with 50% transparency
2028
    // Create 4D vertices (x, y, 0, 1) to match the shader expectations
UNCOV
2029
    std::array<GLfloat, 16> new_interval_vertices = {
×
2030
            new_start, min_y, 0.0f, 1.0f,
2031
            new_end, min_y, 0.0f, 1.0f,
2032
            new_end, max_y, 0.0f, 1.0f,
UNCOV
2033
            new_start, max_y, 0.0f, 1.0f};
×
2034

UNCOV
2035
    m_vbo.bind();
×
UNCOV
2036
    m_vbo.allocate(new_interval_vertices.data(), new_interval_vertices.size() * sizeof(GLfloat));
×
UNCOV
2037
    m_vbo.release();
×
UNCOV
2038
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
×
2039

UNCOV
2040
    glUseProgram(0);
×
UNCOV
2041
}
×
2042

2043
///////////////////////////////////////////////////////////////////////////////
2044
// Tooltip functionality
2045
///////////////////////////////////////////////////////////////////////////////
2046

UNCOV
2047
void OpenGLWidget::startTooltipTimer(QPoint const & pos) {
×
2048
    // Restart the timer with new position
UNCOV
2049
    _tooltip_hover_pos = pos;
×
UNCOV
2050
    _tooltip_timer->start();
×
UNCOV
2051
}
×
2052

2053
void OpenGLWidget::cancelTooltipTimer() {
2✔
2054
    _tooltip_timer->stop();
2✔
2055
    QToolTip::hideText();
2✔
2056
}
2✔
2057

UNCOV
2058
std::optional<std::pair<std::string, std::string>> OpenGLWidget::findSeriesAtPosition(float canvas_x, float canvas_y) const {
×
UNCOV
2059
    if (!_plotting_manager) {
×
UNCOV
2060
        return std::nullopt;
×
2061
    }
2062

2063
    // Convert canvas Y to normalized device coordinates (NDC)
2064
    // In OpenGL, Y is inverted: top of window is -1, bottom is +1 in our view
UNCOV
2065
    float const ndc_y = -1.0f + 2.0f * (static_cast<float>(height()) - canvas_y) / static_cast<float>(height());
×
2066
    
2067
    // Apply vertical pan offset to get the actual Y position in the coordinate system
UNCOV
2068
    float const adjusted_y = ndc_y - _verticalPanOffset;
×
2069

2070
    // Check analog series first (in stacked mode)
UNCOV
2071
    int analog_index = 0;
×
UNCOV
2072
    for (auto const & [key, analog_data] : _analog_series) {
×
UNCOV
2073
        if (!analog_data.display_options->is_visible) {
×
UNCOV
2074
            continue;
×
2075
        }
2076

2077
        // Get the allocated space for this series
UNCOV
2078
        float allocated_center = analog_data.display_options->allocated_y_center;
×
UNCOV
2079
        float allocated_height = analog_data.display_options->allocated_height;
×
2080

2081
        // Calculate bounds for this series
UNCOV
2082
        float const series_y_min = allocated_center - allocated_height * 0.5f;
×
UNCOV
2083
        float const series_y_max = allocated_center + allocated_height * 0.5f;
×
2084

2085
        // Check if mouse Y is within this series' allocated space
UNCOV
2086
        if (adjusted_y >= series_y_min && adjusted_y <= series_y_max) {
×
UNCOV
2087
            return std::make_pair("Analog", key);
×
2088
        }
2089

UNCOV
2090
        analog_index++;
×
2091
    }
2092

2093
    // Check digital event series (only those in stacked mode)
UNCOV
2094
    int event_index = 0;
×
UNCOV
2095
    for (auto const & [key, event_data] : _digital_event_series) {
×
UNCOV
2096
        if (!event_data.display_options->is_visible) {
×
UNCOV
2097
            continue;
×
2098
        }
2099

2100
        // Only check stacked events
UNCOV
2101
        if (event_data.display_options->display_mode != EventDisplayMode::Stacked) {
×
UNCOV
2102
            continue;
×
2103
        }
2104

2105
        // Get the allocated space for this series
UNCOV
2106
        float allocated_center = event_data.display_options->allocated_y_center;
×
UNCOV
2107
        float allocated_height = event_data.display_options->allocated_height;
×
2108

2109
        // Calculate bounds for this series
UNCOV
2110
        float const series_y_min = allocated_center - allocated_height * 0.5f;
×
UNCOV
2111
        float const series_y_max = allocated_center + allocated_height * 0.5f;
×
2112

2113
        // Check if mouse Y is within this series' allocated space
UNCOV
2114
        if (adjusted_y >= series_y_min && adjusted_y <= series_y_max) {
×
UNCOV
2115
            return std::make_pair("Event", key);
×
2116
        }
2117

UNCOV
2118
        event_index++;
×
2119
    }
2120

UNCOV
2121
    return std::nullopt;
×
2122
}
2123

UNCOV
2124
void OpenGLWidget::showSeriesInfoTooltip(QPoint const & pos) {
×
UNCOV
2125
    float const canvas_x = static_cast<float>(pos.x());
×
UNCOV
2126
    float const canvas_y = static_cast<float>(pos.y());
×
2127

2128
    // Find which series is under the cursor
UNCOV
2129
    auto series_info = findSeriesAtPosition(canvas_x, canvas_y);
×
2130
    
UNCOV
2131
    if (series_info.has_value()) {
×
UNCOV
2132
        auto const & [series_type, series_key] = series_info.value();
×
2133
        
2134
        // Build tooltip text
UNCOV
2135
        QString tooltip_text;
×
UNCOV
2136
        if (series_type == "Analog") {
×
2137
            // Get the analog value at this Y coordinate
UNCOV
2138
            float const analog_value = canvasYToAnalogValue(canvas_y, series_key);
×
UNCOV
2139
            tooltip_text = QString("<b>Analog Series</b><br>Key: %1<br>Value: %2")
×
UNCOV
2140
                .arg(QString::fromStdString(series_key))
×
UNCOV
2141
                .arg(analog_value, 0, 'f', 3);
×
UNCOV
2142
        } else if (series_type == "Event") {
×
UNCOV
2143
            tooltip_text = QString("<b>Event Series</b><br>Key: %1")
×
UNCOV
2144
                .arg(QString::fromStdString(series_key));
×
2145
        }
2146
        
2147
        // Show tooltip at cursor position
UNCOV
2148
        QToolTip::showText(mapToGlobal(pos), tooltip_text, this);
×
UNCOV
2149
    } else {
×
2150
        // No series under cursor, hide tooltip
UNCOV
2151
        QToolTip::hideText();
×
2152
    }
UNCOV
2153
}
×
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