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

paulmthompson / WhiskerToolbox / 17465586740

04 Sep 2025 01:21PM UTC coverage: 70.828% (-0.1%) from 70.97%
17465586740

push

github

paulmthompson
feature tree widget shouldn't emit signals during rebuild

121 of 131 new or added lines in 4 files covered. (92.37%)

108 existing lines in 7 files now uncovered.

34146 of 48210 relevant lines covered (70.83%)

1299.48 hits per line

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

25.15
/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)
14✔
60
    : QOpenGLWidget(parent) {
70✔
61
    setMouseTracking(true);// Enable mouse tracking for hover events
14✔
62
}
14✔
63

64
OpenGLWidget::~OpenGLWidget() {
28✔
65
    cleanup();
14✔
66
}
28✔
67

68
void OpenGLWidget::updateCanvas(int time) {
81✔
69
    _time = time;
81✔
70
    //std::cout << "Redrawing at " << _time << std::endl;
71
    update();
81✔
72
}
81✔
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() {
26✔
208
    // Avoid re-entrancy or cleanup without a valid context
209
    if (!_gl_initialized) {
26✔
210
        return;
14✔
211
    }
212

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

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

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

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

234
    doneCurrent();
12✔
235

236
    _gl_initialized = false;
12✔
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() {
12✔
260
    // Ensure QOpenGLFunctions is initialized
261
    initializeOpenGLFunctions();
12✔
262

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

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

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

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

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

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

350
    m_vbo.release();
152✔
351
}
152✔
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() {
35✔
367
    int r, g, b;
35✔
368
    auto const start_time = _xAxis.getStart();
35✔
369
    auto const end_time = _xAxis.getEnd();
35✔
370
    auto axesProgram = ShaderManager::instance().getProgram("axes");
105✔
371
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
35✔
372

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

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

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

387
    if (visible_event_count == 0 || !_master_time_frame) {
35✔
388
        glUseProgram(0);
35✔
389
        return;
35✔
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
}
35✔
481

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

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

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

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

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

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

503
    for (auto const & [key, interval_data]: _digital_interval_series) {
35✔
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);
35✔
659
}
35✔
660

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

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

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

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

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

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

680
    int i = 0;
35✔
681

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

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

693
        if (!display_options->is_visible) continue;
24✔
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) {
24✔
699
            std::cerr << "Warning: PlottingManager not set in OpenGLWidget" << std::endl;
×
700
            continue;
×
701
        }
702

703
        float allocated_y_center, allocated_height;
24✔
704
        _plotting_manager->calculateAnalogSeriesAllocation(i, allocated_y_center, allocated_height);
24✔
705

706
        display_options->allocated_y_center = allocated_y_center;
24✔
707
        display_options->allocated_height = allocated_height;
24✔
708

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

715
        m_vertices.clear();
24✔
716

717
        auto series_start_index = getTimeIndexForSeries(start_time, _master_time_frame.get(), time_frame.get());
24✔
718
        auto series_end_index = getTimeIndexForSeries(end_time, _master_time_frame.get(), time_frame.get());
24✔
719

720
        // === MVP MATRIX SETUP ===
721

722
        // Apply PlottingManager pan offset
723
        _plotting_manager->setPanOffset(_verticalPanOffset);
24✔
724

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

729
        glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
24✔
730
        glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
24✔
731
        glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
24✔
732

733
        // Set color and alpha uniforms
734
        glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
24✔
735
        glUniform1f(m_alphaLoc, 1.0f);
24✔
736

737
        auto analog_range = series->getTimeValueSpanInTimeFrameIndexRange(series_start_index, series_end_index);
24✔
738

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

747
        if (display_options->gap_handling == AnalogGapHandling::AlwaysConnect) {
24✔
748

749
            auto time_begin = analog_range.time_indices.begin();
×
750

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

756
                m_vertices.push_back(xCanvasPos);
×
757
                m_vertices.push_back(yCanvasPos);
×
758
                m_vertices.push_back(0.0f);  // z coordinate
×
759
                m_vertices.push_back(1.0f);  // w coordinate
×
760

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

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

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

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

783

784
        i++;
24✔
785
    }
