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

paulmthompson / WhiskerToolbox / 17468365695

04 Sep 2025 03:09PM UTC coverage: 70.849% (+0.02%) from 70.828%
17468365695

push

github

paulmthompson
faster loading for multi channel analog

0 of 3 new or added lines in 1 file covered. (0.0%)

770 existing lines in 6 files now uncovered.

34341 of 48471 relevant lines covered (70.85%)

1293.61 hits per line

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

25.12
/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

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

39
/*
40

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

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

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

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

56

57
*/
58

59
OpenGLWidget::OpenGLWidget(QWidget * parent)
15✔
60
    : QOpenGLWidget(parent) {
75✔
61
    setMouseTracking(true);// Enable mouse tracking for hover events
15✔
62
}
15✔
63

64
OpenGLWidget::~OpenGLWidget() {
30✔
65
    cleanup();
15✔
66
}
30✔
67

68
void OpenGLWidget::updateCanvas(int time) {
101✔
69
    _time = time;
101✔
70
    //std::cout << "Redrawing at " << _time << std::endl;
71
    update();
101✔
72
}
101✔
73

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

85
        _isPanning = true;
×
86
        _lastMousePos = event->pos();
×
87

88
        // Emit click coordinates for interval selection
89
        float const canvas_x = static_cast<float>(event->pos().x());
×
90
        float const canvas_y = static_cast<float>(event->pos().y());
×
91

92
        // Convert canvas X to time coordinate
93
        float const time_coord = canvasXToTime(canvas_x);
×
94

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

107
        emit mouseClick(time_coord, canvas_y, series_info);
×
108
    }
×
109
    QOpenGLWidget::mousePressEvent(event);
×
110
}
111

112
void OpenGLWidget::mouseMoveEvent(QMouseEvent * event) {
×
113
    if (_is_dragging_interval) {
×
114
        // Update interval drag
115
        updateIntervalDrag(event->pos());
×
116
        return;// Don't do other mouse move processing while dragging
×
117
    }
118

119
    if (_is_creating_new_interval) {
×
120
        // Update new interval creation
121
        updateNewIntervalCreation(event->pos());
×
122
        return;// Don't do other mouse move processing while creating
×
123
    }
124

125
    if (_isPanning) {
×
126
        // Calculate vertical movement in pixels
127
        int const deltaY = event->pos().y() - _lastMousePos.y();
×
128

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

133
        // Adjust vertical offset based on movement
134
        _verticalPanOffset += normalizedDeltaY;
×
135

136
        _lastMousePos = event->pos();
×
137
        update();// Request redraw
×
138
    } else {
139
        // Check for cursor changes when hovering near interval edges
140
        auto edge_info = findIntervalEdgeAtPosition(static_cast<float>(event->pos().x()), static_cast<float>(event->pos().y()));
×
141
        if (edge_info.has_value()) {
×
142
            setCursor(Qt::SizeHorCursor);
×
143
        } else {
144
            setCursor(Qt::ArrowCursor);
×
145
        }
146
    }
×
147

148
    // Emit hover coordinates for coordinate display
149
    float const canvas_x = static_cast<float>(event->pos().x());
×
150
    float const canvas_y = static_cast<float>(event->pos().y());
×
151

152
    // Convert canvas X to time coordinate
153
    float const time_coord = canvasXToTime(canvas_x);
×
154

155
    // Find the closest analog series for Y coordinate conversion
156
    QString series_info = "";
×
157
    if (!_analog_series.empty()) {
×
158
        // For now, use the first visible analog series for Y coordinate conversion
159
        for (auto const & [key, data]: _analog_series) {
×
160
            if (data.display_options->is_visible) {
×
161
                float const analog_value = canvasYToAnalogValue(canvas_y, key);
×
162
                series_info = QString("Series: %1, Value: %2").arg(QString::fromStdString(key)).arg(analog_value, 0, 'f', 3);
×
163
                break;
×
164
            }
165
        }
166
    }
167

168
    emit mouseHover(time_coord, canvas_y, series_info);
×
169

170
    QOpenGLWidget::mouseMoveEvent(event);
×
171
}
×
172

173
void OpenGLWidget::mouseReleaseEvent(QMouseEvent * event) {
×
174
    if (event->button() == Qt::LeftButton) {
×
175
        if (_is_dragging_interval) {
×
176
            finishIntervalDrag();
×
177
        } else if (_is_creating_new_interval) {
×
178
            finishNewIntervalCreation();
×
179
        } else {
180
            _isPanning = false;
×
181
        }
182
    }
183
    QOpenGLWidget::mouseReleaseEvent(event);
×
184
}
×
185

186
void OpenGLWidget::setBackgroundColor(std::string const & hexColor) {
×
187
    m_background_color = hexColor;
×
188
    updateCanvas(_time);
×
189
}
×
190

191
void OpenGLWidget::setPlotTheme(PlotTheme theme) {
×
192
    _plot_theme = theme;
×
193

194
    if (theme == PlotTheme::Dark) {
×
195
        // Dark theme: black background, white axes
196
        m_background_color = "#000000";
×
197
        m_axis_color = "#FFFFFF";
×
198
    } else {
199
        // Light theme: white background, dark axes
200
        m_background_color = "#FFFFFF";
×
201
        m_axis_color = "#333333";
×
202
    }
203

204
    updateCanvas(_time);
×
205
}
×
206

207
void OpenGLWidget::cleanup() {
28✔
208
    // Avoid re-entrancy or cleanup without a valid context
209
    if (!_gl_initialized) {
28✔
210
        return;
15✔
211
    }
212

213
    // Guard: QOpenGLContext may already be gone during teardown
214
    if (QOpenGLContext::currentContext() == nullptr && context() == nullptr) {
13✔
215
        _gl_initialized = false;
×
216
        return;
×
217
    }
218

219
    // Safe to release our GL resources
220
    makeCurrent();
13✔
221

222
    if (m_program) {
13✔
223
        delete m_program;
×
224
        m_program = nullptr;
×
225
    }
226
    if (m_dashedProgram) {
13✔
227
        delete m_dashedProgram;
×
228
        m_dashedProgram = nullptr;
×
229
    }
230

231
    m_vbo.destroy();
13✔
232
    m_vao.destroy();
13✔
233

234
    doneCurrent();
13✔
235

236
    _gl_initialized = false;
13✔
237
}
238

239
QOpenGLShaderProgram * create_shader_program(char const * vertexShaderSource,
×
240
                                             char const * fragmentShaderSource) {
241
    auto prog = new QOpenGLShaderProgram;
×
242
    prog->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource);
×
243
    prog->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource);
×
244

245
    prog->link();
×
246

247
    return prog;
×
248
}
249

250
// Replace the old create_shader_program with a new version that loads from resource files for the dashed shader
251
QOpenGLShaderProgram * create_shader_program_from_resource(QString const & vertexResource, QString const & fragmentResource) {
×
252
    auto prog = new QOpenGLShaderProgram;
×
253
    prog->addShaderFromSourceFile(QOpenGLShader::Vertex, vertexResource);
×
254
    prog->addShaderFromSourceFile(QOpenGLShader::Fragment, fragmentResource);
×
255
    prog->link();
×
256
    return prog;
×
257
}
258

259
void OpenGLWidget::initializeGL() {
13✔
260
    // Ensure QOpenGLFunctions is initialized
261
    initializeOpenGLFunctions();
13✔
262

263
    // Track GL init and connect context destruction
264
    _gl_initialized = true;
13✔
265
    if (context()) {
13✔
266
        // Disconnect any previous connection to avoid duplicates
267
        if (_ctxAboutToBeDestroyedConn) {
13✔
268
            disconnect(_ctxAboutToBeDestroyedConn);
×
269
        }
270
        _ctxAboutToBeDestroyedConn = connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, [this]() {
26✔
271
            cleanup();
13✔
272
        });
13✔
273
    }
274

275
    auto fmt = format();
13✔
276
    std::cout << "OpenGL major version: " << fmt.majorVersion() << std::endl;
13✔
277
    std::cout << "OpenGL minor version: " << fmt.minorVersion() << std::endl;
13✔
278
    int r, g, b;
13✔
279
    hexToRGB(m_background_color, r, g, b);
13✔
280
    glClearColor(
13✔
281
            static_cast<float>(r) / 255.0f,
13✔
282
            static_cast<float>(g) / 255.0f,
13✔
283
            static_cast<float>(b) / 255.0f,
13✔
284
            1.0f);
285
    glEnable(GL_BLEND);
13✔
286
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
13✔
287
    // Load axes shader
288
    ShaderManager::instance().loadProgram(
117✔
289
            "axes",
290
            m_shaderSourceType == ShaderSourceType::Resource ? ":/shaders/colored_vertex.vert" : "src/WhiskerToolbox/shaders/colored_vertex.vert",
13✔
291
            m_shaderSourceType == ShaderSourceType::Resource ? ":/shaders/colored_vertex.frag" : "src/WhiskerToolbox/shaders/colored_vertex.frag",
13✔
292
            "",
293
            m_shaderSourceType);
294
    // Load dashed line shader
295
    ShaderManager::instance().loadProgram(
117✔
296
            "dashed_line",
297
            m_shaderSourceType == ShaderSourceType::Resource ? ":/shaders/dashed_line.vert" : "src/WhiskerToolbox/shaders/dashed_line.vert",
13✔
298
            m_shaderSourceType == ShaderSourceType::Resource ? ":/shaders/dashed_line.frag" : "src/WhiskerToolbox/shaders/dashed_line.frag",
13✔
299
            "",
300
            m_shaderSourceType);
301
    
302
    // Get uniform locations for axes shader
303
    auto axesProgram = ShaderManager::instance().getProgram("axes");
39✔
304
    if (axesProgram) {
13✔
305
        auto nativeProgram = axesProgram->getNativeProgram();
13✔
306
        if (nativeProgram) {
13✔
307
            m_projMatrixLoc = nativeProgram->uniformLocation("projMatrix");
13✔
308
            m_viewMatrixLoc = nativeProgram->uniformLocation("viewMatrix");
13✔
309
            m_modelMatrixLoc = nativeProgram->uniformLocation("modelMatrix");
13✔
310
            m_colorLoc = nativeProgram->uniformLocation("u_color");
13✔
311
            m_alphaLoc = nativeProgram->uniformLocation("u_alpha");
13✔
312
        }
313
    }
314
    
315
    // Get uniform locations for dashed line shader
316
    auto dashedProgram = ShaderManager::instance().getProgram("dashed_line");
39✔
317
    if (dashedProgram) {
13✔
318
        auto nativeProgram = dashedProgram->getNativeProgram();
13✔
319
        if (nativeProgram) {
13✔
320
            m_dashedProjMatrixLoc = nativeProgram->uniformLocation("u_mvp");
13✔
321
            m_dashedResolutionLoc = nativeProgram->uniformLocation("u_resolution");
13✔
322
            m_dashedDashSizeLoc = nativeProgram->uniformLocation("u_dashSize");
13✔
323
            m_dashedGapSizeLoc = nativeProgram->uniformLocation("u_gapSize");
13✔
324
        }
325
    }
