/* * Copyright (C) 2015-2016, Mike Walters * * This file is part of inspectrum. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "plotview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "plots.h" PlotView::PlotView(InputSource *input) : cursors(this), viewRange({0, 0}) { mainSampleSource = input; setDragMode(QGraphicsView::ScrollHandDrag); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setMouseTracking(true); enableCursors(false); connect(&cursors, SIGNAL(cursorsMoved()), this, SLOT(cursorsMoved())); spectrogramPlot = new SpectrogramPlot(std::shared_ptr>>(mainSampleSource)); auto tunerOutput = std::dynamic_pointer_cast>>(spectrogramPlot->output()); enableScales(true); addPlot(spectrogramPlot); mainSampleSource->subscribe(this); } void PlotView::addPlot(Plot *plot) { plots.emplace_back(plot); connect(plot, &Plot::repaint, this, &PlotView::repaint); } void PlotView::contextMenuEvent(QContextMenuEvent * event) { QMenu menu; // Get selected plot Plot *selectedPlot = nullptr; auto it = plots.begin(); int y = -verticalScrollBar()->value(); for (; it != plots.end(); it++) { auto&& plot = *it; if (range_t{y, y + plot->height()}.contains(event->pos().y())) { selectedPlot = plot.get(); break; } y += plot->height(); } if (selectedPlot == nullptr) return; // Add actions to add derived plots // that are compatible with selectedPlot's output QMenu *plotsMenu = menu.addMenu("Add derived plot"); auto src = selectedPlot->output(); auto compatiblePlots = as_range(Plots::plots.equal_range(src->sampleType())); for (auto p : compatiblePlots) { auto plotInfo = p.second; auto action = new QAction(QString("Add %1").arg(plotInfo.name), plotsMenu); auto plotCreator = plotInfo.creator; connect( action, &QAction::triggered, this, [=]() { addPlot(plotCreator(src)); } ); plotsMenu->addAction(action); } // Add action to extract symbols from selected plot auto extract = new QAction("Extract symbols (to stdout)...", &menu); connect( extract, &QAction::triggered, this, [=]() { extractSymbols(src); } ); extract->setEnabled(cursorsEnabled && (src->sampleType() == typeid(float))); menu.addAction(extract); // Add action to export the selected samples into a file auto save = new QAction("Export samples to file...", &menu); connect( save, &QAction::triggered, this, [=]() { if (selectedPlot == spectrogramPlot) { exportSamples(spectrogramPlot->tunerEnabled() ? spectrogramPlot->output() : spectrogramPlot->input()); } else { exportSamples(src); } } ); menu.addAction(save); // Add action to remove the selected plot auto rem = new QAction("Remove plot", &menu); connect( rem, &QAction::triggered, this, [=]() { plots.erase(it); } ); // Don't allow remove the first plot (the spectrogram) rem->setEnabled(it != plots.begin()); menu.addAction(rem); updateViewRange(false); if(menu.exec(event->globalPos())) updateView(false); } void PlotView::cursorsMoved() { selectedSamples = { columnToSample(horizontalScrollBar()->value() + cursors.selection().minimum), columnToSample(horizontalScrollBar()->value() + cursors.selection().maximum) }; emitTimeSelection(); viewport()->update(); } void PlotView::emitTimeSelection() { size_t sampleCount = selectedSamples.length(); float selectionTime = sampleCount / (float)mainSampleSource->rate(); emit timeSelectionChanged(selectionTime); } void PlotView::enableCursors(bool enabled) { cursorsEnabled = enabled; if (enabled) { int margin = viewport()->rect().width() / 3; cursors.setSelection({viewport()->rect().left() + margin, viewport()->rect().right() - margin}); cursorsMoved(); } viewport()->update(); } bool PlotView::viewportEvent(QEvent *event) { // Handle wheel events for zooming (before the parent's handler to stop normal scrolling) if (event->type() == QEvent::Wheel) { QWheelEvent *wheelEvent = (QWheelEvent*)event; if (QApplication::keyboardModifiers() & Qt::ControlModifier) { bool canZoomIn = zoomLevel < fftSize; bool canZoomOut = zoomLevel > 1; int delta = wheelEvent->angleDelta().y(); if ((delta > 0 && canZoomIn) || (delta < 0 && canZoomOut)) { scrollZoomStepsAccumulated += delta; // `updateViewRange()` keeps the center sample in the same place after zoom. Apply // a scroll adjustment to keep the sample under the mouse cursor in the same place instead. zoomPos = wheelEvent->pos().x(); zoomSample = columnToSample(horizontalScrollBar()->value() + zoomPos); if (scrollZoomStepsAccumulated >= 120) { scrollZoomStepsAccumulated -= 120; zoomIn(); } else if (scrollZoomStepsAccumulated <= -120) { scrollZoomStepsAccumulated += 120; zoomOut(); } } return true; } } // Pass mouse events to individual plot objects if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonRelease || event->type() == QEvent::Leave) { QMouseEvent *mouseEvent = static_cast(event); int plotY = -verticalScrollBar()->value(); for (auto&& plot : plots) { bool result = plot->mouseEvent( event->type(), QMouseEvent( event->type(), QPoint(mouseEvent->pos().x(), mouseEvent->pos().y() - plotY), mouseEvent->button(), mouseEvent->buttons(), QApplication::keyboardModifiers() ) ); if (result) return true; plotY += plot->height(); } if (cursorsEnabled) if (cursors.mouseEvent(event->type(), *mouseEvent)) return true; } // Handle parent eveents return QGraphicsView::viewportEvent(event); } void PlotView::extractSymbols(std::shared_ptr src) { if (!cursorsEnabled) return; auto floatSrc = std::dynamic_pointer_cast>(src); if (!floatSrc) return; auto samples = floatSrc->getSamples(selectedSamples.minimum, selectedSamples.length()); auto step = (float)selectedSamples.length() / cursors.segments(); auto symbols = std::vector(); for (auto i = step / 2; i < selectedSamples.length(); i += step) { symbols.push_back(samples[i]); } for (auto f : symbols) std::cout << f << ", "; std::cout << std::endl << std::flush; } void PlotView::exportSamples(std::shared_ptr src) { if (src->sampleType() == typeid(std::complex)) { exportSamples>(src); } else { exportSamples(src); } } template void PlotView::exportSamples(std::shared_ptr src) { auto sampleSrc = std::dynamic_pointer_cast>(src); if (!sampleSrc) { return; } QFileDialog dialog(this); dialog.setAcceptMode(QFileDialog::AcceptSave); dialog.setFileMode(QFileDialog::AnyFile); dialog.setNameFilter(getFileNameFilter()); dialog.setOption(QFileDialog::DontUseNativeDialog, true); QGroupBox groupBox("Selection To Export", &dialog); QVBoxLayout vbox(&groupBox); QRadioButton cursorSelection("Cursor Selection", &groupBox); QRadioButton currentView("Current View", &groupBox); QRadioButton completeFile("Complete File (Experimental)", &groupBox); if (cursorsEnabled) { cursorSelection.setChecked(true); } else { currentView.setChecked(true); cursorSelection.setEnabled(false); } vbox.addWidget(&cursorSelection); vbox.addWidget(¤tView); vbox.addWidget(&completeFile); vbox.addStretch(1); groupBox.setLayout(&vbox); QGridLayout *l = dialog.findChild(); l->addWidget(&groupBox, 4, 1); QGroupBox groupBox2("Decimation"); QSpinBox decimation(&groupBox2); decimation.setValue(1 / sampleSrc->relativeBandwidth()); QVBoxLayout vbox2; vbox2.addWidget(&decimation); groupBox2.setLayout(&vbox2); l->addWidget(&groupBox2, 4, 2); if (dialog.exec()) { QStringList fileNames = dialog.selectedFiles(); size_t start, end; if (cursorSelection.isChecked()) { start = selectedSamples.minimum; end = start + selectedSamples.length(); } else if(currentView.isChecked()) { start = viewRange.minimum; end = start + viewRange.length(); } else { start = 0; end = sampleSrc->count(); } std::ofstream os (fileNames[0].toStdString(), std::ios::binary); size_t index; // viewRange.length() is used as some less arbitrary step value size_t step = viewRange.length(); for (index = start; index < end; index += step) { size_t length = std::min(step, end - index); auto samples = sampleSrc->getSamples(index, length); if (samples != nullptr) { for (auto i = 0; i < length; i += decimation.value()) { os.write((const char*)&samples[i], sizeof(SOURCETYPE)); } } } } } void PlotView::invalidateEvent() { horizontalScrollBar()->setMinimum(0); horizontalScrollBar()->setMaximum(sampleToColumn(mainSampleSource->count())); } void PlotView::repaint() { viewport()->update(); } void PlotView::setCursorSegments(int segments) { // Calculate number of samples per segment float sampPerSeg = (float)selectedSamples.length() / cursors.segments(); // Alter selection to keep samples per segment the same selectedSamples.maximum = selectedSamples.minimum + (segments * sampPerSeg + 0.5f); cursors.setSegments(segments); updateView(); emitTimeSelection(); } void PlotView::setFFTSize(int fftSize) { setFFTAndZoom(fftSize, zoomLevel); } void PlotView::setFFTAndZoom(int size, int zoom) { // Set new FFT size fftSize = size; if (spectrogramPlot != nullptr) spectrogramPlot->setFFTSize(size); // Set new zoom level zoomLevel = std::min(fftSize, zoom); if (spectrogramPlot != nullptr) spectrogramPlot->setZoomLevel(zoom); // Update horizontal (time) scrollbar horizontalScrollBar()->setSingleStep(10); horizontalScrollBar()->setPageStep(100); updateView(true); } void PlotView::setPowerMin(int power) { powerMin = power; if (spectrogramPlot != nullptr) spectrogramPlot->setPowerMin(power); updateView(); } void PlotView::setPowerMax(int power) { powerMax = power; if (spectrogramPlot != nullptr) spectrogramPlot->setPowerMax(power); updateView(); } void PlotView::paintEvent(QPaintEvent *event) { if (mainSampleSource == nullptr) return; QRect rect = QRect(0, 0, width(), height()); QPainter painter(viewport()); painter.fillRect(rect, Qt::black); #define PLOT_LAYER(paintFunc) \ { \ int y = -verticalScrollBar()->value(); \ for (auto&& plot : plots) { \ QRect rect = QRect(0, y, width(), plot->height()); \ plot->paintFunc(painter, rect, viewRange); \ y += plot->height(); \ } \ } PLOT_LAYER(paintBack); PLOT_LAYER(paintMid); PLOT_LAYER(paintFront); if (cursorsEnabled) cursors.paintFront(painter, rect, viewRange); if (timeScaleEnabled) { paintTimeScale(painter, rect, viewRange); } #undef PLOT_LAYER } void PlotView::paintTimeScale(QPainter &painter, QRect &rect, range_t sampleRange) { float startTime = (float)sampleRange.minimum / sampleRate; float stopTime = (float)sampleRange.maximum / sampleRate; float duration = stopTime - startTime; if (duration <= 0) return; painter.save(); QPen pen(Qt::white, 1, Qt::SolidLine); painter.setPen(pen); QFontMetrics fm(painter.font()); int tickWidth = 80; int maxTicks = rect.width() / tickWidth; double durationPerTick = 10 * pow(10, floor(log(duration / maxTicks) / log(10))); double firstTick = int(startTime / durationPerTick) * durationPerTick; double tick = firstTick; while (tick <= stopTime) { size_t tickSample = tick * sampleRate; int tickLine = sampleToColumn(tickSample - sampleRange.minimum); char buf[128]; snprintf(buf, sizeof(buf), "%.06f", tick); painter.drawLine(tickLine, 0, tickLine, 30); painter.drawText(tickLine + 2, 25, buf); tick += durationPerTick; } // Draw small ticks durationPerTick /= 10; firstTick = int(startTime / durationPerTick) * durationPerTick; tick = firstTick; while (tick <= stopTime) { size_t tickSample = tick * sampleRate; int tickLine = sampleToColumn(tickSample - sampleRange.minimum); painter.drawLine(tickLine, 0, tickLine, 10); tick += durationPerTick; } painter.restore(); } int PlotView::plotsHeight() { int height = 0; for (auto&& plot : plots) { height += plot->height(); } return height; } void PlotView::resizeEvent(QResizeEvent * event) { updateView(); } size_t PlotView::samplesPerColumn() { return fftSize / zoomLevel; } void PlotView::scrollContentsBy(int dx, int dy) { updateView(); } void PlotView::updateViewRange(bool reCenter) { // Update current view auto start = columnToSample(horizontalScrollBar()->value()); viewRange = {start, std::min(start + columnToSample(width()), mainSampleSource->count())}; // Adjust time offset to zoom around central sample if (reCenter) { horizontalScrollBar()->setValue( sampleToColumn(zoomSample) - zoomPos ); } // zoomSample = viewRange.minimum + viewRange.length() / 2; // zoomPos = width() / 2; } void PlotView::updateView(bool reCenter) { horizontalScrollBar()->setMaximum(std::max(0, sampleToColumn(mainSampleSource->count()) - width())); verticalScrollBar()->setMaximum(std::max(0, plotsHeight() - viewport()->height())); updateViewRange(reCenter); // Update cursors range_t newSelection = { sampleToColumn(selectedSamples.minimum) - horizontalScrollBar()->value(), sampleToColumn(selectedSamples.maximum) - horizontalScrollBar()->value() }; cursors.setSelection(newSelection); // Re-paint viewport()->update(); } void PlotView::setSampleRate(size_t rate) { sampleRate = rate; if (spectrogramPlot != nullptr) spectrogramPlot->setSampleRate(rate); emitTimeSelection(); } void PlotView::enableScales(bool enabled) { timeScaleEnabled = enabled; if (spectrogramPlot != nullptr) spectrogramPlot->enableScales(enabled); viewport()->update(); } int PlotView::sampleToColumn(size_t sample) { return sample / samplesPerColumn(); } size_t PlotView::columnToSample(int col) { return col * samplesPerColumn(); } void PlotView::zoomIn() { if (zoomLevel < fftSize) setFFTAndZoom(fftSize, zoomLevel * 2); } void PlotView::zoomOut() { if (zoomLevel > 1) setFFTAndZoom(fftSize, zoomLevel / 2); }