24✔
786

787
    // Reset line width to default
788
    glLineWidth(1.0f);
35✔
789
    glUseProgram(0);
35✔
790
}
35✔
791

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

797
    std::vector<GLfloat> segment_vertices;
24✔
798
    auto prev_index = 0;
24✔
799

800
    auto time_begin = analog_range.time_indices.begin();
24✔
801

802
    for (size_t i = 0; i < analog_range.values.size(); i++) {
72✔
803
        auto const xCanvasPos = time_frame->getTimeAtIndex(**time_begin);
48✔
804
        auto const yCanvasPos = analog_range.values[i];
48✔
805

806
        // Check for gap if this isn't the first point
807
        if (prev_index != 0) {
48✔
808

809
            float const time_gap = (**time_begin).getValue() - prev_index;
×
810

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

820
                // Start new segment
821
                segment_vertices.clear();
×
822
            }
823
        }
824

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

831
        prev_index = (**time_begin).getValue();
48✔
832
        ++(*time_begin);
48✔
833
    }
834

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

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

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

855
    auto time_begin = analog_range.time_indices.begin();
×
856

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

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

862
        m_vertices.push_back(xCanvasPos);
×
863
        m_vertices.push_back(yCanvasPos);
×
864
        m_vertices.push_back(0.0f);  // z coordinate
×
865
        m_vertices.push_back(1.0f);  // w coordinate
×
866

867
        ++(*time_begin);
×
868
    }
869

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

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

882
///////////////////////////////////////////////////////////////////////////////
883

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

893
    //This has been converted to master coordinates
894
    int const currentTime = _time;
35✔
895

896
    int64_t const zoom = _xAxis.getEnd() - _xAxis.getStart();
35✔
897
    _xAxis.setCenterAndZoom(currentTime, zoom);
35✔
898

899
    // Update Y boundaries based on pan and zoom
900
    _updateYViewBoundaries();
35✔
901

902
    // Draw the series
903
    drawDigitalEventSeries();
35✔
904
    drawDigitalIntervalSeries();
35✔
905
    drawAnalogSeries();
35✔
906

907
    drawAxis();
35✔
908

909
    drawGridLines();
35✔
910

911
    drawDraggedInterval();
35✔
912
    drawNewIntervalBeingCreated();
35✔
913
}
35✔
914

915
void OpenGLWidget::resizeGL(int w, int h) {
12✔
916

917
    static_cast<void>(w);
918
    static_cast<void>(h);
919

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

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

927
    m_view.setToIdentity();
12✔
928
    m_view.translate(0, 0, -1);// Move slightly back for orthographic view
12✔
929

930
    // Trigger a repaint with the new dimensions
931
    update();
12✔
932
}
12✔
933

934
void OpenGLWidget::drawAxis() {
35✔
935

936
    auto axesProgram = ShaderManager::instance().getProgram("axes");
105✔
937
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
35✔
938

939
    glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, m_proj.constData());
35✔
940
    glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, m_view.constData());
35✔
941
    glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, m_model.constData());
35✔
942

943
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
35✔
944
    setupVertexAttribs();
35✔
945

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

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

957
    m_vbo.bind();
35✔
958
    m_vbo.allocate(lineVertices.data(), lineVertices.size() * sizeof(GLfloat));
105✔
959
    m_vbo.release();
35✔
960
    glDrawArrays(GL_LINES, 0, 2);
35✔
961
    glUseProgram(0);
35✔
962
}
70✔
963

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

970
    auto display_options = std::make_unique<NewAnalogTimeSeriesDisplayOptions>();
9✔
971

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

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