326
    
327
    // Connect reload signal to redraw
328
    connect(&ShaderManager::instance(), &ShaderManager::shaderReloaded, this, [this](std::string const &) { update(); });
13✔
329
    m_vao.create();
13✔
330
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
13✔
331
    m_vbo.create();
13✔
332
    m_vbo.bind();
13✔
333
    m_vbo.setUsagePattern(QOpenGLBuffer::StaticDraw);
13✔
334
    setupVertexAttribs();
13✔
335
}
26✔
336

337
void OpenGLWidget::setupVertexAttribs() {
181✔
338

339
    m_vbo.bind();                     // glBindBuffer(GL_ARRAY_BUFFER, m_vbo.bufferId());
181✔
340
    int const vertex_argument_num = 4;// Position (x, y, 0, 1) for axes shader
181✔
341

342
    // Attribute 0: vertex positions (x, y, 0, 1) for axes shader
343
    glEnableVertexAttribArray(0);
181✔
344
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, vertex_argument_num * sizeof(GLfloat), nullptr);
181✔
345

346
    // Disable unused vertex attributes
347
    glDisableVertexAttribArray(1);
181✔
348
    glDisableVertexAttribArray(2);
181✔
349

350
    m_vbo.release();
181✔
351
}
181✔
352

353
///////////////////////////////////////////////////////////////////////////////
354

355
/**
356
 * @brief OpenGLWidget::drawDigitalEventSeries
357
 *
358
 * Each event is specified by a single time point.
359
 * We can find which of the time points are within the visible time frame
360
 * After those are found, we will draw a vertical line at that time point
361
 * 
362
 * Now supports two display modes:
363
 * - Stacked: Events are positioned in separate horizontal lanes with configurable spacing
364
 * - Full Canvas: Events stretch from top to bottom of the canvas (original behavior)
365
 */
366
void OpenGLWidget::drawDigitalEventSeries() {
42✔
367
    int r, g, b;
42✔
368
    auto const start_time = _xAxis.getStart();
42✔
369
    auto const end_time = _xAxis.getEnd();
42✔
370
    auto axesProgram = ShaderManager::instance().getProgram("axes");
126✔
371
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
42✔
372

373
    auto const min_y = _yMin;
42✔
374
    auto const max_y = _yMax;
42✔
375

376
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);// glBindVertexArray
42✔
377
    setupVertexAttribs();
42✔
378

379
    // Count visible event series for stacked positioning
380
    int visible_event_count = 0;
42✔
381
    for (auto const & [key, event_data]: _digital_event_series) {
42✔
382
        if (event_data.display_options->is_visible) {
×
383
            visible_event_count++;
×
384
        }
385
    }
386

387
    if (visible_event_count == 0 || !_master_time_frame) {
42✔
388
        glUseProgram(0);
42✔
389
        return;
42✔
390
    }
391

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

395
    int visible_series_index = 0;
×
396

397
    for (auto const & [key, event_data]: _digital_event_series) {
×
398
        auto const & series = event_data.series;
×
399
        auto const & time_frame = event_data.time_frame;
×
400
        auto const & display_options = event_data.display_options;
×
401

402
        if (!display_options->is_visible) continue;
×
403

404
        hexToRGB(display_options->hex_color, r, g, b);
×
405
        float const rNorm = static_cast<float>(r) / 255.0f;
×
406
        float const gNorm = static_cast<float>(g) / 255.0f;
×
407
        float const bNorm = static_cast<float>(b) / 255.0f;
×
408
        float const alpha = display_options->alpha;
×
409

410
        auto visible_events = series->getEventsInRange(TimeFrameIndex(start_time),
×
411
                                                       TimeFrameIndex(end_time),
412
                                                       time_frame.get(),
×
413
                                                       _master_time_frame.get());
×
414

415
        // === MVP MATRIX SETUP ===
416

417
        // We need to check if we have a PlottingManager reference
418
        if (!_plotting_manager) {
×
419
            std::cerr << "Warning: PlottingManager not set in OpenGLWidget" << std::endl;
×
420
            continue;
×
421
        }
422

423
        // Calculate coordinate allocation from PlottingManager
424
        // For now, we'll use the visible series index to allocate coordinates
425
        float allocated_y_center, allocated_height;
×
426
        _plotting_manager->calculateGlobalStackedAllocation(-1, visible_series_index, visible_event_count, allocated_y_center, allocated_height);
×
427

428
        display_options->allocated_y_center = allocated_y_center;
×
429
        display_options->allocated_height = allocated_height;
×
430
        display_options->plotting_mode = (display_options->display_mode == EventDisplayMode::Stacked) ? EventPlottingMode::Stacked : EventPlottingMode::FullCanvas;
×
431

432
        // Apply PlottingManager pan offset
433
        _plotting_manager->setPanOffset(_verticalPanOffset);
×
434

435
        auto Model = new_getEventModelMat(*display_options, *_plotting_manager);
×
436
        auto View = new_getEventViewMat(*display_options, *_plotting_manager);
×
437
        auto Projection = new_getEventProjectionMat(start_time, end_time, _yMin, _yMax, *_plotting_manager);
×
438

439
        glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
×
440
        glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
×
441
        glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
×
442

443
        // Set color and alpha uniforms
444
        glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
×
445
        glUniform1f(m_alphaLoc, alpha);
×
446

447
        // Set line thickness from display options
448
        glLineWidth(static_cast<float>(display_options->line_thickness));
×
449

450
        for (auto const & event: visible_events) {
×
451
            // Calculate X position in master time frame coordinates for consistent rendering
452
            float xCanvasPos;
453
            if (time_frame.get() == _master_time_frame.get()) {
×
454
                // Same time frame - event is already in correct coordinates
455
                xCanvasPos = event;
×
456
            } else {
457
                // Different time frames - convert event index to time, then to master time frame
458
                float event_time = static_cast<float>(time_frame->getTimeAtIndex(TimeFrameIndex(static_cast<int>(event))));
×
459
                xCanvasPos = event_time;// This should work if both time frames use the same time units
×
460
            }
461

462
            std::array<GLfloat, 4> vertices = {
×
463
                    xCanvasPos, min_y,
464
                    xCanvasPos, max_y};
×
465

466
            glBindBuffer(GL_ARRAY_BUFFER, m_vbo.bufferId());
×
467
            m_vbo.allocate(vertices.data(), vertices.size() * sizeof(GLfloat));
×
468

469
            GLint const first = 0;  // Starting index of enabled array
×
470
            GLsizei const count = 2;// number of indexes to render
×
471
            glDrawArrays(GL_LINES, first, count);
×
472
        }
473

474
        visible_series_index++;
×
475
    }
476

477
    // Reset line width to default
478
    glLineWidth(1.0f);
×
479
    glUseProgram(0);
×
480
}
42✔
481

482
///////////////////////////////////////////////////////////////////////////////
483

484
void OpenGLWidget::drawDigitalIntervalSeries() {
42✔
485
    int r, g, b;
42✔
486
    auto const start_time = static_cast<float>(_xAxis.getStart());
42✔
487
    auto const end_time = static_cast<float>(_xAxis.getEnd());
42✔
488

489
    //auto const min_y = _yMin;
490
    //auto const max_y = _yMax;
491

492
    auto axesProgram = ShaderManager::instance().getProgram("axes");
126✔
493
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
42✔
494

495
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);// glBindVertexArray
42✔
496
    setupVertexAttribs();
42✔
497

498
    if (!_master_time_frame) {
42✔
499
        glUseProgram(0);
×
500
        return;
×
501
    }
502

503
    for (auto const & [key, interval_data]: _digital_interval_series) {
42✔
504
        auto const & series = interval_data.series;
×
505
        auto const & time_frame = interval_data.time_frame;
×
506
        auto const & display_options = interval_data.display_options;
×
507

508
        if (!display_options->is_visible) continue;
×
509

510
        // Get only the intervals that overlap with the visible range
511
        // These will be
512
        auto visible_intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(
×
513
                TimeFrameIndex(static_cast<int64_t>(start_time)),
514
                TimeFrameIndex(static_cast<int64_t>(end_time)),
515
                _master_time_frame.get(),
×
516
                time_frame.get()
×
517
                );
×
518

519
        hexToRGB(display_options->hex_color, r, g, b);
×
520
        float const rNorm = static_cast<float>(r) / 255.0f;
×
521
        float const gNorm = static_cast<float>(g) / 255.0f;
×
522
        float const bNorm = static_cast<float>(b) / 255.0f;
×
523
        float const alpha = display_options->alpha;
×
524

525
        // === MVP MATRIX SETUP ===
526

527
        // We need to check if we have a PlottingManager reference
528
        if (!_plotting_manager) {
×
529
            std::cerr << "Warning: PlottingManager not set in OpenGLWidget" << std::endl;
×
530
            continue;
×
531
        }
532

533
        // Calculate coordinate allocation from PlottingManager
534
        // Digital intervals typically use full canvas allocation
535
        float allocated_y_center, allocated_height;
×
536
        _plotting_manager->calculateDigitalIntervalSeriesAllocation(0, allocated_y_center, allocated_height);
×
537

538
        display_options->allocated_y_center = allocated_y_center;
×
539
        display_options->allocated_height = allocated_height;
×
540

541
        // Apply PlottingManager pan offset
542
        _plotting_manager->setPanOffset(_verticalPanOffset);
×
543

544
        auto Model = new_getIntervalModelMat(*display_options, *_plotting_manager);
×
545
        auto View = new_getIntervalViewMat(*_plotting_manager);
×
546
        auto Projection = new_getIntervalProjectionMat(start_time, end_time, _yMin, _yMax, *_plotting_manager);
×
547

548
        glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
×
549
        glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
×
550
        glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
×
551

552
        // Set color and alpha uniforms
553
        glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
×
554
        glUniform1f(m_alphaLoc, alpha);
×
555

556
        for (auto const & interval: visible_intervals) {
×
557

558
            std::cout << "interval.start:" << interval.start << "interval.end:" << interval.end << std::endl;
×
559

560
            auto start = static_cast<float>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.start)));
×
561
            auto end = static_cast<float>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.end)));
×
562

563
            //Clip the interval to the visible range
564
            start = std::max(start, start_time);
×
565
            end = std::min(end, end_time);
×
566

567
            float const xStart = start;
×
568
            float const xEnd = end;
×
569

570
            // Use normalized coordinates for intervals
571
            // The Model matrix will handle positioning and scaling
572
            float const interval_y_min = -1.0f;// Bottom of interval in local coordinates
×
573
            float const interval_y_max = +1.0f;// Top of interval in local coordinates
×
574

575
            // Create 4D vertices (x, y, 0, 1) to match the shader expectations
576
            std::array<GLfloat, 16> vertices = {
×
577
                    xStart, interval_y_min, 0.0f, 1.0f,
578
                    xEnd, interval_y_min, 0.0f, 1.0f,
579
                    xEnd, interval_y_max, 0.0f, 1.0f,
580
                    xStart, interval_y_max, 0.0f, 1.0f};
