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