985
    if (time_frame->getTotalFrameCount() / 5 > series->getNumSamples()) {
9✔
986
        display_options->gap_handling = AnalogGapHandling::AlwaysConnect;
×
987
        display_options->enable_gap_detection = false;
×
988

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

995
    _analog_series[key] = AnalogSeriesData{
45✔
996
            std::move(series),
9✔
997
            std::move(time_frame),
9✔
998
            std::move(display_options)};
18✔
999

1000
    updateCanvas(_time);
9✔
1001
}
18✔
1002

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

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

1017
    auto display_options = std::make_unique<NewDigitalEventSeriesDisplayOptions>();
×
1018

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

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

1028
    updateCanvas(_time);
×
1029
}
×
1030

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

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

1045
    auto display_options = std::make_unique<NewDigitalIntervalSeriesDisplayOptions>();
×
1046

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

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

1056
    updateCanvas(_time);
×
1057
}
×
1058

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

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

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

1079
void OpenGLWidget::clearSeries() {
×
1080
    _analog_series.clear();
×
1081
    updateCanvas(_time);
×
1082
}
×
1083

1084
void OpenGLWidget::drawDashedLine(LineParameters const & params) {
×
1085

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

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

1095
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
×
1096

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

1102
    m_vbo.bind();
×
1103
    m_vbo.allocate(vertices.data(), vertices.size() * sizeof(float));
×
1104

1105
    glEnableVertexAttribArray(0);
×
1106
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), nullptr);
×
1107

1108
    glDrawArrays(GL_LINES, 0, 2);
×
1109

1110
    m_vbo.release();
×
1111

1112
    glUseProgram(0);
×
1113
}
×
1114

1115
void OpenGLWidget::drawGridLines() {
35✔
1116
    if (!_grid_lines_enabled) {
35✔
1117
        return;// Grid lines are disabled
35✔
1118
    }
1119

1120
    auto const start_time = _xAxis.getStart();
×
1121
    auto const end_time = _xAxis.getEnd();
×
1122

1123
    // Calculate the range of time values
1124
    auto const time_range = end_time - start_time;
×
1125

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

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

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

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

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

1156
        drawDashedLine(gridLine);
×
1157
    }
1158
}
1159