×
581

582
            m_vbo.bind();
×
583
            m_vbo.allocate(vertices.data(), vertices.size() * sizeof(GLfloat));
×
584
            m_vbo.release();
×
585

586
            GLint const first = 0;  // Starting index of enabled array
×
587
            GLsizei const count = 4;// number of indexes to render
×
588
            glDrawArrays(GL_TRIANGLE_FAN, first, count);
×
589
        }
590

591
        // Draw highlighting for selected intervals
592
        auto selected_interval = getSelectedInterval(key);
×
593
        if (selected_interval.has_value() && !(_is_dragging_interval && _dragging_series_key == key)) {
×
594
            auto const [sel_start_time, sel_end_time] = selected_interval.value();
×
595

596
            // Check if the selected interval overlaps with visible range
597
            if (sel_end_time >= static_cast<int64_t>(start_time) && sel_start_time <= static_cast<int64_t>(end_time)) {
×
598
                // Clip the selected interval to the visible range
599
                float const highlighted_start = std::max(static_cast<float>(sel_start_time), start_time);
×
600
                float const highlighted_end = std::min(static_cast<float>(sel_end_time), end_time);
×
601

602
                // Draw a thick border around the selected interval
603
                // Use a brighter version of the same color for highlighting
604
                float const highlight_rNorm = std::min(1.0f, rNorm + 0.3f);
×
605
                float const highlight_gNorm = std::min(1.0f, gNorm + 0.3f);
×
606
                float const highlight_bNorm = std::min(1.0f, bNorm + 0.3f);
×
607

608
                // Set line width for highlighting
609
                glLineWidth(4.0f);
×
610

611
                // Draw the four border lines of the rectangle
612
                // Set highlight color uniforms
613
                glUniform3f(m_colorLoc, highlight_rNorm, highlight_gNorm, highlight_bNorm);
×
614
                glUniform1f(m_alphaLoc, 1.0f);
×
615

616
                // Bottom edge
617
                std::array<GLfloat, 8> bottom_edge = {
×
618
                        highlighted_start, -1.0f, 0.0f, 1.0f,
619
                        highlighted_end, -1.0f, 0.0f, 1.0f};
×
620
                m_vbo.bind();
×
621
                m_vbo.allocate(bottom_edge.data(), bottom_edge.size() * sizeof(GLfloat));
×
622
                m_vbo.release();
×
623
                glDrawArrays(GL_LINES, 0, 2);
×
624

625
                // Top edge
626
                std::array<GLfloat, 8> top_edge = {
×
627
                        highlighted_start, 1.0f, 0.0f, 1.0f,
628
                        highlighted_end, 1.0f, 0.0f, 1.0f};
×
629
                m_vbo.bind();
×
630
                m_vbo.allocate(top_edge.data(), top_edge.size() * sizeof(GLfloat));
×
631
                m_vbo.release();
×
632
                glDrawArrays(GL_LINES, 0, 2);
×
633

634
                // Left edge
635
                std::array<GLfloat, 8> left_edge = {
×
636
                        highlighted_start, -1.0f, 0.0f, 1.0f,
637
                        highlighted_start, 1.0f, 0.0f, 1.0f};
×
638
                m_vbo.bind();
×
639
                m_vbo.allocate(left_edge.data(), left_edge.size() * sizeof(GLfloat));
×
640
                m_vbo.release();
×
641
                glDrawArrays(GL_LINES, 0, 2);
×
642

643
                // Right edge
644
                std::array<GLfloat, 8> right_edge = {
×
645
                        highlighted_end, -1.0f, 0.0f, 1.0f,
646
                        highlighted_end, 1.0f, 0.0f, 1.0f};
×
647
                m_vbo.bind();
×
648
                m_vbo.allocate(right_edge.data(), right_edge.size() * sizeof(GLfloat));
×
649
                m_vbo.release();
×
650
                glDrawArrays(GL_LINES, 0, 2);
×
651

652
                // Reset line width
653
                glLineWidth(1.0f);
×
654
            }
655
        }
656
    }
657

658
    glUseProgram(0);
42✔
659
}
42✔
660

661
///////////////////////////////////////////////////////////////////////////////
662

663
void OpenGLWidget::drawAnalogSeries() {
42✔
664
    int r, g, b;
42✔
665

666
    auto const start_time = TimeFrameIndex(_xAxis.getStart());
42✔
667
    auto const end_time = TimeFrameIndex(_xAxis.getEnd());
42✔
668

669
    auto axesProgram = ShaderManager::instance().getProgram("axes");
126✔
670
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
42✔
671

672
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
42✔
673
    setupVertexAttribs();
42✔
674

675
    if (!_master_time_frame) {
42✔
676
        glUseProgram(0);
×
677
        return;
×
678
    }
679

680
    int i = 0;
42✔
681

682
    for (auto const & [key, analog_data]: _analog_series) {
80✔
683
        auto const & series = analog_data.series;
38✔
684
        auto const & data = series->getAnalogTimeSeries();
38✔
685
        //if (!series->hasTimeFrameV2()) {
686
        //    continue;
687
        //}
688
        //auto time_frame = series->getTimeFrameV2().value();
689
        auto time_frame = analog_data.time_frame;
38✔
690

691
        auto const & display_options = analog_data.display_options;
38✔
692

693
        if (!display_options->is_visible) continue;
38✔
694

695
        // Calculate coordinate allocation from PlottingManager
696
        // For now, we'll use the analog series index to allocate coordinates
697
        // This is a temporary bridge until we fully migrate series management to PlottingManager
698
        if (!_plotting_manager) {
38✔
699
            std::cerr << "Warning: PlottingManager not set in OpenGLWidget" << std::endl;
×
700
            continue;
×
701
        }
702

703
        float allocated_y_center, allocated_height;
38✔
704
        if (!_plotting_manager->getAnalogSeriesAllocationForKey(key, allocated_y_center, allocated_height)) {
38✔
UNCOV
705
            _plotting_manager->calculateAnalogSeriesAllocation(i, allocated_y_center, allocated_height);
×
706
        }
707

708
        display_options->allocated_y_center = allocated_y_center;
38✔
709
        display_options->allocated_height = allocated_height;
38✔
710

711
        // Set the color for the current series
712
        hexToRGB(display_options->hex_color, r, g, b);
38✔
713
        float const rNorm = static_cast<float>(r) / 255.0f;
38✔
714
        float const gNorm = static_cast<float>(g) / 255.0f;
38✔
715
        float const bNorm = static_cast<float>(b) / 255.0f;
38✔
716

717
        m_vertices.clear();
38✔
718

719
        auto series_start_index = getTimeIndexForSeries(start_time, _master_time_frame.get(), time_frame.get());
38✔
720
        auto series_end_index = getTimeIndexForSeries(end_time, _master_time_frame.get(), time_frame.get());
38✔
721

722
        // === MVP MATRIX SETUP ===
723

724
        // Apply PlottingManager pan offset
725
        _plotting_manager->setPanOffset(_verticalPanOffset);
38✔
726

727
        auto Model = new_getAnalogModelMat(*display_options, display_options->cached_std_dev, display_options->cached_mean, *_plotting_manager);
38✔
728
        auto View = new_getAnalogViewMat(*_plotting_manager);
38✔
729
        auto Projection = new_getAnalogProjectionMat(start_time, end_time, _yMin, _yMax, *_plotting_manager);
38✔
730

731
        glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
38✔
732
        glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
38✔
733
        glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
38✔
734

735
        // Set color and alpha uniforms
736
        glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
38✔
737
        glUniform1f(m_alphaLoc, 1.0f);
38✔
738

739
        auto analog_range = series->getTimeValueSpanInTimeFrameIndexRange(series_start_index, series_end_index);
38✔
740

741
        if (analog_range.values.empty()) {
38✔
742
            // Instead of returning early (which stops rendering ALL series),
743
            // continue to the next series. This allows other series to still be rendered
744
            // even if this particular series has no data in the current visible range.
UNCOV
745
            i++;
×
UNCOV
746
            continue;
×
747
        }
748

749
        if (display_options->gap_handling == AnalogGapHandling::AlwaysConnect) {
38✔
750

751
            auto time_begin = analog_range.time_indices.begin();
×
752

UNCOV
753
            for (size_t i = 0; i < analog_range.values.size(); i++) {
×
754
                auto const xCanvasPos = time_frame->getTimeAtIndex(**time_begin);
×
755
                //auto const xCanvasPos = point.time_frame_index.getValue();
756
                auto const yCanvasPos = analog_range.values[i];
×
757

758
                m_vertices.push_back(xCanvasPos);
×
759
                m_vertices.push_back(yCanvasPos);
×
UNCOV
760
                m_vertices.push_back(0.0f);  // z coordinate
×
761
                m_vertices.push_back(1.0f);  // w coordinate
×
762

763
                ++(*time_begin);
×
764
            }
765
            m_vbo.bind();
×
UNCOV
766
            m_vbo.allocate(m_vertices.data(), static_cast<int>(m_vertices.size() * sizeof(GLfloat)));
×
UNCOV
767
            m_vbo.release();
×
768

769
            // Set line thickness from display options
UNCOV
770
            glLineWidth(static_cast<float>(display_options->line_thickness));
×
UNCOV
771
            glDrawArrays(GL_LINE_STRIP, 0, static_cast<int>(m_vertices.size() / 4));
×
772

773
        } else if (display_options->gap_handling == AnalogGapHandling::DetectGaps) {
38✔
774
            // Draw multiple line segments, breaking at gaps
775
            // Set line thickness before drawing segments
776
            glLineWidth(static_cast<float>(display_options->line_thickness));
38✔
777
            _drawAnalogSeriesWithGapDetection(data, time_frame, analog_range,
38✔
778
                                              display_options->gap_threshold);
38✔
779

780
        } else if (display_options->gap_handling == AnalogGapHandling::ShowMarkers) {
×
781
            // Draw individual markers instead of lines
UNCOV
782
            _drawAnalogSeriesAsMarkers(data, time_frame, analog_range);
×
783
        }
784

785

786
        i++;
38✔
787
    }
38✔
788

789
    // Reset line width to default
790
    glLineWidth(1.0f);
42✔
791
    glUseProgram(0);
42✔
792
}
42✔
793

