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(&currentView);
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