1160
void OpenGLWidget::_updateYViewBoundaries() {
35✔
1161
    /*
1162
    float viewHeight = 2.0f;
1163

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

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

1178
    auto const start_time = static_cast<float>(_xAxis.getStart());
×
1179
    auto const end_time = static_cast<float>(_xAxis.getEnd());
×
1180

1181
    return start_time + normalized_x * (end_time - start_time);
×
1182
}
1183

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

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

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

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

1202
    auto const & display_options = analog_it->second.display_options;
×
1203

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

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

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

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

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

1236
        float const relative_y = adjusted_y - series_y_offset;
×
1237

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

1244
        return relative_y / legacy_amplitude_scale;
×
1245
    }
×
1246
}
1247

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

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

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

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

1276
    auto const & interval_data = it->second;
×
1277
    auto const & series = interval_data.series;
×
1278
    auto const & time_frame = interval_data.time_frame;
×
1279

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

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

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

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

1308
        return std::make_pair(interval_start_master, interval_end_master);
×
1309
    }
1310

1311
    return std::nullopt;
×
1312
}
1313

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

1317
    static_cast<void>(canvas_y);
1318

1319
    float const time_coord = canvasXToTime(canvas_x);
×
1320
    constexpr float EDGE_TOLERANCE_PX = 10.0f;
×
1321

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

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

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

1337
            float const start_canvas_x = (start_time_f - start_time_canvas) / (end_time_canvas - start_time_canvas) * canvas_width;
×
1338
            float const end_canvas_x = (end_time_f - start_time_canvas) / (end_time_canvas - start_time_canvas) * canvas_width;
×
1339

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

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

1352
    return std::nullopt;
×
1353
}
1354

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

1361
    auto const [start_time, end_time] = selected_interval.value();
×
1362

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

1372
    // Disable normal mouse interactions during drag
1373
    setCursor(Qt::SizeHorCursor);
×
1374

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

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

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

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

1395
    auto const & series = it->second.series;
×
1396
    auto const & time_frame = it->second.time_frame;
×
1397

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

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

1420
    // Perform dragging logic in series time frame
1421
    int64_t new_start_series, new_end_series;
1422

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

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

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

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

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

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

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

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

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

1490
    // Trigger redraw to show the dragged interval
1491
    updateCanvas(_time);
×
1492
}
1493

1494
void OpenGLWidget::finishIntervalDrag() {
×
1495
    if (!_is_dragging_interval) {
×
1496
        return;
×
1497
    }
1498

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

1507
    auto const & series = it->second.series;
×
1508
    auto const & time_frame = it->second.time_frame;
×
1509

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

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

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

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

1540
        // Add the new interval
1541
        series->addEvent(TimeFrameIndex(new_start_series), TimeFrameIndex(new_end_series));
×
1542

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

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

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

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

1562
    // Trigger final redraw
1563
    updateCanvas(_time);
×
1564
}
1565

1566
void OpenGLWidget::cancelIntervalDrag() {
×
1567
    if (!_is_dragging_interval) {
×
1568
        return;
×
1569
    }
1570

1571
    std::cout << "Cancelled interval drag" << std::endl;
×
1572

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

1578
    // Trigger redraw to remove the dragged interval visualization
1579
    updateCanvas(_time);
×
1580
}
1581

1582
void OpenGLWidget::drawDraggedInterval() {
35✔
1583
    if (!_is_dragging_interval) {
35✔
1584
        return;
35✔
1585
    }
1586

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

1593
    auto const & display_options = it->second.display_options;
×
1594

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

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

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

1608
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
×
1609
    setupVertexAttribs();
×
1610

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

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

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

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

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

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

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

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

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

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

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

1667
    glUseProgram(0);
×
1668
}
×
1669

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

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

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

1697
    QOpenGLWidget::mouseDoubleClickEvent(event);
×
1698
}
1699

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

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

1712
    _is_creating_new_interval = true;
×
1713
    _new_interval_series_key = series_key;
×
1714
    _new_interval_click_pos = start_pos;
×
1715

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

1720
    // Initialize start and end to the click position
1721
    _new_interval_start_time = _new_interval_click_time;
×
1722
    _new_interval_end_time = _new_interval_click_time;
×
1723

1724
    // Set cursor to indicate creation mode
1725
    setCursor(Qt::SizeHorCursor);
×
1726

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

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

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

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

1748
    auto const & series = it->second.series;
×
1749
    auto const & time_frame = it->second.time_frame;
×
1750

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

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

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

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

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

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

1820
    // Trigger redraw to show the new interval being created
1821
    updateCanvas(_time);
×
1822
}
1823

1824
void OpenGLWidget::finishNewIntervalCreation() {
×
1825
    if (!_is_creating_new_interval) {
×
1826
        return;
×
1827
    }
1828

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

1837
    auto const & series = it->second.series;
×
1838
    auto const & time_frame = it->second.time_frame;
×
1839

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

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

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

1859
        // Add the new interval to the series
1860
        series->addEvent(TimeFrameIndex(new_start_series), TimeFrameIndex(new_end_series));
×
1861

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

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

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

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

1881
    // Trigger final redraw
1882
    updateCanvas(_time);
×
1883
}
1884

1885
void OpenGLWidget::cancelNewIntervalCreation() {
×
1886
    if (!_is_creating_new_interval) {
×
1887
        return;
×
1888
    }
1889

1890
    std::cout << "Cancelled new interval creation" << std::endl;
×
1891

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

1897
    // Trigger redraw to remove the new interval visualization
1898
    updateCanvas(_time);
×
1899
}
1900

1901
void OpenGLWidget::drawNewIntervalBeingCreated() {
35✔
1902
    if (!_is_creating_new_interval) {
35✔
1903
        return;
35✔
1904
    }
1905

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

1912
    auto const & display_options = it->second.display_options;
×
1913

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

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

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

1927
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
×
1928
    setupVertexAttribs();
×
1929

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

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

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

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

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

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

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

1967
    glUseProgram(0);
×
1968
}
×
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