794
void OpenGLWidget::_drawAnalogSeriesWithGapDetection(std::vector<float> const & data,
38✔
795
                                                     std::shared_ptr<TimeFrame> const & time_frame,
796
                                                     AnalogTimeSeries::TimeValueSpanPair analog_range,
797
                                                     float gap_threshold) {
798

799
    std::vector<GLfloat> segment_vertices;
38✔
800
    auto prev_index = 0;
38✔
801

802
    auto time_begin = analog_range.time_indices.begin();
38✔
803

804
    for (size_t i = 0; i < analog_range.values.size(); i++) {
114✔
805
        auto const xCanvasPos = time_frame->getTimeAtIndex(**time_begin);
76✔
806
        auto const yCanvasPos = analog_range.values[i];
76✔
807

808
        // Check for gap if this isn't the first point
809
        if (prev_index != 0) {
76✔
810

811
            float const time_gap = (**time_begin).getValue() - prev_index;
×
812

813
            if (time_gap > gap_threshold) {
×
814
                // Draw current segment if it has points
815
                if (segment_vertices.size() >= 4) {// At least 2 points (2 floats each)
×
816
                    m_vbo.bind();
×
817
                    m_vbo.allocate(segment_vertices.data(), static_cast<int>(segment_vertices.size() * sizeof(GLfloat)));
×
UNCOV
818
                    m_vbo.release();
×
UNCOV
819
                    glDrawArrays(GL_LINE_STRIP, 0, static_cast<int>(segment_vertices.size() / 4));
×
820
                }
821

822
                // Start new segment
UNCOV
823
                segment_vertices.clear();
×
824
            }
825
        }
826

827
        // Add current point to segment (4D coordinates: x, y, 0, 1)
828
        segment_vertices.push_back(xCanvasPos);
76✔
829
        segment_vertices.push_back(yCanvasPos);
76✔
830
        segment_vertices.push_back(0.0f);  // z coordinate
76✔
831
        segment_vertices.push_back(1.0f);  // w coordinate
76✔
832

833
        prev_index = (**time_begin).getValue();
76✔
834
        ++(*time_begin);
76✔
835
    }
836

837
    // Draw final segment
838
    if (segment_vertices.size() >= 4) {// At least 1 point (4 floats)
38✔
839
        m_vbo.bind();
38✔
840
        m_vbo.allocate(segment_vertices.data(), static_cast<int>(segment_vertices.size() * sizeof(GLfloat)));
38✔
841
        m_vbo.release();
38✔
842

843
        if (segment_vertices.size() >= 8) {// At least 2 points (8 floats)
38✔
844
            glDrawArrays(GL_LINE_STRIP, 0, static_cast<int>(segment_vertices.size() / 4));
38✔
845
        } else {
846
            // Single point - draw as a small marker
UNCOV
847
            glDrawArrays(GL_POINTS, 0, static_cast<int>(segment_vertices.size() / 4));
×
848
        }
849
    }
850
}
76✔
851

UNCOV
852
void OpenGLWidget::_drawAnalogSeriesAsMarkers(std::vector<float> const & data,
×
853
                                              std::shared_ptr<TimeFrame> const & time_frame,
854
                                              AnalogTimeSeries::TimeValueSpanPair analog_range) {
855
    m_vertices.clear();
×
856

857
    auto time_begin = analog_range.time_indices.begin();
×
858

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

UNCOV
861
        auto const xCanvasPos = time_frame->getTimeAtIndex(**time_begin);
×
862
        auto const yCanvasPos = analog_range.values[i];
×
863

864
        m_vertices.push_back(xCanvasPos);
×
865
        m_vertices.push_back(yCanvasPos);
×
UNCOV
866
        m_vertices.push_back(0.0f);  // z coordinate
×
867
        m_vertices.push_back(1.0f);  // w coordinate
×
868

UNCOV
869
        ++(*time_begin);
×
870
    }
871

872
    if (!m_vertices.empty()) {
×
873
        m_vbo.bind();
×
UNCOV
874
        m_vbo.allocate(m_vertices.data(), static_cast<int>(m_vertices.size() * sizeof(GLfloat)));
×
UNCOV
875
        m_vbo.release();
×
876

877
        // Set point size for better visibility
878
        //glPointSize(3.0f);
UNCOV
879
        glDrawArrays(GL_POINTS, 0, static_cast<int>(m_vertices.size() / 4));
×
880
        //glPointSize(1.0f); // Reset to default
881
    }
UNCOV
882
}
×
883

884
///////////////////////////////////////////////////////////////////////////////
885

886
void OpenGLWidget::paintGL() {
42✔
887
    int r, g, b;
42✔
888
    hexToRGB(m_background_color, r, g, b);
42✔
889
    glClearColor(
42✔
890
            static_cast<float>(r) / 255.0f,
42✔
891
            static_cast<float>(g) / 255.0f,
42✔
892
            static_cast<float>(b) / 255.0f, 1.0f);
42✔
893
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
42✔
894

895
    //This has been converted to master coordinates
896
    int const currentTime = _time;
42✔
897

898
    int64_t const zoom = _xAxis.getEnd() - _xAxis.getStart();
42✔
899
    _xAxis.setCenterAndZoom(currentTime, zoom);
42✔
900

901
    // Update Y boundaries based on pan and zoom
902
    _updateYViewBoundaries();
42✔
903

904
    // Draw the series
905
    drawDigitalEventSeries();
42✔
906
    drawDigitalIntervalSeries();
42✔
907
    drawAnalogSeries();
42✔
908

909
    drawAxis();
42✔
910

911
    drawGridLines();
42✔
912

913
    drawDraggedInterval();
42✔
914
    drawNewIntervalBeingCreated();
42✔
915
}
42✔
916

917
void OpenGLWidget::resizeGL(int w, int h) {
13✔
918

919
    static_cast<void>(w);
920
    static_cast<void>(h);
921

922
    // Store the new dimensions
923
    // Note: width() and height() will return the new values after this call
924

925
    // For 2D plotting, we should use orthographic projection
926
    m_proj.setToIdentity();
13✔
927
    m_proj.ortho(-1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f);// Use orthographic projection for 2D plotting
13✔
928

929
    m_view.setToIdentity();
13✔
930
    m_view.translate(0, 0, -1);// Move slightly back for orthographic view
13✔
931

932
    // Trigger a repaint with the new dimensions
933
    update();
13✔
934
}
13✔
935

936
void OpenGLWidget::drawAxis() {
42✔
937

938
    auto axesProgram = ShaderManager::instance().getProgram("axes");
126✔
939
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
42✔
940

941
    glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, m_proj.constData());
42✔
942
    glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, m_view.constData());
42✔
943
    glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, m_model.constData());
42✔
944

945
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
42✔
946
    setupVertexAttribs();
42✔
947

948
    // Get axis color from theme and set uniforms
949
    float r, g, b;
42✔
950
    hexToRGB(m_axis_color, r, g, b);
42✔
951
    glUniform3f(m_colorLoc, r, g, b);
42✔
952
    glUniform1f(m_alphaLoc, 1.0f);
42✔
953

954
    // Draw horizontal line at x=0 with 4D coordinates (x, y, 0, 1)
955
    std::array<GLfloat, 8> lineVertices = {
42✔
956
            0.0f, _yMin, 0.0f, 1.0f,
42✔
957
            0.0f, _yMax, 0.0f, 1.0f};
42✔
958

959
    m_vbo.bind();
42✔
960
    m_vbo.allocate(lineVertices.data(), lineVertices.size() * sizeof(GLfloat));
126✔
961
    m_vbo.release();
42✔
962
    glDrawArrays(GL_LINES, 0, 2);
42✔
963
    glUseProgram(0);
42✔
964
}
84✔
965

966
void OpenGLWidget::addAnalogTimeSeries(
13✔
967
        std::string const & key,
968
        std::shared_ptr<AnalogTimeSeries> series,
969
        std::shared_ptr<TimeFrame> time_frame,
970
        std::string const & color) {
971

972
    auto display_options = std::make_unique<NewAnalogTimeSeriesDisplayOptions>();
13✔
973

974
    // Set color
975
    display_options->hex_color = color.empty() ? TimeSeriesDefaultValues::getColorForIndex(_analog_series.size()) : color;
13✔
976
    display_options->is_visible = true;
13✔
977

978
    // Calculate scale factor based on standard deviation
979
    auto start_time = std::chrono::high_resolution_clock::now();
13✔
980
    setAnalogIntrinsicProperties(series.get(), *display_options);
13✔
981
    auto end_time = std::chrono::high_resolution_clock::now();
13✔
982
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
13✔
983
    std::cout << "Standard deviation calculation took " << duration.count() << " milliseconds" << std::endl;
13✔
984
    display_options->scale_factor = display_options->cached_std_dev * 5.0f;
13✔
985
    display_options->user_scale_factor = 1.0f;// Default user scale
13✔
986

987
    if (time_frame->getTotalFrameCount() / 5 > series->getNumSamples()) {
13✔
UNCOV
988
        display_options->gap_handling = AnalogGapHandling::AlwaysConnect;
×
UNCOV
989
        display_options->enable_gap_detection = false;
×
990

991
    } else {
992
        display_options->enable_gap_detection = true;
13✔
993
        display_options->gap_handling = AnalogGapHandling::DetectGaps;
13✔
994
        display_options->gap_threshold = time_frame->getTotalFrameCount() / 1000;
13✔
995
    }
996

997
    _analog_series[key] = AnalogSeriesData{
65✔
998
            std::move(series),
13✔
999
            std::move(time_frame),
13✔
1000
            std::move(display_options)};
26✔
1001

1002
    updateCanvas(_time);
13✔
1003
}
26✔
1004

1005
void OpenGLWidget::removeAnalogTimeSeries(std::string const & key) {
×
1006
    auto item = _analog_series.find(key);
×
UNCOV
1007
    if (item != _analog_series.end()) {
×
1008
        _analog_series.erase(item);
×
1009
    }
UNCOV
1010
    updateCanvas(_time);
×
1011
}
×
1012

UNCOV
1013
void OpenGLWidget::addDigitalEventSeries(
×
1014
        std::string const & key,
1015
        std::shared_ptr<DigitalEventSeries> series,
1016
        std::shared_ptr<TimeFrame> time_frame,
1017
        std::string const & color) {
1018

UNCOV
1019
    auto display_options = std::make_unique<NewDigitalEventSeriesDisplayOptions>();
×
1020

1021
    // Set color
UNCOV
1022
    display_options->hex_color = color.empty() ? TimeSeriesDefaultValues::getColorForIndex(_digital_event_series.size()) : color;
×
1023
    display_options->is_visible = true;
×
1024

1025
    _digital_event_series[key] = DigitalEventSeriesData{
×
1026
            std::move(series),
×
UNCOV
1027
            std::move(time_frame),
×
1028
            std::move(display_options)};
×
1029

UNCOV
1030
    updateCanvas(_time);
×
1031
}
×
1032

1033
void OpenGLWidget::removeDigitalEventSeries(std::string const & key) {
×
1034
    auto item = _digital_event_series.find(key);
×
UNCOV
1035
    if (item != _digital_event_series.end()) {
×
1036
        _digital_event_series.erase(item);
×
1037
    }
UNCOV
1038
    updateCanvas(_time);
×
1039
}
×
1040

