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

paulmthompson / WhiskerToolbox / 17446119178

03 Sep 2025 09:06PM UTC coverage: 70.31% (-1.9%) from 72.184%
17446119178

push

github

paulmthompson
fix life cycle issues with data viewer widget. It now cleans up references it holds if something is deleted from data manager that it is plotting

243 of 273 new or added lines in 3 files covered. (89.01%)

130 existing lines in 3 files now uncovered.

33739 of 47986 relevant lines covered (70.31%)

1302.4 hits per line

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

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

64
OpenGLWidget::~OpenGLWidget() {
22✔
65
    cleanup();
11✔
66
}
22✔
67

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

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

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

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

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

234
    doneCurrent();
9✔
235

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

680
    int i = 0;
25✔
681

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

691
        auto const & display_options = analog_data.display_options;
×
692

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

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

706
        display_options->allocated_y_center = allocated_y_center;
×
707
        display_options->allocated_height = allocated_height;
×
708

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

715
        m_vertices.clear();
×
716

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

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

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

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

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

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

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

739
        if (analog_range.values.empty()) {
×
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) {
×
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) {
×
772
            // Draw multiple line segments, breaking at gaps
773
            // Set line thickness before drawing segments
774
            glLineWidth(static_cast<float>(display_options->line_thickness));
×
775
            _drawAnalogSeriesWithGapDetection(data, time_frame, analog_range,
×
776
                                              display_options->gap_threshold);
×
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++;
×
785
    }
×
786

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

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

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

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

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

806
        // Check for gap if this isn't the first point
807
        if (prev_index != 0) {
×
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);
×
827
        segment_vertices.push_back(yCanvasPos);
×
828
        segment_vertices.push_back(0.0f);  // z coordinate
×
829
        segment_vertices.push_back(1.0f);  // w coordinate
×
830

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

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

841
        if (segment_vertices.size() >= 8) {// At least 2 points (8 floats)
×
842
            glDrawArrays(GL_LINE_STRIP, 0, static_cast<int>(segment_vertices.size() / 4));
×
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
}
×
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() {
25✔
885
    int r, g, b;
25✔
886
    hexToRGB(m_background_color, r, g, b);
25✔
887
    glClearColor(
25✔
888
            static_cast<float>(r) / 255.0f,
25✔
889
            static_cast<float>(g) / 255.0f,
25✔
890
            static_cast<float>(b) / 255.0f, 1.0f);
25✔
891
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
25✔
892

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

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

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

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

907
    drawAxis();
25✔
908

909
    drawGridLines();
25✔
910

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

915
void OpenGLWidget::resizeGL(int w, int h) {
9✔
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();
9✔
925
    m_proj.ortho(-1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f);// Use orthographic projection for 2D plotting
9✔
926

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

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

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

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

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

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

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

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

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

964
void OpenGLWidget::addAnalogTimeSeries(
×
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>();
×
971

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

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

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

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

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

1000
    updateCanvas(_time);
×
1001
}
×
1002

1003
void OpenGLWidget::removeAnalogTimeSeries(std::string const & key) {
16✔
1004
    auto item = _analog_series.find(key);
16✔
1005
    if (item != _analog_series.end()) {
16✔
1006
        _analog_series.erase(item);
×
1007
    }
1008
    updateCanvas(_time);
16✔
1009
}
16✔
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

1031
void OpenGLWidget::removeDigitalEventSeries(std::string const & key) {
28✔
1032
    auto item = _digital_event_series.find(key);
28✔
1033
    if (item != _digital_event_series.end()) {
28✔
1034
        _digital_event_series.erase(item);
×
1035
    }
1036
    updateCanvas(_time);
28✔
1037
}
28✔
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

1059
void OpenGLWidget::removeDigitalIntervalSeries(std::string const & key) {
36✔
1060
    auto item = _digital_interval_series.find(key);
36✔
1061
    if (item != _digital_interval_series.end()) {
36✔
1062
        _digital_interval_series.erase(item);
×
1063
    }
1064
    updateCanvas(_time);
36✔
1065
}
36✔
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() {
25✔
1116
    if (!_grid_lines_enabled) {
25✔
1117
        return;// Grid lines are disabled
25✔
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() {
25✔
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
}
25✔
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() {
25✔
1583
    if (!_is_dragging_interval) {
25✔
1584
        return;
25✔
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() {
25✔
1902
    if (!_is_creating_new_interval) {
25✔
1903
        return;
25✔
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