trafficgraphwidget.cpp raw

   1  // Copyright (c) 2011-2022 The Limenka developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
   4  
   5  #include <interfaces/node.h>
   6  #include <qt/trafficgraphwidget.h>
   7  #include <qt/clientmodel.h>
   8  #include <qt/guiutil.h>
   9  
  10  #include <QMouseEvent>
  11  #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
  12  #include <QtNumeric>
  13  #endif
  14  #include <QPainter>
  15  #include <QPainterPath>
  16  #include <QColor>
  17  #include <QTimer>
  18  #include <QToolTip>
  19  
  20  #include <algorithm>
  21  #include <chrono>
  22  #include <cmath>
  23  
  24  #define DESIRED_SAMPLES         800
  25  
  26  #define XMARGIN                 10
  27  #define YMARGIN                 10
  28  
  29  TrafficGraphWidget::TrafficGraphWidget(QWidget* parent)
  30      : QWidget(parent),
  31        vSamplesIn(),
  32        vSamplesOut()
  33  {
  34      timer = new QTimer(this);
  35      tt_timer = new QTimer(this);
  36      connect(timer, &QTimer::timeout, this, &TrafficGraphWidget::updateRates);
  37      connect(tt_timer, &QTimer::timeout, this, &TrafficGraphWidget::updateToolTip);
  38      tt_timer->setInterval(500);
  39      tt_timer->start();
  40      setMouseTracking(true);
  41  }
  42  
  43  void TrafficGraphWidget::setClientModel(ClientModel *model)
  44  {
  45      clientModel = model;
  46      if(model) {
  47          nLastBytesIn = model->node().getTotalBytesRecv();
  48          nLastBytesOut = model->node().getTotalBytesSent();
  49      }
  50  }
  51  
  52  std::chrono::minutes TrafficGraphWidget::getGraphRange() const { return m_range; }
  53  
  54  int TrafficGraphWidget::y_value(float value)
  55  {
  56      if (fMax == 0) return 0;
  57      int h = height() - YMARGIN * 2;
  58      return YMARGIN + h - (h * 1.0 * (fToggle ? (pow(value, 0.30102) / pow(fMax, 0.30102)) : (value / fMax)));
  59  }
  60  
  61  void TrafficGraphWidget::paintPath(QPainterPath &path, QQueue<float> &samples)
  62  {
  63      int sampleCount = samples.size();
  64      if(sampleCount > 0) {
  65          int h = height() - YMARGIN * 2, w = width() - XMARGIN * 2;
  66          int x = XMARGIN + w;
  67          path.moveTo(x, YMARGIN + h);
  68          for(int i = 0; i < sampleCount; ++i) {
  69              x = XMARGIN + w - w * i / DESIRED_SAMPLES;
  70              int y = y_value(samples.at(i));
  71              path.lineTo(x, y);
  72          }
  73          path.lineTo(x, YMARGIN + h);
  74      }
  75  }
  76  
  77  void TrafficGraphWidget::mousePressEvent(QMouseEvent *event)
  78  {
  79      QWidget::mousePressEvent(event);
  80      fToggle = !fToggle;
  81      update();
  82  }
  83  
  84  void TrafficGraphWidget::mouseMoveEvent(QMouseEvent *event)
  85  {
  86      QWidget::mouseMoveEvent(event);
  87      static int last_x = -1;
  88      static int last_y = -1;
  89  #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
  90      const QPointF event_local_pos = event->position();
  91      const QPointF event_global_pos = event->globalPosition();
  92  #else
  93      const QPointF event_local_pos = event->localPos();
  94      const QPointF event_global_pos = event->screenPos();
  95  #endif
  96      int x = qRound(event_local_pos.x());
  97      int y = qRound(event_local_pos.y());
  98      x_offset = qRound(event_global_pos.x()) - x;
  99      y_offset = qRound(event_global_pos.y()) - y;
 100      if (last_x == x && last_y == y) return; // Do nothing if mouse hasn't moved
 101      int h = height() - YMARGIN * 2, w = width() - XMARGIN * 2;
 102      int i = (w + XMARGIN - x) * DESIRED_SAMPLES / w;
 103      unsigned int smallest_distance = 50; int closest_i = -1;
 104      int sampleSize = vTimeStamp.size();
 105      if (sampleSize && i >= -10 && i < sampleSize + 2 && y <= h + YMARGIN + 3) {
 106          for (int test_i = std::max(i - 2, 0); test_i < std::min(i + 10, sampleSize); test_i++) {
 107              float val = std::max(vSamplesIn.at(test_i), vSamplesOut.at(test_i));
 108              int y_data = y_value(val);
 109              unsigned int distance = abs(y - y_data);
 110              if (distance < smallest_distance) {
 111                  smallest_distance = distance;
 112                  closest_i = test_i;
 113              }
 114          }
 115      }
 116      if (ttpoint != closest_i) {
 117          ttpoint = closest_i;
 118          update(); // Calls paintEvent() to draw or delete the highlighted point
 119      }
 120      last_x = x; last_y = y;
 121  }
 122  
 123  void TrafficGraphWidget::paintEvent(QPaintEvent *)
 124  {
 125      QPainter painter(this);
 126      painter.fillRect(rect(), Qt::black);
 127  
 128      if(fMax <= 0.0f) return;
 129  
 130      QColor axisCol(Qt::gray);
 131      int h = height() - YMARGIN * 2;
 132      painter.setPen(axisCol);
 133      painter.drawLine(XMARGIN, YMARGIN + h, width() - XMARGIN, YMARGIN + h);
 134  
 135      // decide what order of magnitude we are
 136      int base = std::floor(std::log10(fMax));
 137      float val = std::pow(10.0f, base);
 138  
 139      const QString units = tr("kB/s");
 140      const float yMarginText = 2.0;
 141  
 142      // if we drew 10 or 3 fewer lines, break them up at the next lower order of magnitude
 143      if(fMax / val <= (fToggle ? 10.0f : 3.0f)) {
 144          float oldval = val;
 145          val = pow(10.0f, base - 1);
 146          painter.setPen(axisCol.darker());
 147          painter.drawText(XMARGIN, y_value(val)-yMarginText, QString("%1 %2").arg(val).arg(units));
 148          if (fToggle) {
 149              int yy = y_value(val*0.1);
 150              painter.drawText(XMARGIN, yy-yMarginText, QString("%1 %2").arg(val*0.1).arg(units));
 151              painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy);
 152          }
 153          int count = 1;
 154          for(float y = val; y < (!fToggle || fMax / val < 20 ? fMax : oldval); y += val, count++) {
 155              if(count % 10 == 0)
 156                  continue;
 157              int yy = y_value(y);
 158              painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy);
 159          }
 160          val = oldval;
 161      }
 162      // draw lines
 163      painter.setPen(axisCol);
 164      for(float y = val; y < fMax; y += val) {
 165          int yy = y_value(y);
 166          painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy);
 167      }
 168      painter.drawText(XMARGIN, y_value(val)-yMarginText, QString("%1 %2").arg(val).arg(units));
 169  
 170      painter.setRenderHint(QPainter::Antialiasing);
 171      if(!vSamplesIn.empty()) {
 172          QPainterPath p;
 173          paintPath(p, vSamplesIn);
 174          painter.fillPath(p, QColor(0, 255, 0, 128));
 175          painter.setPen(Qt::green);
 176          painter.drawPath(p);
 177      }
 178      if(!vSamplesOut.empty()) {
 179          QPainterPath p;
 180          paintPath(p, vSamplesOut);
 181          painter.fillPath(p, QColor(255, 0, 0, 128));
 182          painter.setPen(Qt::red);
 183          painter.drawPath(p);
 184      }
 185      int sampleCount = vTimeStamp.size();
 186      if (ttpoint >= 0 && ttpoint < sampleCount) {
 187          painter.setPen(Qt::yellow);
 188          int w = width() - XMARGIN * 2;
 189          int x = XMARGIN + w - w * ttpoint / DESIRED_SAMPLES;
 190          int y = y_value(std::max(vSamplesIn.at(ttpoint), vSamplesOut.at(ttpoint)));
 191          painter.drawEllipse(QPointF(x, y), 3, 3);
 192          QString strTime;
 193          int64_t sampleTime = vTimeStamp.at(ttpoint);
 194          int age = GetTime() - sampleTime/1000;
 195          if (age < 60*60*23)
 196              strTime = QString::fromStdString(FormatISO8601Time(sampleTime/1000));
 197          else
 198              strTime = QString::fromStdString(FormatISO8601DateTime(sampleTime/1000));
 199          int milliseconds_between_samples = 1000;
 200          if (ttpoint > 0)
 201              milliseconds_between_samples = std::min(milliseconds_between_samples, int(vTimeStamp.at(ttpoint-1) - sampleTime));
 202          if (ttpoint + 1 < sampleCount)
 203              milliseconds_between_samples = std::min(milliseconds_between_samples, int(sampleTime - vTimeStamp.at(ttpoint+1)));
 204          if (milliseconds_between_samples < 1000)
 205              strTime += QString::fromStdString(strprintf(".%03d", (sampleTime%1000)));
 206          QString strData = tr("In") + " " + GUIUtil::formatBytesps(vSamplesIn.at(ttpoint)*1000) + "\n" + tr("Out") + " " + GUIUtil::formatBytesps(vSamplesOut.at(ttpoint)*1000);
 207          // Line below allows ToolTip to move faster than once every 10 seconds.
 208          QToolTip::showText(QPoint(x + x_offset, y + y_offset), strTime + "\n. " + strData);
 209          QToolTip::showText(QPoint(x + x_offset, y + y_offset), strTime + "\n  " + strData);
 210          tt_time = GetTime();
 211      } else
 212          QToolTip::hideText();
 213  }
 214  
 215  void TrafficGraphWidget::updateToolTip()
 216  {
 217      if (!QToolTip::isVisible()) {
 218          if (ttpoint >= 0) { // Remove the yellow circle if the ToolTip has gone due to mouse moving elsewhere.
 219              ttpoint = -1;
 220              update();
 221          }
 222      } else if (GetTime() >= tt_time + 9) { // ToolTip is about to expire so refresh it.
 223          update();
 224      }
 225  }
 226  
 227  void TrafficGraphWidget::updateRates()
 228  {
 229      if(!clientModel) return;
 230  
 231      int64_t nTime = TicksSinceEpoch<std::chrono::milliseconds>(SystemClock::now());
 232      static int64_t nLastTime = nTime - timer->interval();
 233      int nRealInterval = nTime - nLastTime;
 234      quint64 bytesIn = clientModel->node().getTotalBytesRecv(),
 235              bytesOut = clientModel->node().getTotalBytesSent();
 236      float in_rate_kilobytes_per_sec = static_cast<float>(bytesIn - nLastBytesIn) / nRealInterval;
 237      float out_rate_kilobytes_per_sec = static_cast<float>(bytesOut - nLastBytesOut) / nRealInterval;
 238      vSamplesIn.push_front(in_rate_kilobytes_per_sec);
 239      vSamplesOut.push_front(out_rate_kilobytes_per_sec);
 240      vTimeStamp.push_front(nLastTime);
 241      nLastTime = nTime;
 242      nLastBytesIn = bytesIn;
 243      nLastBytesOut = bytesOut;
 244  
 245      while(vSamplesIn.size() > DESIRED_SAMPLES) {
 246          vSamplesIn.pop_back();
 247      }
 248      while(vSamplesOut.size() > DESIRED_SAMPLES) {
 249          vSamplesOut.pop_back();
 250      }
 251      while(vTimeStamp.size() > DESIRED_SAMPLES) {
 252          vTimeStamp.pop_back();
 253      }
 254  
 255      float tmax = 0.0f;
 256      for (const float f : vSamplesIn) {
 257          if(f > tmax) tmax = f;
 258      }
 259      for (const float f : vSamplesOut) {
 260          if(f > tmax) tmax = f;
 261      }
 262      fMax = tmax;
 263      if (ttpoint >=0 && ttpoint < vTimeStamp.size()) ttpoint++; // Move the selected point to the left
 264      update();
 265  }
 266  
 267  void TrafficGraphWidget::setGraphRange(std::chrono::minutes new_range)
 268  {
 269      m_range = new_range;
 270      const auto msecs_per_sample{std::chrono::duration_cast<std::chrono::milliseconds>(m_range) / DESIRED_SAMPLES};
 271      timer->stop();
 272      timer->setInterval(msecs_per_sample);
 273  
 274      clear();
 275  }
 276  
 277  void TrafficGraphWidget::clear()
 278  {
 279      timer->stop();
 280  
 281      vSamplesOut.clear();
 282      vSamplesIn.clear();
 283      vTimeStamp.clear();
 284      fMax = 0.0f;
 285  
 286      if(clientModel) {
 287          nLastBytesIn = clientModel->node().getTotalBytesRecv();
 288          nLastBytesOut = clientModel->node().getTotalBytesSent();
 289      }
 290      timer->start();
 291  }
 292