UNCOV
1041
void OpenGLWidget::addDigitalIntervalSeries(
×
1042
        std::string const & key,
1043
        std::shared_ptr<DigitalIntervalSeries> series,
1044
        std::shared_ptr<TimeFrame> time_frame,
1045
        std::string const & color) {
1046

UNCOV
1047
    auto display_options = std::make_unique<NewDigitalIntervalSeriesDisplayOptions>();
×
1048

1049
    // Set color
UNCOV
1050
    display_options->hex_color = color.empty() ? TimeSeriesDefaultValues::getColorForIndex(_digital_interval_series.size()) : color;
×
1051
    display_options->is_visible = true;
×
1052

1053
    _digital_interval_series[key] = DigitalIntervalSeriesData{
×
1054
            std::move(series),
×
UNCOV
1055
            std::move(time_frame),
×
1056
            std::move(display_options)};
×
1057

UNCOV
1058
    updateCanvas(_time);
×
1059
}
×
1060

1061
void OpenGLWidget::removeDigitalIntervalSeries(std::string const & key) {
×
1062
    auto item = _digital_interval_series.find(key);
×
UNCOV
1063
    if (item != _digital_interval_series.end()) {
×
1064
        _digital_interval_series.erase(item);
×
1065
    }
UNCOV
1066
    updateCanvas(_time);
×
1067
}
×
1068

UNCOV
1069
void OpenGLWidget::_addSeries(std::string const & key) {
×
1070
    static_cast<void>(key);
1071
    //auto item = _series_y_position.find(key);
1072
}
×
1073

1074
void OpenGLWidget::_removeSeries(std::string const & key) {
×
1075
    auto item = _series_y_position.find(key);
×
UNCOV
1076
    if (item != _series_y_position.end()) {
×
1077
        _series_y_position.erase(item);
×
1078
    }
1079
}
×
1080

1081
void OpenGLWidget::clearSeries() {
×
1082
    _analog_series.clear();
×
UNCOV
1083
    updateCanvas(_time);
×
1084
}
×
1085

1086
void OpenGLWidget::drawDashedLine(LineParameters const & params) {
×
1087

UNCOV
1088
    auto dashedProgram = ShaderManager::instance().getProgram("dashed_line");
×
1089
    if (dashedProgram) glUseProgram(dashedProgram->getProgramId());
×
1090

1091
    glUniformMatrix4fv(m_dashedProjMatrixLoc, 1, GL_FALSE, (m_proj * m_view * m_model).constData());
×
1092
    std::array<GLfloat, 2> hw = {static_cast<float>(width()), static_cast<float>(height())};
×
1093
    glUniform2fv(m_dashedResolutionLoc, 1, hw.data());
×
UNCOV
1094
    glUniform1f(m_dashedDashSizeLoc, params.dashLength);
×
1095
    glUniform1f(m_dashedGapSizeLoc, params.gapLength);
×
1096

UNCOV
1097
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
×
1098

1099
    // Pass 3D coordinates (z=0) to match the vertex shader input format
1100
    std::array<float, 6> vertices = {
×
UNCOV
1101
            params.xStart, params.yStart, 0.0f,
×
1102
            params.xEnd, params.yEnd, 0.0f};
×
1103

UNCOV
1104
    m_vbo.bind();
×
1105
    m_vbo.allocate(vertices.data(), vertices.size() * sizeof(float));
×
1106

UNCOV
1107
    glEnableVertexAttribArray(0);
×
1108
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), nullptr);
×
1109

1110
    glDrawArrays(GL_LINES, 0, 2);
×
1111

1112
    m_vbo.release();
×
1113

UNCOV
1114
    glUseProgram(0);
×
UNCOV
1115
}
×
1116

1117
void OpenGLWidget::drawGridLines() {
42✔
1118
    if (!_grid_lines_enabled) {
42✔
1119
        return;// Grid lines are disabled
42✔
1120
    }
1121

UNCOV
1122
    auto const start_time = _xAxis.getStart();
×
UNCOV
1123
    auto const end_time = _xAxis.getEnd();
×
1124

1125
    // Calculate the range of time values
UNCOV
1126
    auto const time_range = end_time - start_time;
×
1127

1128
    // Avoid drawing grid lines if the range is too small or invalid
UNCOV
1129
    if (time_range <= 0 || _grid_spacing <= 0) {
×
UNCOV
1130
        return;
×
1131
    }
1132

1133
    // Find the first grid line position that's >= start_time
1134
    // Use integer division to ensure proper alignment
1135
    int64_t first_grid_time = ((start_time / _grid_spacing) * _grid_spacing);
×
UNCOV
1136
    if (first_grid_time < start_time) {
×
UNCOV
1137
        first_grid_time += _grid_spacing;
×
1138
    }
1139

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

1145
        // Skip grid lines that are outside the visible range due to floating point precision
UNCOV
1146
        if (normalized_x < -1.0f || normalized_x > 1.0f) {
×
UNCOV
1147
            continue;
×
1148
        }
1149

1150
        LineParameters gridLine;
×
1151
        gridLine.xStart = normalized_x;
×
1152
        gridLine.xEnd = normalized_x;
×
1153
        gridLine.yStart = _yMin;
×
1154
        gridLine.yEnd = _yMax;
×
UNCOV
1155
        gridLine.dashLength = 3.0f;// Shorter dashes for grid lines
×
1156
        gridLine.gapLength = 3.0f; // Shorter gaps for grid lines
×
1157

UNCOV
1158
        drawDashedLine(gridLine);
×
1159
    }
1160
}
1161

1162
void OpenGLWidget::_updateYViewBoundaries() {
42✔
1163
    /*
1164
    float viewHeight = 2.0f;
1165

1166
    // Calculate center point (adjusted by vertical pan)
1167
    float centerY = _verticalPanOffset;
1168
    
1169
    // Calculate min and max values
1170
    _yMin = centerY - (viewHeight / 2.0f);
1171
    _yMax = centerY + (viewHeight / 2.0f);
1172
     */
1173
}
42✔
1174

1175
float OpenGLWidget::canvasXToTime(float canvas_x) const {
×
1176
    // Convert canvas pixel coordinate to time coordinate
UNCOV
1177
    float const canvas_width = static_cast<float>(width());
×
1178
    float const normalized_x = canvas_x / canvas_width;// 0.0 to 1.0
×
1179

UNCOV
1180
    auto const start_time = static_cast<float>(_xAxis.getStart());
×
1181
    auto const end_time = static_cast<float>(_xAxis.getEnd());
×
1182

UNCOV
1183
    return start_time + normalized_x * (end_time - start_time);
×
1184
}
1185

1186
float OpenGLWidget::canvasYToAnalogValue(float canvas_y, std::string const & series_key) const {
×
1187
    // Get canvas dimensions
UNCOV
1188
    auto [canvas_width, canvas_height] = getCanvasSize();
×
1189

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

1193
    // Convert to view coordinates using current viewport bounds (accounting for pan offset)
1194
    float const dynamic_min_y = _yMin + _verticalPanOffset;
×
UNCOV
1195
    float const dynamic_max_y = _yMax + _verticalPanOffset;
×
UNCOV
1196
    float const view_y = dynamic_min_y + normalized_y * (dynamic_max_y - dynamic_min_y);
×
1197

1198
    // Find the series configuration to get positioning info
1199
    auto const analog_it = _analog_series.find(series_key);
×
UNCOV
1200
    if (analog_it == _analog_series.end()) {
×
UNCOV
1201
        return 0.0f;// Series not found
×
1202
    }
1203

UNCOV
1204
    auto const & display_options = analog_it->second.display_options;
×
1205

1206
    // Check if this series uses VerticalSpaceManager positioning
1207
    if (display_options->y_offset != 0.0f && display_options->allocated_height > 0.0f) {
×
1208
        // VerticalSpaceManager mode: series is positioned at y_offset with its own scaling
UNCOV
1209
        float const series_local_y = view_y - display_options->y_offset;
×
1210

1211
        // Apply inverse of the scaling used in rendering to get actual analog value
1212
        auto const series = analog_it->second.series;
×
UNCOV
1213
        auto const stdDev = getCachedStdDev(*series, *display_options);
×
UNCOV
1214
        auto const user_scale_combined = display_options->user_scale_factor * _global_zoom;
×
1215

1216
        // Calculate the same scaling factors used in rendering
1217
        float const usable_height = display_options->allocated_height * 0.8f;
×
1218
        float const base_amplitude_scale = 1.0f / stdDev;
×
UNCOV
1219
        float const height_scale = usable_height / 1.0f;
×
UNCOV
1220
        float const amplitude_scale = base_amplitude_scale * height_scale * user_scale_combined;
×
1221

1222
        // Apply inverse scaling to get actual data value
UNCOV
1223
        return series_local_y / amplitude_scale;
×
1224
    } else {
×
1225
        // Legacy mode: use corrected calculation
UNCOV
1226
        float const adjusted_y = view_y;// No pan offset adjustment needed since it's in projection
×
1227

1228
        // Calculate series center and scaling for legacy mode
UNCOV
1229
        auto series_pos_it = _series_y_position.find(series_key);
×
1230
        if (series_pos_it == _series_y_position.end()) {
×
1231
            // Series not found in position map, return 0 as fallback
1232
            return 0.0f;
×
1233
        }
1234
        int const series_index = series_pos_it->second;
×
UNCOV
1235
        float const center_coord = -0.5f * _ySpacing * (static_cast<float>(_analog_series.size() - 1));
×
1236
        float const series_y_offset = static_cast<float>(series_index) * _ySpacing + center_coord;
×
1237

UNCOV
1238
        float const relative_y = adjusted_y - series_y_offset;
×
1239

1240
        // Use corrected scaling calculation
1241
        auto const series = analog_it->second.series;
×
1242
        auto const stdDev = getCachedStdDev(*series, *display_options);
×
UNCOV
1243
        auto const user_scale_combined = display_options->user_scale_factor * _global_zoom;
×
1244
        float const legacy_amplitude_scale = (1.0f / stdDev) * user_scale_combined;
×
1245

UNCOV
1246
        return relative_y / legacy_amplitude_scale;
×
UNCOV
1247
    }
×
1248
}
1249

1250
// Interval selection methods
1251
void OpenGLWidget::setSelectedInterval(std::string const & series_key, int64_t start_time, int64_t end_time) {
×
1252
    _selected_intervals[series_key] = std::make_pair(start_time, end_time);
×
UNCOV
1253
    updateCanvas(_time);
×
1254
}
×
1255

1256
void OpenGLWidget::clearSelectedInterval(std::string const & series_key) {
×
1257
    auto it = _selected_intervals.find(series_key);
×
1258
    if (it != _selected_intervals.end()) {
×
UNCOV
1259
        _selected_intervals.erase(it);
×
1260
        updateCanvas(_time);
×
1261
    }
1262
}
×
1263

1264
std::optional<std::pair<int64_t, int64_t>> OpenGLWidget::getSelectedInterval(std::string const & series_key) const {
×
1265
    auto it = _selected_intervals.find(series_key);
×
UNCOV
1266
    if (it != _selected_intervals.end()) {
×
1267
        return it->second;
×
1268
    }
UNCOV
1269
    return std::nullopt;
×
1270
}
1271

