1 /*
2 * Copyright (C) 2015-2016, Mike Walters <mike@flomp.net>
3 *
4 * This file is part of inspectrum.
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "plotview.h"
21 #include <iostream>
22 #include <fstream>
23 #include <QApplication>
24 #include <QDebug>
25 #include <QMenu>
26 #include <QPainter>
27 #include <QScrollBar>
28 #include <QFileDialog>
29 #include <QRadioButton>
30 #include <QVBoxLayout>
31 #include <QGroupBox>
32 #include <QGridLayout>
33 #include <QSpinBox>
34 #include <QClipboard>
35 #include "plots.h"
36
PlotView(InputSource * input)37 PlotView::PlotView(InputSource *input) : cursors(this), viewRange({0, 0})
38 {
39 mainSampleSource = input;
40 setDragMode(QGraphicsView::ScrollHandDrag);
41 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
42 setMouseTracking(true);
43 enableCursors(false);
44 connect(&cursors, &Cursors::cursorsMoved, this, &PlotView::cursorsMoved);
45
46 spectrogramPlot = new SpectrogramPlot(std::shared_ptr<SampleSource<std::complex<float>>>(mainSampleSource));
47 auto tunerOutput = std::dynamic_pointer_cast<SampleSource<std::complex<float>>>(spectrogramPlot->output());
48
49 enableScales(true);
50
51 addPlot(spectrogramPlot);
52
53 mainSampleSource->subscribe(this);
54 }
55
addPlot(Plot * plot)56 void PlotView::addPlot(Plot *plot)
57 {
58 plots.emplace_back(plot);
59 connect(plot, &Plot::repaint, this, &PlotView::repaint);
60 }
61
contextMenuEvent(QContextMenuEvent * event)62 void PlotView::contextMenuEvent(QContextMenuEvent * event)
63 {
64 QMenu menu;
65
66 // Get selected plot
67 Plot *selectedPlot = nullptr;
68 auto it = plots.begin();
69 int y = -verticalScrollBar()->value();
70 for (; it != plots.end(); it++) {
71 auto&& plot = *it;
72 if (range_t<int>{y, y + plot->height()}.contains(event->pos().y())) {
73 selectedPlot = plot.get();
74 break;
75 }
76 y += plot->height();
77 }
78 if (selectedPlot == nullptr)
79 return;
80
81 // Add actions to add derived plots
82 // that are compatible with selectedPlot's output
83 QMenu *plotsMenu = menu.addMenu("Add derived plot");
84 auto src = selectedPlot->output();
85 auto compatiblePlots = as_range(Plots::plots.equal_range(src->sampleType()));
86 for (auto p : compatiblePlots) {
87 auto plotInfo = p.second;
88 auto action = new QAction(QString("Add %1").arg(plotInfo.name), plotsMenu);
89 auto plotCreator = plotInfo.creator;
90 connect(
91 action, &QAction::triggered,
92 this, [=]() {
93 addPlot(plotCreator(src));
94 }
95 );
96 plotsMenu->addAction(action);
97 }
98
99 // Add submenu for extracting symbols
100 QMenu *extractMenu = menu.addMenu("Extract symbols");
101 // Add action to extract symbols from selected plot to stdout
102 auto extract = new QAction("To stdout", extractMenu);
103 connect(
104 extract, &QAction::triggered,
105 this, [=]() {
106 extractSymbols(src, false);
107 }
108 );
109 extract->setEnabled(cursorsEnabled && (src->sampleType() == typeid(float)));
110 extractMenu->addAction(extract);
111
112 // Add action to extract symbols from selected plot to clipboard
113 auto extractClipboard = new QAction("Copy to clipboard", extractMenu);
114 connect(
115 extractClipboard, &QAction::triggered,
116 this, [=]() {
117 extractSymbols(src, true);
118 }
119 );
120 extractClipboard->setEnabled(cursorsEnabled && (src->sampleType() == typeid(float)));
121 extractMenu->addAction(extractClipboard);
122
123 // Add action to export the selected samples into a file
124 auto save = new QAction("Export samples to file...", &menu);
125 connect(
126 save, &QAction::triggered,
127 this, [=]() {
128 if (selectedPlot == spectrogramPlot) {
129 exportSamples(spectrogramPlot->tunerEnabled() ? spectrogramPlot->output() : spectrogramPlot->input());
130 } else {
131 exportSamples(src);
132 }
133 }
134 );
135 menu.addAction(save);
136
137 // Add action to remove the selected plot
138 auto rem = new QAction("Remove plot", &menu);
139 connect(
140 rem, &QAction::triggered,
141 this, [=]() {
142 plots.erase(it);
143 }
144 );
145 // Don't allow remove the first plot (the spectrogram)
146 rem->setEnabled(it != plots.begin());
147 menu.addAction(rem);
148
149 updateViewRange(false);
150 if(menu.exec(event->globalPos()))
151 updateView(false);
152 }
153
cursorsMoved()154 void PlotView::cursorsMoved()
155 {
156 selectedSamples = {
157 columnToSample(horizontalScrollBar()->value() + cursors.selection().minimum),
158 columnToSample(horizontalScrollBar()->value() + cursors.selection().maximum)
159 };
160
161 emitTimeSelection();
162 viewport()->update();
163 }
164
emitTimeSelection()165 void PlotView::emitTimeSelection()
166 {
167 size_t sampleCount = selectedSamples.length();
168 float selectionTime = sampleCount / (float)mainSampleSource->rate();
169 emit timeSelectionChanged(selectionTime);
170 }
171
enableCursors(bool enabled)172 void PlotView::enableCursors(bool enabled)
173 {
174 cursorsEnabled = enabled;
175 if (enabled) {
176 int margin = viewport()->rect().width() / 3;
177 cursors.setSelection({viewport()->rect().left() + margin, viewport()->rect().right() - margin});
178 cursorsMoved();
179 }
180 viewport()->update();
181 }
182
viewportEvent(QEvent * event)183 bool PlotView::viewportEvent(QEvent *event) {
184 // Handle wheel events for zooming (before the parent's handler to stop normal scrolling)
185 if (event->type() == QEvent::Wheel) {
186 QWheelEvent *wheelEvent = (QWheelEvent*)event;
187 if (QApplication::keyboardModifiers() & Qt::ControlModifier) {
188 bool canZoomIn = zoomLevel < fftSize;
189 bool canZoomOut = zoomLevel > 1;
190 int delta = wheelEvent->angleDelta().y();
191 if ((delta > 0 && canZoomIn) || (delta < 0 && canZoomOut)) {
192 scrollZoomStepsAccumulated += delta;
193
194 // `updateViewRange()` keeps the center sample in the same place after zoom. Apply
195 // a scroll adjustment to keep the sample under the mouse cursor in the same place instead.
196 zoomPos = wheelEvent->pos().x();
197 zoomSample = columnToSample(horizontalScrollBar()->value() + zoomPos);
198 if (scrollZoomStepsAccumulated >= 120) {
199 scrollZoomStepsAccumulated -= 120;
200 emit zoomIn();
201 } else if (scrollZoomStepsAccumulated <= -120) {
202 scrollZoomStepsAccumulated += 120;
203 emit zoomOut();
204 }
205 }
206 return true;
207 }
208 }
209
210 // Pass mouse events to individual plot objects
211 if (event->type() == QEvent::MouseButtonPress ||
212 event->type() == QEvent::MouseMove ||
213 event->type() == QEvent::MouseButtonRelease ||
214 event->type() == QEvent::Leave) {
215
216 QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
217
218 int plotY = -verticalScrollBar()->value();
219 for (auto&& plot : plots) {
220 bool result = plot->mouseEvent(
221 event->type(),
222 QMouseEvent(
223 event->type(),
224 QPoint(mouseEvent->pos().x(), mouseEvent->pos().y() - plotY),
225 mouseEvent->button(),
226 mouseEvent->buttons(),
227 QApplication::keyboardModifiers()
228 )
229 );
230 if (result)
231 return true;
232 plotY += plot->height();
233 }
234
235 if (cursorsEnabled)
236 if (cursors.mouseEvent(event->type(), *mouseEvent))
237 return true;
238 }
239
240 // Handle parent eveents
241 return QGraphicsView::viewportEvent(event);
242 }
243
extractSymbols(std::shared_ptr<AbstractSampleSource> src,bool toClipboard)244 void PlotView::extractSymbols(std::shared_ptr<AbstractSampleSource> src,
245 bool toClipboard)
246 {
247 if (!cursorsEnabled)
248 return;
249 auto floatSrc = std::dynamic_pointer_cast<SampleSource<float>>(src);
250 if (!floatSrc)
251 return;
252 auto samples = floatSrc->getSamples(selectedSamples.minimum, selectedSamples.length());
253 auto step = (float)selectedSamples.length() / cursors.segments();
254 auto symbols = std::vector<float>();
255 for (auto i = step / 2; i < selectedSamples.length(); i += step)
256 {
257 symbols.push_back(samples[i]);
258 }
259 if (!toClipboard) {
260 for (auto f : symbols)
261 std::cout << f << ", ";
262 std::cout << std::endl << std::flush;
263 } else {
264 QClipboard *clipboard = QGuiApplication::clipboard();
265 QString symbolText;
266 QTextStream symbolStream(&symbolText);
267 for (auto f : symbols)
268 symbolStream << f << ", ";
269 clipboard->setText(symbolText);
270 }
271 }
272
exportSamples(std::shared_ptr<AbstractSampleSource> src)273 void PlotView::exportSamples(std::shared_ptr<AbstractSampleSource> src)
274 {
275 if (src->sampleType() == typeid(std::complex<float>)) {
276 exportSamples<std::complex<float>>(src);
277 } else {
278 exportSamples<float>(src);
279 }
280 }
281
282 template<typename SOURCETYPE>
exportSamples(std::shared_ptr<AbstractSampleSource> src)283 void PlotView::exportSamples(std::shared_ptr<AbstractSampleSource> src)
284 {
285 auto sampleSrc = std::dynamic_pointer_cast<SampleSource<SOURCETYPE>>(src);
286 if (!sampleSrc) {
287 return;
288 }
289
290 QFileDialog dialog(this);
291 dialog.setAcceptMode(QFileDialog::AcceptSave);
292 dialog.setFileMode(QFileDialog::AnyFile);
293 dialog.setNameFilter(getFileNameFilter<SOURCETYPE>());
294 dialog.setOption(QFileDialog::DontUseNativeDialog, true);
295
296 QGroupBox groupBox("Selection To Export", &dialog);
297 QVBoxLayout vbox(&groupBox);
298
299 QRadioButton cursorSelection("Cursor Selection", &groupBox);
300 QRadioButton currentView("Current View", &groupBox);
301 QRadioButton completeFile("Complete File (Experimental)", &groupBox);
302
303 if (cursorsEnabled) {
304 cursorSelection.setChecked(true);
305 } else {
306 currentView.setChecked(true);
307 cursorSelection.setEnabled(false);
308 }
309
310 vbox.addWidget(&cursorSelection);
311 vbox.addWidget(¤tView);
312 vbox.addWidget(&completeFile);
313 vbox.addStretch(1);
314
315 groupBox.setLayout(&vbox);
316
317 QGridLayout *l = dialog.findChild<QGridLayout*>();
318 l->addWidget(&groupBox, 4, 1);
319
320 QGroupBox groupBox2("Decimation");
321 QSpinBox decimation(&groupBox2);
322 decimation.setMinimum(1);
323 decimation.setValue(1 / sampleSrc->relativeBandwidth());
324
325 QVBoxLayout vbox2;
326 vbox2.addWidget(&decimation);
327
328 groupBox2.setLayout(&vbox2);
329 l->addWidget(&groupBox2, 4, 2);
330
331 if (dialog.exec()) {
332 QStringList fileNames = dialog.selectedFiles();
333
334 size_t start, end;
335 if (cursorSelection.isChecked()) {
336 start = selectedSamples.minimum;
337 end = start + selectedSamples.length();
338 } else if(currentView.isChecked()) {
339 start = viewRange.minimum;
340 end = start + viewRange.length();
341 } else {
342 start = 0;
343 end = sampleSrc->count();
344 }
345
346 std::ofstream os (fileNames[0].toStdString(), std::ios::binary);
347
348 size_t index;
349 // viewRange.length() is used as some less arbitrary step value
350 size_t step = viewRange.length();
351
352 for (index = start; index < end; index += step) {
353 size_t length = std::min(step, end - index);
354 auto samples = sampleSrc->getSamples(index, length);
355 if (samples != nullptr) {
356 for (auto i = 0; i < length; i += decimation.value()) {
357 os.write((const char*)&samples[i], sizeof(SOURCETYPE));
358 }
359 }
360 }
361 }
362 }
363
invalidateEvent()364 void PlotView::invalidateEvent()
365 {
366 horizontalScrollBar()->setMinimum(0);
367 horizontalScrollBar()->setMaximum(sampleToColumn(mainSampleSource->count()));
368 }
369
repaint()370 void PlotView::repaint()
371 {
372 viewport()->update();
373 }
374
setCursorSegments(int segments)375 void PlotView::setCursorSegments(int segments)
376 {
377 // Calculate number of samples per segment
378 float sampPerSeg = (float)selectedSamples.length() / cursors.segments();
379
380 // Alter selection to keep samples per segment the same
381 selectedSamples.maximum = selectedSamples.minimum + (segments * sampPerSeg + 0.5f);
382
383 cursors.setSegments(segments);
384 updateView();
385 emitTimeSelection();
386 }
387
setFFTAndZoom(int size,int zoom)388 void PlotView::setFFTAndZoom(int size, int zoom)
389 {
390 // Set new FFT size
391 fftSize = size;
392 if (spectrogramPlot != nullptr)
393 spectrogramPlot->setFFTSize(size);
394
395 // Set new zoom level
396 zoomLevel = zoom;
397 if (spectrogramPlot != nullptr)
398 spectrogramPlot->setZoomLevel(zoom);
399
400 // Update horizontal (time) scrollbar
401 horizontalScrollBar()->setSingleStep(10);
402 horizontalScrollBar()->setPageStep(100);
403
404 updateView(true);
405 }
406
setPowerMin(int power)407 void PlotView::setPowerMin(int power)
408 {
409 powerMin = power;
410 if (spectrogramPlot != nullptr)
411 spectrogramPlot->setPowerMin(power);
412 updateView();
413 }
414
setPowerMax(int power)415 void PlotView::setPowerMax(int power)
416 {
417 powerMax = power;
418 if (spectrogramPlot != nullptr)
419 spectrogramPlot->setPowerMax(power);
420 updateView();
421 }
422
paintEvent(QPaintEvent * event)423 void PlotView::paintEvent(QPaintEvent *event)
424 {
425 if (mainSampleSource == nullptr) return;
426
427 QRect rect = QRect(0, 0, width(), height());
428 QPainter painter(viewport());
429 painter.fillRect(rect, Qt::black);
430
431
432 #define PLOT_LAYER(paintFunc) \
433 { \
434 int y = -verticalScrollBar()->value(); \
435 for (auto&& plot : plots) { \
436 QRect rect = QRect(0, y, width(), plot->height()); \
437 plot->paintFunc(painter, rect, viewRange); \
438 y += plot->height(); \
439 } \
440 }
441
442 PLOT_LAYER(paintBack);
443 PLOT_LAYER(paintMid);
444 PLOT_LAYER(paintFront);
445 if (cursorsEnabled)
446 cursors.paintFront(painter, rect, viewRange);
447
448 if (timeScaleEnabled) {
449 paintTimeScale(painter, rect, viewRange);
450 }
451
452
453 #undef PLOT_LAYER
454 }
455
paintTimeScale(QPainter & painter,QRect & rect,range_t<size_t> sampleRange)456 void PlotView::paintTimeScale(QPainter &painter, QRect &rect, range_t<size_t> sampleRange)
457 {
458 float startTime = (float)sampleRange.minimum / sampleRate;
459 float stopTime = (float)sampleRange.maximum / sampleRate;
460 float duration = stopTime - startTime;
461
462 if (duration <= 0)
463 return;
464
465 painter.save();
466
467 QPen pen(Qt::white, 1, Qt::SolidLine);
468 painter.setPen(pen);
469 QFontMetrics fm(painter.font());
470
471 int tickWidth = 80;
472 int maxTicks = rect.width() / tickWidth;
473
474 double durationPerTick = 10 * pow(10, floor(log(duration / maxTicks) / log(10)));
475
476 double firstTick = int(startTime / durationPerTick) * durationPerTick;
477
478 double tick = firstTick;
479
480 while (tick <= stopTime) {
481
482 size_t tickSample = tick * sampleRate;
483 int tickLine = sampleToColumn(tickSample - sampleRange.minimum);
484
485 char buf[128];
486 snprintf(buf, sizeof(buf), "%.06f", tick);
487 painter.drawLine(tickLine, 0, tickLine, 30);
488 painter.drawText(tickLine + 2, 25, buf);
489
490 tick += durationPerTick;
491 }
492
493 // Draw small ticks
494 durationPerTick /= 10;
495 firstTick = int(startTime / durationPerTick) * durationPerTick;
496 tick = firstTick;
497 while (tick <= stopTime) {
498
499 size_t tickSample = tick * sampleRate;
500 int tickLine = sampleToColumn(tickSample - sampleRange.minimum);
501
502 painter.drawLine(tickLine, 0, tickLine, 10);
503 tick += durationPerTick;
504 }
505
506 painter.restore();
507 }
508
plotsHeight()509 int PlotView::plotsHeight()
510 {
511 int height = 0;
512 for (auto&& plot : plots) {
513 height += plot->height();
514 }
515 return height;
516 }
517
resizeEvent(QResizeEvent * event)518 void PlotView::resizeEvent(QResizeEvent * event)
519 {
520 updateView();
521 }
522
samplesPerColumn()523 size_t PlotView::samplesPerColumn()
524 {
525 return fftSize / zoomLevel;
526 }
527
scrollContentsBy(int dx,int dy)528 void PlotView::scrollContentsBy(int dx, int dy)
529 {
530 updateView();
531 }
532
updateViewRange(bool reCenter)533 void PlotView::updateViewRange(bool reCenter)
534 {
535 // Update current view
536 auto start = columnToSample(horizontalScrollBar()->value());
537 viewRange = {start, std::min(start + columnToSample(width()), mainSampleSource->count())};
538
539 // Adjust time offset to zoom around central sample
540 if (reCenter) {
541 horizontalScrollBar()->setValue(
542 sampleToColumn(zoomSample) - zoomPos
543 );
544 }
545 // zoomSample = viewRange.minimum + viewRange.length() / 2;
546 // zoomPos = width() / 2;
547 }
548
updateView(bool reCenter)549 void PlotView::updateView(bool reCenter)
550 {
551 horizontalScrollBar()->setMaximum(std::max(0, sampleToColumn(mainSampleSource->count()) - width()));
552 verticalScrollBar()->setMaximum(std::max(0, plotsHeight() - viewport()->height()));
553 updateViewRange(reCenter);
554
555 // Update cursors
556 range_t<int> newSelection = {
557 sampleToColumn(selectedSamples.minimum) - horizontalScrollBar()->value(),
558 sampleToColumn(selectedSamples.maximum) - horizontalScrollBar()->value()
559 };
560 cursors.setSelection(newSelection);
561
562 // Re-paint
563 viewport()->update();
564 }
565
setSampleRate(double rate)566 void PlotView::setSampleRate(double rate)
567 {
568 sampleRate = rate;
569
570 if (spectrogramPlot != nullptr)
571 spectrogramPlot->setSampleRate(rate);
572
573 emitTimeSelection();
574 }
575
enableScales(bool enabled)576 void PlotView::enableScales(bool enabled)
577 {
578 timeScaleEnabled = enabled;
579
580 if (spectrogramPlot != nullptr)
581 spectrogramPlot->enableScales(enabled);
582
583 viewport()->update();
584 }
585
sampleToColumn(size_t sample)586 int PlotView::sampleToColumn(size_t sample)
587 {
588 return sample / samplesPerColumn();
589 }
590
columnToSample(int col)591 size_t PlotView::columnToSample(int col)
592 {
593 return col * samplesPerColumn();
594 }
595