diff --git a/SavvyCAN.pro b/SavvyCAN.pro index 1e02488..9d67acf 100644 --- a/SavvyCAN.pro +++ b/SavvyCAN.pro @@ -67,7 +67,8 @@ SOURCES += main.cpp\ helpwindow.cpp \ blfhandler.cpp \ re/sniffer/SnifferDelegate.cpp \ - connections/newconnectiondialog.cpp + connections/newconnectiondialog.cpp \ + re/temporalgraphwindow.cpp HEADERS += mainwindow.h \ can_structs.h \ @@ -125,7 +126,8 @@ HEADERS += mainwindow.h \ helpwindow.h \ blfhandler.h \ re/sniffer/SnifferDelegate.h \ - connections/newconnectiondialog.h + connections/newconnectiondialog.h \ + re/temporalgraphwindow.h FORMS += ui/candatagrid.ui \ ui/connectionwindow.ui \ @@ -153,7 +155,8 @@ FORMS += ui/candatagrid.ui \ ui/bisectwindow.ui \ ui/signalviewerwindow.ui \ helpwindow.ui \ - ui/newconnectiondialog.ui + ui/newconnectiondialog.ui \ + ui/temporalgraphwindow.ui DISTFILES += diff --git a/mainwindow.cpp b/mainwindow.cpp index ae91358..56e6844 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -83,6 +83,7 @@ MainWindow::MainWindow(QWidget *parent) : snifferWindow = nullptr; bisectWindow = nullptr; signalViewerWindow = nullptr; + temporalGraphWindow = nullptr; dbcHandler = DBCHandler::getReference(); bDirty = false; inhibitFilterUpdate = false; @@ -131,6 +132,7 @@ MainWindow::MainWindow(QWidget *parent) : connect(ui->actionCapture_Bisector, &QAction::triggered, this, &MainWindow::showBisectWindow); connect(ui->actionSignal_Viewer, &QAction::triggered, this, &MainWindow::showSignalViewer); connect(ui->actionSave_Continuous_Logfile, &QAction::triggered, this, &MainWindow::handleContinousLogging); + connect(ui->actionTemporal_Graph, &QAction::triggered, this, &MainWindow::showTemporalGraphWindow); connect(CANConManager::getInstance(), &CANConManager::framesReceived, model, &CANFrameModel::addFrames); @@ -226,6 +228,7 @@ void MainWindow::killEmAll() killWindow(motorctrlConfigWindow); killWindow(signalViewerWindow); killWindow(connectionWindow); + killWindow(temporalGraphWindow); } //forcefully close the window, kill it, and salt the earth @@ -837,6 +840,19 @@ void MainWindow::showGraphingWindow() graphingWindow->show(); } +void MainWindow::showTemporalGraphWindow() +{ + //only create an instance of the object if we dont have one. Otherwise just display the existing one. + if (!temporalGraphWindow) + { + if (!useFiltered) + temporalGraphWindow = new TemporalGraphWindow(model->getListReference()); + else + temporalGraphWindow = new TemporalGraphWindow(model->getFilteredListReference()); + } + temporalGraphWindow->show(); +} + void MainWindow::showFrameDataAnalysis() { //only create an instance of the object if we dont have one. Otherwise just display the existing one. diff --git a/mainwindow.h b/mainwindow.h index 5814211..d19a322 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -31,6 +31,7 @@ #include "re/isotp_interpreterwindow.h" #include "motorcontrollerconfigwindow.h" #include "signalviewerwindow.h" +#include "re/temporalgraphwindow.h" class CANConnection; class ConnectionWindow; @@ -81,6 +82,7 @@ private slots: void showSnifferWindow(); void showBisectWindow(); void showSignalViewer(); + void showTemporalGraphWindow(); void exitApp(); void handleSaveDecoded(); void connectionStatusUpdated(int conns); @@ -159,6 +161,7 @@ private: MotorControllerConfigWindow *motorctrlConfigWindow; BisectWindow* bisectWindow; SignalViewerWindow *signalViewerWindow; + TemporalGraphWindow *temporalGraphWindow; //various private storage QLabel lbStatusConnected; diff --git a/re/temporalgraphwindow.cpp b/re/temporalgraphwindow.cpp new file mode 100644 index 0000000..7fedfa6 --- /dev/null +++ b/re/temporalgraphwindow.cpp @@ -0,0 +1,381 @@ +#include "temporalgraphwindow.h" +#include "ui_temporalgraphwindow.h" +#include "helpwindow.h" +#include "mainwindow.h" + +QString HexTicker::getTickLabel (double tick, const QLocale& locale, QChar formatChar, int precision) +{ + int valu = static_cast(tick); + //qDebug() << valu; + return "0x" + QString::number(valu, 16).toUpper().rightJustified(3,'0'); +} + +TemporalGraphWindow::TemporalGraphWindow(const QVector *frames, QWidget *parent) : + QDialog(parent), + ui(new Ui::TemporalGraphWindow) +{ + ui->setupUi(this); + + setWindowFlags(Qt::Window); + + readSettings(); + + modelFrames = frames; + + ui->graphingView->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectAxes); + + ui->graphingView->xAxis->setRange(0, 8); + ui->graphingView->yAxis->setRange(0, 255); + ui->graphingView->axisRect()->setupFullAxesBox(); + + + ui->graphingView->xAxis->setLabel("Elapsed Time"); + ui->graphingView->yAxis->setLabel("ID"); + ui->graphingView->xAxis->setNumberFormat("f"); + ui->graphingView->xAxis->setNumberPrecision(6); + + HexTicker *tick = new HexTicker; + QSharedPointer sharedTicker(tick); + tick->setTickCount(10); + ui->graphingView->yAxis->setTicker(sharedTicker); + + // connect slot that ties some axis selections together (especially opposite axes): + connect(ui->graphingView, SIGNAL(selectionChangedByUser()), this, SLOT(selectionChanged())); + connect(ui->graphingView, SIGNAL(mousePress(QMouseEvent*)), this, SLOT(mousePress())); + connect(ui->graphingView, SIGNAL(mouseWheel(QWheelEvent*)), this, SLOT(mouseWheel())); + connect(MainWindow::getReference(), SIGNAL(framesUpdated(int)), this, SLOT(updatedFrames(int))); + // make bottom and left axes transfer their ranges to top and right axes: + connect(ui->graphingView->xAxis, SIGNAL(rangeChanged(QCPRange)), ui->graphingView->xAxis2, SLOT(setRange(QCPRange))); + connect(ui->graphingView->yAxis, SIGNAL(rangeChanged(QCPRange)), ui->graphingView->yAxis2, SLOT(setRange(QCPRange))); + + if (useOpenGL) + { + ui->graphingView->setAntialiasedElements(QCP::aeAll); + //ui->graphingView->setNoAntialiasingOnDrag(true); + ui->graphingView->setOpenGl(true); + } + else + { + ui->graphingView->setOpenGl(false); + ui->graphingView->setAntialiasedElements(QCP::aeNone); + } +} + +TemporalGraphWindow::~TemporalGraphWindow() +{ + delete ui; +} + +void TemporalGraphWindow::showEvent(QShowEvent* event) +{ + QDialog::showEvent(event); + installEventFilter(this); + readSettings(); + generateGraph(); + ui->graphingView->replot(); +} + +void TemporalGraphWindow::closeEvent(QCloseEvent *event) +{ + Q_UNUSED(event); + removeEventFilter(this); + writeSettings(); +} + +void TemporalGraphWindow::readSettings() +{ + QSettings settings; + if (settings.value("Main/SaveRestorePositions", false).toBool()) + { + resize(settings.value("Temporal/WindowSize", QSize(800, 600)).toSize()); + move(settings.value("Temporal/WindowPos", QPoint(50, 50)).toPoint()); + } + useOpenGL = settings.value("Main/UseOpenGL", false).toBool(); +} + +void TemporalGraphWindow::writeSettings() +{ + QSettings settings; + + if (settings.value("Main/SaveRestorePositions", false).toBool()) + { + settings.setValue("Temporal/WindowSize", size()); + settings.setValue("Temporal/WindowPos", pos()); + } +} + +void TemporalGraphWindow::updatedFrames(int numFrames) +{ + CANFrame thisFrame; + QVector x, y; + bool appendedToGraph = false; + bool needReplot = false; + + if (numFrames == -1) //all frames deleted. Kill the display + { + //removeAllGraphs(); + //now instead of removing the graphs regenerate them which will blank them out but leave them there in case + //more traffic that matches comes in or someone otherwise loads more data + ui->graphingView->clearGraphs(); //temporarily remove the graphs from the graph view + ui->graphingView->replot(); //now, redisplay them all + + } + else if (numFrames == -2) //all new set of frames. Reset + { + //there shouldn't be any need to actually remove the graphs. + //regenerate them instead + ui->graphingView->clearGraphs(); //temporarily remove the graphs from the graph view + //needScaleSetup = true; + generateGraph(); + //ui->graphingView->replot(); //now, redisplay them all + } + else //just got some new frames. See if they are relevant. + { + if (numFrames > modelFrames->count()) return; + + appendedToGraph = false; + x.clear(); + y.clear(); + for (int i = modelFrames->count() - numFrames; i < modelFrames->count(); i++) + { + thisFrame = modelFrames->at(i); + /* + if (graphParams[j].ID == thisFrame.ID) + { + appendToGraph(graphParams[j], thisFrame, x, y); + appendedToGraph = true; + }*/ + } + if (appendedToGraph) + { + //graphParams[j].ref->addData(x, y); + needReplot = true; + } + + if (needReplot) + { + if (followGraphEnd) + { + //find the current X span and maintain that span but move the end of it over to match the new end + //of the actual graph. This causes the view to move with the data to always show the end + QCPRange range = ui->graphingView->xAxis->range(); + double size = range.size(); + bool foundRange; + QCPRange keyRange = ui->graphingView->graph()->getKeyRange(foundRange); + if (foundRange) + { + double end, start; + end = keyRange.upper; + start = end - size; + ui->graphingView->xAxis->setRange(start, end); + } + } + ui->graphingView->replot(); + } + } +} + +void TemporalGraphWindow::generateGraph() +{ + if (modelFrames->count() == 0) return; + qDebug() << "Regenerating the graph"; + ui->graphingView->addGraph(); + graph = ui->graphingView->graph(); + + QVector x, y; + double xminval = 0, xmaxval = 0, yminval = 0, ymaxval = 0; + int frameCount = modelFrames->count(); + + x.reserve(frameCount); + y.reserve(frameCount); + + xminval = xmaxval = modelFrames->at(0).timestamp / 1000000.0; + yminval = ymaxval = modelFrames->at(0).ID; + + for (int i = 0; i < frameCount; i++) + { + x.append(modelFrames->at(i).timestamp / 1000000.0); + y.append(modelFrames->at(i).ID); + if (x[i] > xmaxval) xmaxval = x[i]; + if (x[i] < xminval) xminval = x[i]; + if (y[i] > ymaxval) ymaxval = y[i]; + if (y[i] < yminval) yminval = y[i]; + } + + ui->graphingView->graph()->setData(x,y); + ui->graphingView->graph()->setLineStyle(QCPGraph::lsNone); //no lines + ui->graphingView->graph()->setScatterStyle(QCPScatterStyle::ssPlus); + QPen graphPen; + graphPen.setColor(Qt::blue); + graphPen.setWidth(1); + ui->graphingView->graph()->setPen(graphPen); + + qDebug() << "xmin: " << xminval; + qDebug() << "xmax: " << xmaxval; + qDebug() << "ymin: " << yminval; + qDebug() << "ymax: " << ymaxval; + + ui->graphingView->xAxis->setRange(xminval, xmaxval); + ui->graphingView->yAxis->setRange(yminval, ymaxval); + ui->graphingView->axisRect()->setupFullAxesBox(); + + ui->graphingView->replot(); +} + +void TemporalGraphWindow::selectionChanged() +{ + /* + normally, axis base line, axis tick labels and axis labels are selectable separately, but we want + the user only to be able to select the axis as a whole, so we tie the selected states of the tick labels + and the axis base line together. However, the axis label shall be selectable individually. + + The selection state of the left and right axes shall be synchronized as well as the state of the + bottom and top axes. + + Further, we want to synchronize the selection of the graphs with the selection state of the respective + legend item belonging to that graph. So the user can select a graph by either clicking on the graph itself + or on its legend item. + */ + + qDebug() << "SelectionChanged"; + + // make top and bottom axes be selected synchronously, and handle axis and tick labels as one selectable object: + if (ui->graphingView->xAxis->selectedParts().testFlag(QCPAxis::spAxis) || ui->graphingView->xAxis->selectedParts().testFlag(QCPAxis::spTickLabels) || + ui->graphingView->xAxis2->selectedParts().testFlag(QCPAxis::spAxis) || ui->graphingView->xAxis2->selectedParts().testFlag(QCPAxis::spTickLabels)) + { + ui->graphingView->xAxis2->setSelectedParts(QCPAxis::spAxis|QCPAxis::spTickLabels); + ui->graphingView->xAxis->setSelectedParts(QCPAxis::spAxis|QCPAxis::spTickLabels); + } + // make left and right axes be selected synchronously, and handle axis and tick labels as one selectable object: + if (ui->graphingView->yAxis->selectedParts().testFlag(QCPAxis::spAxis) || ui->graphingView->yAxis->selectedParts().testFlag(QCPAxis::spTickLabels) || + ui->graphingView->yAxis2->selectedParts().testFlag(QCPAxis::spAxis) || ui->graphingView->yAxis2->selectedParts().testFlag(QCPAxis::spTickLabels)) + { + ui->graphingView->yAxis2->setSelectedParts(QCPAxis::spAxis|QCPAxis::spTickLabels); + ui->graphingView->yAxis->setSelectedParts(QCPAxis::spAxis|QCPAxis::spTickLabels); + } +} + +void TemporalGraphWindow::mousePress() +{ + // if an axis is selected, only allow the direction of that axis to be dragged + // if no axis is selected, both directions may be dragged + + if (ui->graphingView->xAxis->selectedParts().testFlag(QCPAxis::spAxis)) + { + ui->graphingView->axisRect()->setRangeDrag(ui->graphingView->xAxis->orientation()); + } + else if (ui->graphingView->yAxis->selectedParts().testFlag(QCPAxis::spAxis)) + { + ui->graphingView->axisRect()->setRangeDrag(ui->graphingView->yAxis->orientation()); + } + else + { + ui->graphingView->axisRect()->setRangeDrag(Qt::Horizontal|Qt::Vertical); + } +} + +void TemporalGraphWindow::mouseWheel() +{ + qDebug() << "Mouse WHeel"; + // if an axis is selected, only allow the direction of that axis to be zoomed + // if no axis is selected, both directions may be zoomed + + if (ui->graphingView->xAxis->selectedParts().testFlag(QCPAxis::spAxis)) + ui->graphingView->axisRect()->setRangeZoom(ui->graphingView->xAxis->orientation()); + else if (ui->graphingView->yAxis->selectedParts().testFlag(QCPAxis::spAxis)) + ui->graphingView->axisRect()->setRangeZoom(ui->graphingView->yAxis->orientation()); + else + ui->graphingView->axisRect()->setRangeZoom(Qt::Horizontal|Qt::Vertical); +} + +bool TemporalGraphWindow::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::KeyRelease) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) + { + case Qt::Key_Plus: + zoomIn(); + break; + case Qt::Key_Minus: + zoomOut(); + break; + case Qt::Key_F1: + HelpWindow::getRef()->showHelp("temporalwindow.html"); + break; + } + return true; + } else { + // standard event processing + return QObject::eventFilter(obj, event); + } + + return false; +} + +void TemporalGraphWindow::resetView() +{ + double yminval=10000000.0, ymaxval = -1000000.0; + double xminval=100000000000, xmaxval = -10000000000.0; + /* + for (int i = 0; i < graphParams.count(); i++) + { + for (int j = 0; j < graphParams[i].x.count(); j++) + { + if (graphParams[i].x[j] < xminval) xminval = graphParams[i].x[j]; + if (graphParams[i].x[j] > xmaxval) xmaxval = graphParams[i].x[j]; + if (graphParams[i].y[j] < yminval) yminval = graphParams[i].y[j]; + if (graphParams[i].y[j] > ymaxval) ymaxval = graphParams[i].y[j]; + } + } + + ui->graphingView->xAxis->setRange(xminval, xmaxval); + ui->graphingView->yAxis->setRange(yminval, ymaxval); + ui->graphingView->axisRect()->setupFullAxesBox(); + + ui->graphingView->replot(); + */ +} + +void TemporalGraphWindow::zoomIn() +{ + QCPRange xrange = ui->graphingView->xAxis->range(); + QCPRange yrange = ui->graphingView->yAxis->range(); + if (ui->graphingView->xAxis->selectedParts().testFlag(QCPAxis::spAxis)) + { + ui->graphingView->xAxis->scaleRange(0.666, xrange.center()); + } + + else if (ui->graphingView->yAxis->selectedParts().testFlag(QCPAxis::spAxis)) + { + ui->graphingView->yAxis->scaleRange(0.666, yrange.center()); + } + else + { + ui->graphingView->xAxis->scaleRange(0.666, xrange.center()); + ui->graphingView->yAxis->scaleRange(0.666, yrange.center()); + } + ui->graphingView->replot(); +} + +void TemporalGraphWindow::zoomOut() +{ + QCPRange xrange = ui->graphingView->xAxis->range(); + QCPRange yrange = ui->graphingView->yAxis->range(); + if (ui->graphingView->xAxis->selectedParts().testFlag(QCPAxis::spAxis)) + { + ui->graphingView->xAxis->scaleRange(1.5, xrange.center()); + } + + else if (ui->graphingView->yAxis->selectedParts().testFlag(QCPAxis::spAxis)) + { + ui->graphingView->yAxis->scaleRange(1.5, yrange.center()); + } + else + { + ui->graphingView->xAxis->scaleRange(1.5, xrange.center()); + ui->graphingView->yAxis->scaleRange(1.5, yrange.center()); + } + ui->graphingView->replot(); +} + diff --git a/re/temporalgraphwindow.h b/re/temporalgraphwindow.h new file mode 100644 index 0000000..39b1809 --- /dev/null +++ b/re/temporalgraphwindow.h @@ -0,0 +1,50 @@ +#ifndef TEMPORALGRAPHWINDOW_H +#define TEMPORALGRAPHWINDOW_H + +#include +#include "qcustomplot.h" +#include "can_structs.h" + +namespace Ui { +class TemporalGraphWindow; +} + +class HexTicker : public QCPAxisTicker +{ + QString getTickLabel (double tick, const QLocale& locale, QChar formatChar, int precision); +}; + +class TemporalGraphWindow : public QDialog +{ + Q_OBJECT + +public: + explicit TemporalGraphWindow(const QVector *, QWidget *parent = nullptr); + ~TemporalGraphWindow(); + void showEvent(QShowEvent*); + +private slots: + void updatedFrames(int); + void mousePress(); + void mouseWheel(); + void resetView(); + void zoomIn(); + void zoomOut(); + void selectionChanged(); + +private: + Ui::TemporalGraphWindow *ui; + const QVector *modelFrames; + bool useOpenGL; + bool followGraphEnd; + QCPGraph *graph; + + void closeEvent(QCloseEvent *event); + bool eventFilter(QObject *obj, QEvent *event); + void readSettings(); + void writeSettings(); + void generateGraph(); + +}; + +#endif // TEMPORALGRAPHWINDOW_H diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui index e21a989..90c8302 100644 --- a/ui/mainwindow.ui +++ b/ui/mainwindow.ui @@ -282,6 +282,7 @@ + @@ -497,6 +498,11 @@ Start Continuous Logging + + + Temporal Graph + + diff --git a/ui/temporalgraphwindow.ui b/ui/temporalgraphwindow.ui new file mode 100644 index 0000000..d8f02e5 --- /dev/null +++ b/ui/temporalgraphwindow.ui @@ -0,0 +1,32 @@ + + + TemporalGraphWindow + + + + 0 + 0 + 886 + 683 + + + + Temporal Graph + + + + + + + + + + QCustomPlot + QWidget +
qcustomplot.h
+ 1 +
+
+ + +