1272
std::optional<std::pair<int64_t, int64_t>> OpenGLWidget::findIntervalAtTime(std::string const & series_key, float time_coord) const {
×
1273
    auto it = _digital_interval_series.find(series_key);
×
UNCOV
1274
    if (it == _digital_interval_series.end()) {
×
UNCOV
1275
        return std::nullopt;
×
1276
    }
1277

1278
    auto const & interval_data = it->second;
×
UNCOV
1279
    auto const & series = interval_data.series;
×
UNCOV
1280
    auto const & time_frame = interval_data.time_frame;
×
1281

1282
    // Convert time coordinate from master time frame to series time frame
1283
    int64_t query_time_index;
1284
    if (time_frame.get() == _master_time_frame.get()) {
×
1285
        // Same time frame - use time coordinate directly
UNCOV
1286
        query_time_index = static_cast<int64_t>(std::round(time_coord));
×
1287
    } else {
1288
        // Different time frame - convert master time to series time frame index
UNCOV
1289
        query_time_index = time_frame->getIndexAtTime(time_coord).getValue();
×
1290
    }
1291

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

1295
    if (!intervals.empty()) {
×
1296
        // Return the first interval found, converted to master time frame coordinates
UNCOV
1297
        auto const & interval = intervals.front();
×
1298
        int64_t interval_start_master, interval_end_master;
×
1299

1300
        if (time_frame.get() == _master_time_frame.get()) {
×
1301
            // Same time frame - use indices directly as time coordinates
UNCOV
1302
            interval_start_master = interval.start;
×
UNCOV
1303
            interval_end_master = interval.end;
×
1304
        } else {
1305
            // Convert series indices to master time frame coordinates
UNCOV
1306
            interval_start_master = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.start)));
×
UNCOV
1307
            interval_end_master = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.end)));
×
1308
        }
1309

UNCOV
1310
        return std::make_pair(interval_start_master, interval_end_master);
×
1311
    }
1312

UNCOV
1313
    return std::nullopt;
×
1314
}
1315

1316
// Interval edge dragging methods
UNCOV
1317
std::optional<std::pair<std::string, bool>> OpenGLWidget::findIntervalEdgeAtPosition(float canvas_x, float canvas_y) const {
×
1318

1319
    static_cast<void>(canvas_y);
1320

UNCOV
1321
    float const time_coord = canvasXToTime(canvas_x);
×
UNCOV
1322
    constexpr float EDGE_TOLERANCE_PX = 10.0f;
×
1323

1324
    // Only check selected intervals
UNCOV
1325
    for (auto const & [series_key, interval_bounds]: _selected_intervals) {
×
UNCOV
1326
        auto const [start_time, end_time] = interval_bounds;
×
1327

1328
        // Convert interval bounds to canvas coordinates
UNCOV
1329
        auto const start_time_f = static_cast<float>(start_time);
×
UNCOV
1330
        auto const end_time_f = static_cast<float>(end_time);
×
1331

1332
        // Check if we're within the interval's time range (with some tolerance)
1333
        if (time_coord >= start_time_f - EDGE_TOLERANCE_PX && time_coord <= end_time_f + EDGE_TOLERANCE_PX) {
×
1334
            // Convert time coordinates to canvas X positions for pixel-based tolerance
1335
            float const canvas_width = static_cast<float>(width());
×
UNCOV
1336
            auto const start_time_canvas = static_cast<float>(_xAxis.getStart());
×
1337
            auto const end_time_canvas = static_cast<float>(_xAxis.getEnd());
×
1338

UNCOV
1339
            float const start_canvas_x = (start_time_f - start_time_canvas) / (end_time_canvas - start_time_canvas) * canvas_width;
×
UNCOV
1340
            float const end_canvas_x = (end_time_f - start_time_canvas) / (end_time_canvas - start_time_canvas) * canvas_width;
×
1341

1342
            // Check if we're close to the left edge
UNCOV
1343
            if (std::abs(canvas_x - start_canvas_x) <= EDGE_TOLERANCE_PX) {
×
UNCOV
1344
                return std::make_pair(series_key, true);// true = left edge
×
1345
            }
1346

1347
            // Check if we're close to the right edge
UNCOV
1348
            if (std::abs(canvas_x - end_canvas_x) <= EDGE_TOLERANCE_PX) {
×
UNCOV
1349
                return std::make_pair(series_key, false);// false = right edge
×
1350
            }
1351
        }
1352
    }
1353

UNCOV
1354
    return std::nullopt;
×
1355
}
1356

1357
void OpenGLWidget::startIntervalDrag(std::string const & series_key, bool is_left_edge, QPoint const & start_pos) {
×
1358
    auto selected_interval = getSelectedInterval(series_key);
×
UNCOV
1359
    if (!selected_interval.has_value()) {
×
UNCOV
1360
        return;
×
1361
    }
1362

1363
    auto const [start_time, end_time] = selected_interval.value();
×
1364

1365
    _is_dragging_interval = true;
×
1366
    _dragging_series_key = series_key;
×
1367
    _dragging_left_edge = is_left_edge;
×
1368
    _original_start_time = start_time;
×
1369
    _original_end_time = end_time;
×
1370
    _dragged_start_time = start_time;
×
UNCOV
1371
    _dragged_end_time = end_time;
×
UNCOV
1372
    _drag_start_pos = start_pos;
×
1373

1374
    // Disable normal mouse interactions during drag
UNCOV
1375
    setCursor(Qt::SizeHorCursor);
×
1376

1377
    std::cout << "Started dragging " << (is_left_edge ? "left" : "right")
UNCOV
1378
              << " edge of interval [" << start_time << ", " << end_time << "]" << std::endl;
×
1379
}
1380

1381
void OpenGLWidget::updateIntervalDrag(QPoint const & current_pos) {
×
UNCOV
1382
    if (!_is_dragging_interval) {
×
UNCOV
1383
        return;
×
1384
    }
1385

1386
    // Convert mouse position to time coordinate (in master time frame)
UNCOV
1387
    float const current_time_master = canvasXToTime(static_cast<float>(current_pos.x()));
×
1388

1389
    // Get the series data
UNCOV
1390
    auto it = _digital_interval_series.find(_dragging_series_key);
×
1391
    if (it == _digital_interval_series.end()) {
×
1392
        // Series not found - abort drag
UNCOV
1393
        cancelIntervalDrag();
×
UNCOV
1394
        return;
×
1395
    }
1396

UNCOV
1397
    auto const & series = it->second.series;
×
UNCOV
1398
    auto const & time_frame = it->second.time_frame;
×
1399

1400
    // Convert master time coordinate to series time frame index
UNCOV
1401
    int64_t current_time_series_index;
×
1402
    if (time_frame.get() == _master_time_frame.get()) {
×
1403
        // Same time frame - use time coordinate directly
UNCOV
1404
        current_time_series_index = static_cast<int64_t>(std::round(current_time_master));
×
1405
    } else {
1406
        // Different time frame - convert master time to series time frame index
UNCOV
1407
        current_time_series_index = time_frame->getIndexAtTime(current_time_master).getValue();
×
1408
    }
1409

1410
    // Convert original interval bounds to series time frame for constraints
1411
    int64_t original_start_series, original_end_series;
1412
    if (time_frame.get() == _master_time_frame.get()) {
×
1413
        // Same time frame
UNCOV
1414
        original_start_series = _original_start_time;
×
UNCOV
1415
        original_end_series = _original_end_time;
×
1416
    } else {
1417
        // Convert master time coordinates to series time frame indices
UNCOV
1418
        original_start_series = time_frame->getIndexAtTime(static_cast<float>(_original_start_time)).getValue();
×
UNCOV
1419
        original_end_series = time_frame->getIndexAtTime(static_cast<float>(_original_end_time)).getValue();
×
1420
    }
1421

1422
    // Perform dragging logic in series time frame
1423
    int64_t new_start_series, new_end_series;
1424

1425
    if (_dragging_left_edge) {
×
1426
        // Dragging left edge - constrain to not pass right edge
UNCOV
1427
        new_start_series = std::min(current_time_series_index, original_end_series - 1);
×
UNCOV
1428
        new_end_series = original_end_series;
×
1429

1430
        // Check for collision with other intervals in series time frame
UNCOV
1431
        auto overlapping_intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(
×
1432
                new_start_series, new_start_series);
×
1433

1434
        for (auto const & interval: overlapping_intervals) {
×
1435
            // Skip the interval we're currently editing
UNCOV
1436
            if (interval.start == original_start_series && interval.end == original_end_series) {
×
UNCOV
1437
                continue;
×
1438
            }
1439

1440
            // If we would overlap with another interval, stop 1 index after it
1441
            if (new_start_series <= interval.end) {
×
UNCOV
1442
                new_start_series = interval.end + 1;
×
UNCOV
1443
                break;
×
1444
            }
1445
        }
1446
    } else {
1447
        // Dragging right edge - constrain to not pass left edge
UNCOV
1448
        new_start_series = original_start_series;
×
UNCOV
1449
        new_end_series = std::max(current_time_series_index, original_start_series + 1);
×
1450

1451
        // Check for collision with other intervals in series time frame
UNCOV
1452
        auto overlapping_intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(
×
1453
                new_end_series, new_end_series);
×
1454

1455
        for (auto const & interval: overlapping_intervals) {
×
1456
            // Skip the interval we're currently editing
UNCOV
1457
            if (interval.start == original_start_series && interval.end == original_end_series) {
×
UNCOV
1458
                continue;
×
1459
            }
1460

1461
            // If we would overlap with another interval, stop 1 index before it
1462
            if (new_end_series >= interval.start) {
×
UNCOV
1463
                new_end_series = interval.start - 1;
×
UNCOV
1464
                break;
×
1465
            }
1466
        }
1467
    }
1468

1469
    // Validate the new interval bounds
1470
    if (new_start_series >= new_end_series) {
×
1471
        // Invalid bounds - keep current drag state
UNCOV
1472
        return;
×
1473
    }
1474

1475
    // Convert back to master time frame for display
1476
    if (time_frame.get() == _master_time_frame.get()) {
×
1477
        // Same time frame
UNCOV
1478
        _dragged_start_time = new_start_series;
×
UNCOV
1479
        _dragged_end_time = new_end_series;
×
1480
    } else {
1481
        // Convert series indices back to master time coordinates
1482
        try {
1483
            _dragged_start_time = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(new_start_series)));
×
UNCOV
1484
            _dragged_end_time = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(new_end_series)));
×
1485
        } catch (...) {
×
1486
            // Conversion failed - abort drag
1487
            cancelIntervalDrag();
×
UNCOV
1488
            return;
×
UNCOV
1489
        }
×
1490
    }
1491

1492
    // Trigger redraw to show the dragged interval
UNCOV
1493
    updateCanvas(_time);
×
1494
}
1495

1496
void OpenGLWidget::finishIntervalDrag() {
×
UNCOV
1497
    if (!_is_dragging_interval) {
×
UNCOV
1498
        return;
×
1499
    }
1500

1501
    // Get the series data
UNCOV
1502
    auto it = _digital_interval_series.find(_dragging_series_key);
×
1503
    if (it == _digital_interval_series.end()) {
×
1504
        // Series not found - abort drag
UNCOV
1505
        cancelIntervalDrag();
×
UNCOV
1506
        return;
×
1507
    }
1508

UNCOV
1509
    auto const & series = it->second.series;
×
UNCOV
1510
    auto const & time_frame = it->second.time_frame;
×
1511

1512
    try {
1513
        // Convert all coordinates to series time frame for data operations
1514
        int64_t original_start_series, original_end_series, new_start_series, new_end_series;
1515

1516
        if (time_frame.get() == _master_time_frame.get()) {
×
1517
            // Same time frame - use coordinates directly
1518
            original_start_series = _original_start_time;
×
1519
            original_end_series = _original_end_time;
×
UNCOV
1520
            new_start_series = _dragged_start_time;
×
UNCOV
1521
            new_end_series = _dragged_end_time;
×
1522
        } else {
1523
            // Convert master time coordinates to series time frame indices
1524
            original_start_series = time_frame->getIndexAtTime(static_cast<float>(_original_start_time)).getValue();
×
1525
            original_end_series = time_frame->getIndexAtTime(static_cast<float>(_original_end_time)).getValue();
×
UNCOV
1526
            new_start_series = time_frame->getIndexAtTime(static_cast<float>(_dragged_start_time)).getValue();
×
UNCOV
1527
            new_end_series = time_frame->getIndexAtTime(static_cast<float>(_dragged_end_time)).getValue();
×
1528
        }
1529

1530
        // Validate converted coordinates
1531
        if (new_start_series >= new_end_series ||
×
UNCOV
1532
            new_start_series < 0 || new_end_series < 0) {
×
UNCOV
1533
            throw std::runtime_error("Invalid interval bounds after conversion");
×
1534
        }
1535

1536
        // Update the interval data in the series' native time frame
1537
        // First, remove the original interval completely
UNCOV
1538
        for (int64_t time = original_start_series; time <= original_end_series; ++time) {
×
UNCOV
1539
            series->setEventAtTime(TimeFrameIndex(time), false);
×
1540
        }
1541

1542
        // Add the new interval
UNCOV
1543
        series->addEvent(TimeFrameIndex(new_start_series), TimeFrameIndex(new_end_series));
×
1544

1545
        // Update the selection to the new interval (stored in master time frame coordinates)
1546
        setSelectedInterval(_dragging_series_key, _dragged_start_time, _dragged_end_time);
×
1547

1548
        std::cout << "Finished dragging interval. Original: ["
×
UNCOV
1549
                  << _original_start_time << ", " << _original_end_time
×
1550
                  << "] -> New: [" << _dragged_start_time << ", " << _dragged_end_time << "]" << std::endl;
×
1551

1552
    } catch (...) {
×
1553
        // Error occurred during conversion or data update - abort drag
1554
        std::cout << "Error during interval drag completion - keeping original interval" << std::endl;
×
1555
        cancelIntervalDrag();
×
UNCOV
1556
        return;
×
UNCOV
1557
    }
×
1558

1559
    // Reset drag state
1560
    _is_dragging_interval = false;
×
UNCOV
1561
    _dragging_series_key.clear();
×
UNCOV
1562
    setCursor(Qt::ArrowCursor);
×
1563

1564
    // Trigger final redraw
UNCOV
1565
    updateCanvas(_time);
×
1566
}
1567

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

UNCOV
1573
    std::cout << "Cancelled interval drag" << std::endl;
×
1574

1575
    // Reset drag state without applying changes
1576
    _is_dragging_interval = false;
×
UNCOV
1577
    _dragging_series_key.clear();
×
UNCOV
1578
    setCursor(Qt::ArrowCursor);
×
1579

1580
    // Trigger redraw to remove the dragged interval visualization
UNCOV
1581
    updateCanvas(_time);
×
1582
}
1583

1584
void OpenGLWidget::drawDraggedInterval() {
42✔
1585
    if (!_is_dragging_interval) {
42✔
1586
        return;
42✔
1587
    }
1588

1589
    // Get the series data for rendering
1590
    auto it = _digital_interval_series.find(_dragging_series_key);
×
UNCOV
1591
    if (it == _digital_interval_series.end()) {
×
UNCOV
1592
        return;
×
1593
    }
1594

1595
    auto const & display_options = it->second.display_options;
×
1596

1597
    auto const start_time = static_cast<float>(_xAxis.getStart());
×
1598
    auto const end_time = static_cast<float>(_xAxis.getEnd());
×
UNCOV
1599
    auto const min_y = _yMin;
×
UNCOV
1600
    auto const max_y = _yMax;
×
1601

1602
    // Check if the dragged interval is visible
UNCOV
1603
    if (_dragged_end_time < static_cast<int64_t>(start_time) || _dragged_start_time > static_cast<int64_t>(end_time)) {
×
UNCOV
1604
        return;
×
1605
    }
1606

UNCOV
1607
    auto axesProgram = ShaderManager::instance().getProgram("axes");
×
1608
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
×
1609

UNCOV
1610
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
×
UNCOV
1611
    setupVertexAttribs();
×
1612

1613
    // Set up matrices (same as normal interval rendering)
1614
    auto Model = glm::mat4(1.0f);
×
UNCOV
1615
    auto View = glm::mat4(1.0f);
×
1616
    auto Projection = glm::ortho(start_time, end_time, min_y, max_y);
×
1617

1618
    glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
×
UNCOV
1619
    glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
×
UNCOV
1620
    glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
×
1621

1622
    // Get colors
1623
    int r, g, b;
×
1624
    hexToRGB(display_options->hex_color, r, g, b);
×
1625
    float const rNorm = static_cast<float>(r) / 255.0f;
×
UNCOV
1626
    float const gNorm = static_cast<float>(g) / 255.0f;
×
UNCOV
1627
    float const bNorm = static_cast<float>(b) / 255.0f;
×
1628

1629
    // Clip the dragged interval to visible range
UNCOV
1630
    float const dragged_start = std::max(static_cast<float>(_dragged_start_time), start_time);
×
UNCOV
1631
    float const dragged_end = std::min(static_cast<float>(_dragged_end_time), end_time);
×
1632

1633
    // Draw the original interval dimmed (alpha = 0.2)
UNCOV
1634
    float const original_start = std::max(static_cast<float>(_original_start_time), start_time);
×
UNCOV
1635
    float const original_end = std::min(static_cast<float>(_original_end_time), end_time);
×
1636

1637
    // Set color and alpha uniforms for original interval (dimmed)
UNCOV
1638
    glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
×
UNCOV
1639
    glUniform1f(m_alphaLoc, 0.2f);
×
1640

1641
    // Create 4D vertices (x, y, 0, 1) to match the shader expectations
UNCOV
1642
    std::array<GLfloat, 16> original_vertices = {
×
1643
            original_start, min_y, 0.0f, 1.0f,
1644
            original_end, min_y, 0.0f, 1.0f,
1645
            original_end, max_y, 0.0f, 1.0f,
1646
            original_start, max_y, 0.0f, 1.0f};
×
1647

1648
    m_vbo.bind();
×
1649
    m_vbo.allocate(original_vertices.data(), original_vertices.size() * sizeof(GLfloat));
×
UNCOV
1650
    m_vbo.release();
×
UNCOV
1651
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
×
1652

1653
    // Set color and alpha uniforms for dragged interval (semi-transparent)
UNCOV
1654
    glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
×
UNCOV
1655
    glUniform1f(m_alphaLoc, 0.8f);
×
1656

1657
    // Create 4D vertices (x, y, 0, 1) to match the shader expectations
UNCOV
1658
    std::array<GLfloat, 16> dragged_vertices = {
×
1659
            dragged_start, min_y, 0.0f, 1.0f,
1660
            dragged_end, min_y, 0.0f, 1.0f,
1661
            dragged_end, max_y, 0.0f, 1.0f,
1662
            dragged_start, max_y, 0.0f, 1.0f};
×
1663

1664
    m_vbo.bind();
×
1665
    m_vbo.allocate(dragged_vertices.data(), dragged_vertices.size() * sizeof(GLfloat));
×
UNCOV
1666
    m_vbo.release();
×
1667
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
×
1668

UNCOV
1669
    glUseProgram(0);
×
UNCOV
1670
}
×
1671

1672
namespace TimeSeriesDefaultValues {
1673
std::string getColorForIndex(size_t index) {
×
UNCOV
1674
    if (index < DEFAULT_COLORS.size()) {
×
1675
        return DEFAULT_COLORS[index];
×
1676
    } else {
UNCOV
1677
        return generateRandomColor();
×
1678
    }
1679
}
1680
}// namespace TimeSeriesDefaultValues
1681

UNCOV
1682
void OpenGLWidget::mouseDoubleClickEvent(QMouseEvent * event) {
×
UNCOV
1683
    if (event->button() == Qt::LeftButton) {
×
1684
        // Check if we're double-clicking over a digital interval series
1685
        //float const canvas_x = static_cast<float>(event->pos().x());
1686
        //float const canvas_y = static_cast<float>(event->pos().y());
1687

1688
        // Find which digital interval series (if any) is at this Y position
1689
        // For now, use the first visible digital interval series
1690
        // TODO: Improve this to detect which series based on Y coordinate
1691
        for (auto const & [series_key, data]: _digital_interval_series) {
×
1692
            if (data.display_options->is_visible) {
×
UNCOV
1693
                startNewIntervalCreation(series_key, event->pos());
×
UNCOV
1694
                return;
×
1695
            }
1696
        }
1697
    }
1698

UNCOV
1699
    QOpenGLWidget::mouseDoubleClickEvent(event);
×
1700
}
1701

1702
void OpenGLWidget::startNewIntervalCreation(std::string const & series_key, QPoint const & start_pos) {
×
1703
    // Don't start if we're already dragging an interval
UNCOV
1704
    if (_is_dragging_interval || _is_creating_new_interval) {
×
UNCOV
1705
        return;
×
1706
    }
1707

1708
    // Check if the series exists
1709
    auto it = _digital_interval_series.find(series_key);
×
UNCOV
1710
    if (it == _digital_interval_series.end()) {
×
UNCOV
1711
        return;
×
1712
    }
1713

1714
    _is_creating_new_interval = true;
×
UNCOV
1715
    _new_interval_series_key = series_key;
×
UNCOV
1716
    _new_interval_click_pos = start_pos;
×
1717

1718
    // Convert click position to time coordinate (in master time frame)
UNCOV
1719
    float const click_time_master = canvasXToTime(static_cast<float>(start_pos.x()));
×
UNCOV
1720
    _new_interval_click_time = static_cast<int64_t>(std::round(click_time_master));
×
1721

1722
    // Initialize start and end to the click position
UNCOV
1723
    _new_interval_start_time = _new_interval_click_time;
×
UNCOV
1724
    _new_interval_end_time = _new_interval_click_time;
×
1725

1726
    // Set cursor to indicate creation mode
UNCOV
1727
    setCursor(Qt::SizeHorCursor);
×
1728

1729
    std::cout << "Started new interval creation for series " << series_key
UNCOV
1730
              << " at time " << _new_interval_click_time << std::endl;
×
1731
}
1732

1733
void OpenGLWidget::updateNewIntervalCreation(QPoint const & current_pos) {
×
UNCOV
1734
    if (!_is_creating_new_interval) {
×
UNCOV
1735
        return;
×
1736
    }
1737

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

1742
    // Get the series data for constraint checking
UNCOV
1743
    auto it = _digital_interval_series.find(_new_interval_series_key);
×
1744
    if (it == _digital_interval_series.end()) {
×
1745
        // Series not found - abort creation
UNCOV
1746
        cancelNewIntervalCreation();
×
UNCOV
1747
        return;
×
1748
    }
1749

UNCOV
1750
    auto const & series = it->second.series;
×
UNCOV
1751
    auto const & time_frame = it->second.time_frame;
×
1752

1753
    // Convert coordinates to series time frame for collision detection
UNCOV
1754
    int64_t click_time_series, current_time_series;
×
1755
    if (time_frame.get() == _master_time_frame.get()) {
×
1756
        // Same time frame - use coordinates directly
UNCOV
1757
        click_time_series = _new_interval_click_time;
×
UNCOV
1758
        current_time_series = current_time_coord;
×
1759
    } else {
1760
        // Convert master time coordinates to series time frame indices
UNCOV
1761
        click_time_series = time_frame->getIndexAtTime(static_cast<float>(_new_interval_click_time)).getValue();
×
UNCOV
1762
        current_time_series = time_frame->getIndexAtTime(static_cast<float>(current_time_coord)).getValue();
×
1763
    }
1764

1765
    // Determine interval bounds (always ensure start < end)
UNCOV
1766
    int64_t new_start_series = std::min(click_time_series, current_time_series);
×
UNCOV
1767
    int64_t new_end_series = std::max(click_time_series, current_time_series);
×
1768

1769
    // Ensure minimum interval size of 1
1770
    if (new_start_series == new_end_series) {
×
UNCOV
1771
        if (current_time_series >= click_time_series) {
×
1772
            new_end_series = new_start_series + 1;
×
1773
        } else {
UNCOV
1774
            new_start_series = new_end_series - 1;
×
1775
        }
1776
    }
1777

1778
    // Check for collision with existing intervals in series time frame
UNCOV
1779
    auto overlapping_intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(
×
UNCOV
1780
            new_start_series, new_end_series);
×
1781

1782
    // If there are overlapping intervals, constrain the new interval
UNCOV
1783
    for (auto const & interval: overlapping_intervals) {
×
1784
        if (current_time_series >= click_time_series) {
×
1785
            // Dragging right - stop before the first overlapping interval
1786
            if (new_end_series >= interval.start) {
×
1787
                new_end_series = interval.start - 1;
×
UNCOV
1788
                if (new_end_series <= new_start_series) {
×
1789
                    new_end_series = new_start_series + 1;
×
1790
                }
UNCOV
1791
                break;
×
1792
            }
1793
        } else {
1794
            // Dragging left - stop after the last overlapping interval
1795
            if (new_start_series <= interval.end) {
×
1796
                new_start_series = interval.end + 1;
×
UNCOV
1797
                if (new_start_series >= new_end_series) {
×
1798
                    new_start_series = new_end_series - 1;
×
1799
                }
UNCOV
1800
                break;
×
1801
            }
1802
        }
1803
    }
1804

1805
    // Convert back to master time frame for display
1806
    if (time_frame.get() == _master_time_frame.get()) {
×
1807
        // Same time frame
UNCOV
1808
        _new_interval_start_time = new_start_series;
×
UNCOV
1809
        _new_interval_end_time = new_end_series;
×
1810
    } else {
1811
        // Convert series indices back to master time coordinates
1812
        try {
1813
            _new_interval_start_time = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(new_start_series)));
×
UNCOV
1814
            _new_interval_end_time = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(new_end_series)));
×
1815
        } catch (...) {
×
1816
            // Conversion failed - abort creation
1817
            cancelNewIntervalCreation();
×
UNCOV
1818
            return;
×
UNCOV
1819
        }
×
1820
    }
1821

1822
    // Trigger redraw to show the new interval being created
UNCOV
1823
    updateCanvas(_time);
×
1824
}
1825

1826
void OpenGLWidget::finishNewIntervalCreation() {
×
UNCOV
1827
    if (!_is_creating_new_interval) {
×
UNCOV
1828
        return;
×
1829
    }
1830

1831
    // Get the series data
UNCOV
1832
    auto it = _digital_interval_series.find(_new_interval_series_key);
×
1833
    if (it == _digital_interval_series.end()) {
×
1834
        // Series not found - abort creation
UNCOV
1835
        cancelNewIntervalCreation();
×
UNCOV
1836
        return;
×
1837
    }
1838

UNCOV
1839
    auto const & series = it->second.series;
×
UNCOV
1840
    auto const & time_frame = it->second.time_frame;
×
1841

1842
    try {
1843
        // Convert coordinates to series time frame for data operations
1844
        int64_t new_start_series, new_end_series;
1845

1846
        if (time_frame.get() == _master_time_frame.get()) {
×
1847
            // Same time frame - use coordinates directly
UNCOV
1848
            new_start_series = _new_interval_start_time;
×
UNCOV
1849
            new_end_series = _new_interval_end_time;
×
1850
        } else {
1851
            // Convert master time coordinates to series time frame indices
UNCOV
1852
            new_start_series = time_frame->getIndexAtTime(static_cast<float>(_new_interval_start_time)).getValue();
×
UNCOV
1853
            new_end_series = time_frame->getIndexAtTime(static_cast<float>(_new_interval_end_time)).getValue();
×
1854
        }
1855

1856
        // Validate converted coordinates
UNCOV
1857
        if (new_start_series >= new_end_series || new_start_series < 0 || new_end_series < 0) {
×
UNCOV
1858
            throw std::runtime_error("Invalid interval bounds after conversion");
×
1859
        }
1860

1861
        // Add the new interval to the series
UNCOV
1862
        series->addEvent(TimeFrameIndex(new_start_series), TimeFrameIndex(new_end_series));
×
1863

1864
        // Set the new interval as selected (stored in master time frame coordinates)
1865
        setSelectedInterval(_new_interval_series_key, _new_interval_start_time, _new_interval_end_time);
×
1866

1867
        std::cout << "Created new interval [" << _new_interval_start_time
×
UNCOV
1868
                  << ", " << _new_interval_end_time << "] for series "
×
1869
                  << _new_interval_series_key << std::endl;
×
1870

1871
    } catch (...) {
×
1872
        // Error occurred during conversion or data update - abort creation
1873
        std::cout << "Error during new interval creation" << std::endl;
×
1874
        cancelNewIntervalCreation();
×
UNCOV
1875
        return;
×
UNCOV
1876
    }
×
1877

1878
    // Reset creation state
1879
    _is_creating_new_interval = false;
×
UNCOV
1880
    _new_interval_series_key.clear();
×
UNCOV
1881
    setCursor(Qt::ArrowCursor);
×
1882

1883
    // Trigger final redraw
UNCOV
1884
    updateCanvas(_time);
×
1885
}
1886

1887
void OpenGLWidget::cancelNewIntervalCreation() {
×
UNCOV
1888
    if (!_is_creating_new_interval) {
×
UNCOV
1889
        return;
×
1890
    }
1891

UNCOV
1892
    std::cout << "Cancelled new interval creation" << std::endl;
×
1893

1894
    // Reset creation state without applying changes
1895
    _is_creating_new_interval = false;
×
UNCOV
1896
    _new_interval_series_key.clear();
×
UNCOV
1897
    setCursor(Qt::ArrowCursor);
×
1898

1899
    // Trigger redraw to remove the new interval visualization
UNCOV
1900
    updateCanvas(_time);
×
1901
}
1902

1903
void OpenGLWidget::drawNewIntervalBeingCreated() {
42✔
1904
    if (!_is_creating_new_interval) {
42✔
1905
        return;
42✔
1906
    }
1907

1908
    // Get the series data for rendering
1909
    auto it = _digital_interval_series.find(_new_interval_series_key);
×
UNCOV
1910
    if (it == _digital_interval_series.end()) {
×
UNCOV
1911
        return;
×
1912
    }
1913

1914
    auto const & display_options = it->second.display_options;
×
1915

1916
    auto const start_time = static_cast<float>(_xAxis.getStart());
×
1917
    auto const end_time = static_cast<float>(_xAxis.getEnd());
×
UNCOV
1918
    auto const min_y = _yMin;
×
UNCOV
1919
    auto const max_y = _yMax;
×
1920

1921
    // Check if the new interval is visible
UNCOV
1922
    if (_new_interval_end_time < static_cast<int64_t>(start_time) || _new_interval_start_time > static_cast<int64_t>(end_time)) {
×
UNCOV
1923
        return;
×
1924
    }
1925

UNCOV
1926
    auto axesProgram = ShaderManager::instance().getProgram("axes");
×
1927
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
×
1928

UNCOV
1929
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
×
UNCOV
1930
    setupVertexAttribs();
×
1931

1932
    // Set up matrices (same as normal interval rendering)
1933
    auto Model = glm::mat4(1.0f);
×
UNCOV
1934
    auto View = glm::mat4(1.0f);
×
1935
    auto Projection = glm::ortho(start_time, end_time, min_y, max_y);
×
1936

1937
    glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
×
UNCOV
1938
    glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
×
UNCOV
1939
    glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
×
1940

1941
    // Get colors
1942
    int r, g, b;
×
1943
    hexToRGB(display_options->hex_color, r, g, b);
×
1944
    float const rNorm = static_cast<float>(r) / 255.0f;
×
UNCOV
1945
    float const gNorm = static_cast<float>(g) / 255.0f;
×
UNCOV
1946
    float const bNorm = static_cast<float>(b) / 255.0f;
×
1947

1948
    // Set color and alpha uniforms for new interval (50% transparency)
UNCOV
1949
    glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
×
UNCOV
1950
    glUniform1f(m_alphaLoc, 0.5f);
×
1951

1952
    // Clip the new interval to visible range
UNCOV
1953
    float const new_start = std::max(static_cast<float>(_new_interval_start_time), start_time);
×
UNCOV
1954
    float const new_end = std::min(static_cast<float>(_new_interval_end_time), end_time);
×
1955

1956
    // Draw the new interval being created with 50% transparency
1957
    // Create 4D vertices (x, y, 0, 1) to match the shader expectations
UNCOV
1958
    std::array<GLfloat, 16> new_interval_vertices = {
×
1959
            new_start, min_y, 0.0f, 1.0f,
1960
            new_end, min_y, 0.0f, 1.0f,
1961
            new_end, max_y, 0.0f, 1.0f,
1962
            new_start, max_y, 0.0f, 1.0f};
×
1963

1964
    m_vbo.bind();
×
1965
    m_vbo.allocate(new_interval_vertices.data(), new_interval_vertices.size() * sizeof(GLfloat));
×
UNCOV
1966
    m_vbo.release();
×
1967
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
×
1968

UNCOV
1969
    glUseProgram(0);
×
UNCOV
1970
}
×
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