/* -*- c++ -*- */ /* + + + This Software is released under the "Simplified BSD License" + + + * Copyright 2010 Moe Wheatley. All rights reserved. * Copyright 2011-2013 Alexandru Csete OZ9AEC * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY Moe Wheatley ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Moe Wheatley OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and documentation are those of the * authors and should not be interpreted as representing official policies, either expressed * or implied, of Moe Wheatley. */ #include #ifndef _MSC_VER #include #else #include #include int gettimeofday(struct timeval * tp, struct timezone * tzp) { // Note: some broken versions only have 8 trailing zero's, the correct epoch has 9 trailing zero's static const uint64_t EPOCH = ((uint64_t) 116444736000000000ULL); SYSTEMTIME system_time; FILETIME file_time; uint64_t time; GetSystemTime( &system_time ); SystemTimeToFileTime( &system_time, &file_time ); time = ((uint64_t)file_time.dwLowDateTime ) ; time += ((uint64_t)file_time.dwHighDateTime) << 32; tp->tv_sec = (long) ((time - EPOCH) / 10000000L); tp->tv_usec = (long) (system_time.wMilliseconds * 1000); return 0; } #endif #include #include #include #include #include #include #include #include "plotter.h" #include "bookmarks.h" // Comment out to enable plotter debug messages //#define PLOTTER_DEBUG #define CUR_CUT_DELTA 5 //cursor capture delta in pixels #define FFT_MIN_DB -160.0 #define FFT_MAX_DB 0.0 // Colors of type QRgb in 0xAARRGGBB format (unsigned int) #define PLOTTER_BGD_COLOR 0xFF1F1D1D #define PLOTTER_GRID_COLOR 0xFF444242 #define PLOTTER_TEXT_COLOR 0xFFDADADA #define PLOTTER_CENTER_LINE_COLOR 0xFF788296 #define PLOTTER_FILTER_LINE_COLOR 0xFFFF7171 #define PLOTTER_FILTER_BOX_COLOR 0xFFA0A0A4 // FIXME: Should cache the QColors also static inline bool val_is_out_of_range(double val, double min, double max) { return (val < min || val > max); } static inline bool out_of_range(double min, double max) { return (val_is_out_of_range(min, FFT_MIN_DB, FFT_MAX_DB) || val_is_out_of_range(max, FFT_MIN_DB, FFT_MAX_DB) || max < min + 10.f); } /** Current time in milliseconds since Epoch */ static inline quint64 time_ms(void) { struct timeval tval; gettimeofday(&tval, NULL); return 1e3 * tval.tv_sec + 1e-3 * tval.tv_usec; } #define STATUS_TIP \ "Click, drag or scroll on spectrum to tune. " \ "Drag and scroll X and Y axes for pan and zoom. " \ "Drag filter edges to adjust filter." CPlotter::CPlotter(QWidget *parent) : QFrame(parent) , m_maxSize(1024,768) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setFocusPolicy(Qt::StrongFocus); setAttribute(Qt::WA_PaintOnScreen,false); setAutoFillBackground(false); setAttribute(Qt::WA_OpaquePaintEvent, false); setAttribute(Qt::WA_NoSystemBackground, true); setMouseTracking(true); setTooltipsEnabled(false); setStatusTip(tr(STATUS_TIP)); // default waterfall color scheme for (int i = 0; i < 256; i++) { // level 0: black background if (i < 20) m_ColorTbl[i].setRgb(0, 0, 0); // level 1: black -> blue else if ((i >= 20) && (i < 70)) m_ColorTbl[i].setRgb(0, 0, 140*(i-20)/50); // level 2: blue -> light-blue / greenish else if ((i >= 70) && (i < 100)) m_ColorTbl[i].setRgb(60*(i-70)/30, 125*(i-70)/30, 115*(i-70)/30 + 140); // level 3: light blue -> red else if ((i >= 100) && (i < 150)) m_ColorTbl[i].setRgb(195*(i-100)/50 + 60, 125 - 125*(i-100)/50, 255-(255*(i-100)/50)); // level 4: red -> yellow else if ((i >= 150) && (i < 250)) m_ColorTbl[i].setRgb(255, 255*(i-150)/100, 0); // level 5: red -> white else if (i >= 250) m_ColorTbl[i].setRgb(255, 255, 255*(i-250)/5); } m_PeakHoldActive = false; m_PeakHoldValid = false; m_FftCenter = 0; m_CenterFreq = 144500000; m_DemodCenterFreq = 144500000; m_DemodHiCutFreq = 5000; m_DemodLowCutFreq = -5000; m_FLowCmin = -2500000; m_FLowCmax = -1000; m_FHiCmin = 1000; m_FHiCmax = 2500000; m_symetric = true; m_ClickResolution = 100; m_FilterClickResolution = 100; m_CursorCaptureDelta = CUR_CUT_DELTA; m_FilterBoxEnabled = true; m_CenterLineEnabled = true; m_BookmarksEnabled = true; m_Span = 96000; m_SampleFreq = 96000; m_HorDivs = 12; m_VerDivs = 6; m_PandMaxdB = m_WfMaxdB = 0.f; m_PandMindB = m_WfMindB = -150.f; m_FreqUnits = 1000000; m_CursorCaptured = NOCAP; m_Running = false; m_DrawOverlay = true; m_2DPixmap = QPixmap(0,0); m_OverlayPixmap = QPixmap(0,0); m_WaterfallPixmap = QPixmap(0,0); m_Size = QSize(0,0); m_GrabPosition = 0; m_Percent2DScreen = 45; //percent of screen used for 2D display m_VdivDelta = 30; m_HdivDelta = 70; m_FreqDigits = 3; m_Peaks = QMap(); setPeakDetection(false, 2); m_PeakHoldValid = false; setFftPlotColor(QColor(0xFF,0xFF,0xFF,0xFF)); setFftFill(false); // always update waterfall tlast_wf_ms = 0; msec_per_wfline = 0; wf_span = 0; fft_rate = 15; memset(m_wfbuf, 255, MAX_SCREENSIZE); } CPlotter::~CPlotter() { } QSize CPlotter::minimumSizeHint() const { return QSize(50, 50); } QSize CPlotter::sizeHint() const { return QSize(180, 180); } QSize CPlotter::pixSize() { QSize sz = size(); m_dPixRatio = 1; if (sz.isValid()) { double width = sz.width(); double height = sz.height(); if (width > m_maxSize.width()) { double rt = width / m_maxSize.width(); width /= rt; height /= rt; m_dPixRatio *= rt; } if (height > m_maxSize.height()) { double rt = height / m_maxSize.height(); width /= rt; height /= rt; m_dPixRatio *= rt; } sz = QSize(width+.5,height+.5); } return sz; } void CPlotter::mouseMoveEvent(QMouseEvent* event) { QPoint pt = event->pos(); pt.setX(pt.x() / pixRatio()+.5); pt.setY(pt.y() / pixRatio()+.5); /* mouse ent er / mouse leave events */ if (m_OverlayPixmap.rect().contains(pt)) { //is in Overlay bitmap region if (event->buttons() == Qt::NoButton) { bool onTag = false; if(pt.y() < 15 * 10) // FIXME { for(int i = 0; i < m_BookmarkTags.size() && !onTag; i++) { if (m_BookmarkTags[i].first.contains(event->pos())) onTag = true; } } // if no mouse button monitor grab regions and change cursor icon if (onTag) { setCursor(QCursor(Qt::PointingHandCursor)); m_CursorCaptured = BOOKMARK; } else if (isPointCloseTo(pt.x(), m_DemodFreqX, m_CursorCaptureDelta)) { // in move demod box center frequency region if (CENTER != m_CursorCaptured) setCursor(QCursor(Qt::SizeHorCursor)); m_CursorCaptured = CENTER; if (m_TooltipsEnabled) QToolTip::showText(event->globalPos(), QString("Demod: %1 kHz") .arg(m_DemodCenterFreq/1.e3f, 0, 'f', 3), this); } else if (isPointCloseTo(pt.x(), m_DemodHiCutFreqX, m_CursorCaptureDelta)) { // in move demod hicut region if (RIGHT != m_CursorCaptured) setCursor(QCursor(Qt::SizeFDiagCursor)); m_CursorCaptured = RIGHT; if (m_TooltipsEnabled) QToolTip::showText(event->globalPos(), QString("High cut: %1 Hz") .arg(m_DemodHiCutFreq), this); } else if (isPointCloseTo(pt.x(), m_DemodLowCutFreqX, m_CursorCaptureDelta)) { // in move demod lowcut region if (LEFT != m_CursorCaptured) setCursor(QCursor(Qt::SizeBDiagCursor)); m_CursorCaptured = LEFT; if (m_TooltipsEnabled) QToolTip::showText(event->globalPos(), QString("Low cut: %1 Hz") .arg(m_DemodLowCutFreq), this); } else if (isPointCloseTo(pt.x(), m_YAxisWidth/2, m_YAxisWidth/2)) { if (YAXIS != m_CursorCaptured) setCursor(QCursor(Qt::OpenHandCursor)); m_CursorCaptured = YAXIS; if (m_TooltipsEnabled) QToolTip::hideText(); } else if (isPointCloseTo(pt.y(), m_XAxisYCenter, m_CursorCaptureDelta+5)) { if (XAXIS != m_CursorCaptured) setCursor(QCursor(Qt::OpenHandCursor)); m_CursorCaptured = XAXIS; if (m_TooltipsEnabled) QToolTip::hideText(); } else { //if not near any grab boundaries if (NOCAP != m_CursorCaptured) { setCursor(QCursor(Qt::ArrowCursor)); m_CursorCaptured = NOCAP; } if (m_TooltipsEnabled) QToolTip::showText(event->globalPos(), QString("F: %1 kHz") .arg(freqFromX(pt.x())/1.e3f, 0, 'f', 3), this); } m_GrabPosition = 0; } } else { // not in Overlay region if (event->buttons() == Qt::NoButton) { if (NOCAP != m_CursorCaptured) setCursor(QCursor(Qt::ArrowCursor)); m_CursorCaptured = NOCAP; m_GrabPosition = 0; } if (m_TooltipsEnabled) { QDateTime tt; tt.setMSecsSinceEpoch(msecFromY(pt.y())); QToolTip::showText(event->globalPos(), QString("%1\n%2 kHz") .arg(tt.toString("yyyy.MM.dd hh:mm:ss.zzz")) .arg(freqFromX(pt.x())/1.e3f, 0, 'f', 3), this); } } // process mouse moves while in cursor capture modes if (YAXIS == m_CursorCaptured) { if (event->buttons() & Qt::LeftButton) { setCursor(QCursor(Qt::ClosedHandCursor)); // move Y scale up/down double delta_px = m_Yzero - pt.y(); double delta_db = delta_px * fabs(m_PandMindB - m_PandMaxdB) / (double)m_OverlayPixmap.height(); m_PandMindB -= delta_db; m_PandMaxdB -= delta_db; if (out_of_range(m_PandMindB, m_PandMaxdB)) { m_PandMindB += delta_db; m_PandMaxdB += delta_db; } else { emit pandapterRangeChanged(m_PandMindB, m_PandMaxdB); if (m_Running) m_DrawOverlay = true; else drawOverlay(); m_PeakHoldValid = false; m_Yzero = pt.y(); } } } else if (XAXIS == m_CursorCaptured) { if (event->buttons() & (Qt::LeftButton | Qt::MiddleButton)) { setCursor(QCursor(Qt::ClosedHandCursor)); // pan viewable range or move center frequency int delta_px = m_Xzero - pt.x(); qint64 delta_hz = delta_px * m_Span / m_OverlayPixmap.width(); if (event->buttons() & Qt::MiddleButton) { m_CenterFreq += delta_hz; m_DemodCenterFreq += delta_hz; emit newCenterFreq(m_CenterFreq); } else { setFftCenterFreq(m_FftCenter + delta_hz); } updateOverlay(); m_PeakHoldValid = false; m_Xzero = pt.x(); } } else if (LEFT == m_CursorCaptured) { // moving in demod lowcut region if (event->buttons() & (Qt::LeftButton | Qt::RightButton)) { // moving in demod lowcut region with left button held if (m_GrabPosition != 0) { m_DemodLowCutFreq = freqFromX(pt.x() - m_GrabPosition ) - m_DemodCenterFreq; m_DemodLowCutFreq = roundFreq(m_DemodLowCutFreq, m_FilterClickResolution); if (m_symetric && (event->buttons() & Qt::LeftButton)) // symetric adjustment { m_DemodHiCutFreq = -m_DemodLowCutFreq; } clampDemodParameters(); emit newFilterFreq(m_DemodLowCutFreq, m_DemodHiCutFreq); if (m_Running) m_DrawOverlay = true; else drawOverlay(); } else { // save initial grab postion from m_DemodFreqX m_GrabPosition = pt.x()-m_DemodLowCutFreqX; } } else if (event->buttons() & ~Qt::NoButton) { setCursor(QCursor(Qt::ArrowCursor)); m_CursorCaptured = NOCAP; } } else if (RIGHT == m_CursorCaptured) { // moving in demod highcut region if (event->buttons() & (Qt::LeftButton | Qt::RightButton)) { // moving in demod highcut region with right button held if (m_GrabPosition != 0) { m_DemodHiCutFreq = freqFromX( pt.x()-m_GrabPosition ) - m_DemodCenterFreq; m_DemodHiCutFreq = roundFreq(m_DemodHiCutFreq, m_FilterClickResolution); if (m_symetric && (event->buttons() & Qt::LeftButton)) // symetric adjustment { m_DemodLowCutFreq = -m_DemodHiCutFreq; } clampDemodParameters(); emit newFilterFreq(m_DemodLowCutFreq, m_DemodHiCutFreq); updateOverlay(); } else { // save initial grab postion from m_DemodFreqX m_GrabPosition = pt.x() - m_DemodHiCutFreqX; } } else if (event->buttons() & ~Qt::NoButton) { setCursor(QCursor(Qt::ArrowCursor)); m_CursorCaptured = NOCAP; } } else if (CENTER == m_CursorCaptured) { // moving inbetween demod lowcut and highcut region if (event->buttons() & Qt::LeftButton) { // moving inbetween demod lowcut and highcut region with left button held if (m_GrabPosition != 0) { m_DemodCenterFreq = roundFreq(freqFromX(pt.x() - m_GrabPosition), m_ClickResolution ); emit newDemodFreq(m_DemodCenterFreq, m_DemodCenterFreq - m_CenterFreq); updateOverlay(); m_PeakHoldValid = false; } else { // save initial grab postion from m_DemodFreqX m_GrabPosition = pt.x() - m_DemodFreqX; } } else if (event->buttons() & ~Qt::NoButton) { setCursor(QCursor(Qt::ArrowCursor)); m_CursorCaptured = NOCAP; } } else { // cursor not captured m_GrabPosition = 0; } if (!this->rect().contains(pt)) { if (NOCAP != m_CursorCaptured) setCursor(QCursor(Qt::ArrowCursor)); m_CursorCaptured = NOCAP; } } int CPlotter::getNearestPeak(QPoint pt) { QMap::const_iterator i = m_Peaks.lowerBound(pt.x() - PEAK_CLICK_MAX_H_DISTANCE); QMap::const_iterator upperBound = m_Peaks.upperBound(pt.x() + PEAK_CLICK_MAX_H_DISTANCE); double dist = 1.0e10; int best = -1; for ( ; i != upperBound; i++) { int x = i.key(); int y = i.value(); if (abs(y - pt.y()) > PEAK_CLICK_MAX_V_DISTANCE) continue; double d = powf(y - pt.y(), 2) + powf(x - pt.x(), 2); if (d < dist) { dist = d; best = x; } } return best; } /** Set waterfall span in milliseconds */ void CPlotter::setWaterfallSpan(quint64 span_ms) { wf_span = span_ms; msec_per_wfline = wf_span / m_WaterfallPixmap.height(); clearWaterfall(); } void CPlotter::clearWaterfall() { m_WaterfallPixmap.fill(Qt::black); memset(m_wfbuf, 255, MAX_SCREENSIZE); } /** * @brief Save waterfall to a graphics file * @param filename * @return TRUE if the save successful, FALSE if an erorr occurred. * * We assume that frequency strings are up to date */ bool CPlotter::saveWaterfall(const QString & filename) const { QBrush axis_brush(QColor(0x00, 0x00, 0x00, 0x70), Qt::SolidPattern); QPixmap pixmap(m_WaterfallPixmap); QPainter painter(&pixmap); QRect rect; QDateTime tt; QFont font("sans-serif"); QFontMetrics font_metrics(font); double pixperdiv; int x, y, w, h; int hxa, wya = 85; int i; w = pixmap.width(); h = pixmap.height(); hxa = font_metrics.height() + 5; // height of X axis y = h - hxa; pixperdiv = (double) w / (double) m_HorDivs; painter.setBrush(axis_brush); painter.setPen(QColor(0x0, 0x0, 0x0, 0x70)); painter.drawRect(0, y, w, hxa); painter.drawRect(0, 0, wya, h - hxa - 1); painter.setFont(font); painter.setPen(QColor(0xFF, 0xFF, 0xFF, 0xFF)); // skip last frequency entry for (i = 2; i < m_HorDivs - 1; i++) { // frequency tick marks x = (int)((double)i * pixperdiv); painter.drawLine(x, y, x, y + 5); // frequency strings x = (int)((double)i * pixperdiv - pixperdiv / 2.0); rect.setRect(x, y, (int)pixperdiv, hxa); painter.drawText(rect, Qt::AlignHCenter|Qt::AlignBottom, m_HDivText[i]); } rect.setRect(w - pixperdiv - 10, y, pixperdiv, hxa); painter.drawText(rect, Qt::AlignRight|Qt::AlignBottom, tr("MHz")); quint64 msec; int tdivs = h / 70 + 1; pixperdiv = (double) h / (double) tdivs; tt.setTimeSpec(Qt::OffsetFromUTC); for (i = 1; i < tdivs; i++) { y = (int)((double)i * pixperdiv); if (msec_per_wfline > 0) msec = tlast_wf_ms - y * msec_per_wfline; else msec = tlast_wf_ms - y * 1000 / fft_rate; tt.setMSecsSinceEpoch(msec); rect.setRect(0, y - font_metrics.height(), wya - 5, font_metrics.height()); painter.drawText(rect, Qt::AlignRight|Qt::AlignVCenter, tt.toString("yyyy.MM.dd")); painter.drawLine(wya - 5, y, wya, y); rect.setRect(0, y, wya - 5, font_metrics.height()); painter.drawText(rect, Qt::AlignRight|Qt::AlignVCenter, tt.toString("hh:mm:ss")); } return pixmap.save(filename, 0, -1); } /** Get waterfall time resolution in milleconds / line. */ quint64 CPlotter::getWfTimeRes(void) { if (msec_per_wfline) return msec_per_wfline; else return 1000 * fft_rate / m_WaterfallPixmap.height(); // Auto mode } void CPlotter::setFftRate(int rate_hz) { fft_rate = rate_hz; clearWaterfall(); } // Called when a mouse button is pressed void CPlotter::mousePressEvent(QMouseEvent * event) { QPoint pt = event->pos(); pt.setX(pt.x() / pixRatio()+.5); pt.setY(pt.y() / pixRatio()+.5); if (NOCAP == m_CursorCaptured) { if (isPointCloseTo(pt.x(), m_DemodFreqX, m_CursorCaptureDelta)) { // move demod box center frequency region m_CursorCaptured = CENTER; m_GrabPosition = pt.x() - m_DemodFreqX; } else if (isPointCloseTo(pt.x(), m_DemodLowCutFreqX, m_CursorCaptureDelta)) { // filter low cut m_CursorCaptured = LEFT; m_GrabPosition = pt.x() - m_DemodLowCutFreqX; } else if (isPointCloseTo(pt.x(), m_DemodHiCutFreqX, m_CursorCaptureDelta)) { // filter high cut m_CursorCaptured = RIGHT; m_GrabPosition = pt.x() - m_DemodHiCutFreqX; } else { if (event->buttons() == Qt::LeftButton) { int best = -1; if (m_PeakDetection > 0) best = getNearestPeak(pt); if (best != -1) m_DemodCenterFreq = freqFromX(best); else m_DemodCenterFreq = roundFreq(freqFromX(pt.x()), m_ClickResolution); // if cursor not captured set demod frequency and start demod box capture emit newDemodFreq(m_DemodCenterFreq, m_DemodCenterFreq - m_CenterFreq); // save initial grab postion from m_DemodFreqX // setCursor(QCursor(Qt::CrossCursor)); m_CursorCaptured = CENTER; m_GrabPosition = 1; drawOverlay(); } else if (event->buttons() == Qt::MiddleButton) { // set center freq m_CenterFreq = roundFreq(freqFromX(pt.x()), m_ClickResolution); m_DemodCenterFreq = m_CenterFreq; emit newCenterFreq(m_CenterFreq); emit newDemodFreq(m_DemodCenterFreq, m_DemodCenterFreq - m_CenterFreq); drawOverlay(); } else if (event->buttons() == Qt::RightButton) { // reset frequency zoom resetHorizontalZoom(); } } } else { if (m_CursorCaptured == YAXIS) // get ready for moving Y axis m_Yzero = pt.y(); else if (m_CursorCaptured == XAXIS) { m_Xzero = pt.x(); if (event->buttons() == Qt::RightButton) { // reset frequency zoom resetHorizontalZoom(); } } else if (m_CursorCaptured == BOOKMARK) { for (int i = 0; i < m_BookmarkTags.size(); i++) { if (m_BookmarkTags[i].first.contains(event->pos())) { m_DemodCenterFreq = m_BookmarkTags[i].second; emit newDemodFreq(m_DemodCenterFreq, m_DemodCenterFreq - m_CenterFreq); break; } } } } } void CPlotter::mouseReleaseEvent(QMouseEvent * event) { QPoint pt = event->pos(); pt.setX(pt.x() / pixRatio()+.5); pt.setY(pt.y() / pixRatio()+.5); if (!m_OverlayPixmap.rect().contains(pt)) { // not in Overlay region if (NOCAP != m_CursorCaptured) setCursor(QCursor(Qt::ArrowCursor)); m_CursorCaptured = NOCAP; m_GrabPosition = 0; } else { if (YAXIS == m_CursorCaptured) { setCursor(QCursor(Qt::OpenHandCursor)); m_Yzero = -1; } else if (XAXIS == m_CursorCaptured) { setCursor(QCursor(Qt::OpenHandCursor)); m_Xzero = -1; } } } // Make a single zoom step on the X axis. void CPlotter::zoomStepX(double step, int x) { // calculate new range shown on FFT double new_range = qBound(10.0, m_Span * step, m_SampleFreq * 10.0); // Frequency where event occured is kept fixed under mouse double ratio = (double)x / (double)m_OverlayPixmap.width(); double fixed_hz = freqFromX(x); double f_max = fixed_hz + (1.0 - ratio) * new_range; double f_min = f_max - new_range; // ensure we don't go beyond the rangelimits if (f_min < m_CenterFreq - m_SampleFreq / 2.f) f_min = m_CenterFreq - m_SampleFreq / 2.f; if (f_max > m_CenterFreq + m_SampleFreq / 2.f) f_max = m_CenterFreq + m_SampleFreq / 2.f; new_range = f_max - f_min; qint64 fc = (qint64)(f_min + (f_max - f_min) / 2.0); setFftCenterFreq(fc - m_CenterFreq); setSpanFreq((quint32)new_range); double factor = (double)m_SampleFreq / (double)m_Span; emit newZoomLevel(factor); qDebug() << QString("Spectrum zoom: %1x").arg(factor, 0, 'f', 1); m_PeakHoldValid = false; } // Zoom on X axis (absolute level) void CPlotter::zoomOnXAxis(double level) { double current_level = (double)m_SampleFreq / (double)m_Span; zoomStepX(current_level / level, xFromFreq(m_DemodCenterFreq)); } // Called when a mouse wheel is turned void CPlotter::wheelEvent(QWheelEvent * event) { #if QT_VERSION >=0x051500 QPointF pt = event->position(); #else QPoint pt = event->pos(); #endif pt.setX(pt.x() / pixRatio()+.5); pt.setY(pt.y() / pixRatio()+.5); int numDegrees = (event->angleDelta().x() + event->angleDelta().y()) / 8; int numSteps = numDegrees / 15; /** FIXME: Only used for direction **/ /** FIXME: zooming could use some optimisation **/ if (m_CursorCaptured == YAXIS) { // Vertical zoom. Wheel down: zoom out, wheel up: zoom in // During zoom we try to keep the point (dB or kHz) under the cursor fixed double zoom_fac = numDegrees < 0 ? 1.1 : 0.9; double ratio = (double)pt.y() / (double)m_OverlayPixmap.height(); double db_range = m_PandMaxdB - m_PandMindB; double y_range = (double)m_OverlayPixmap.height(); double db_per_pix = db_range / y_range; double fixed_db = m_PandMaxdB - pt.y() * db_per_pix; db_range = qBound(10.0, db_range * zoom_fac, FFT_MAX_DB - FFT_MIN_DB); m_PandMaxdB = fixed_db + ratio * db_range; if (m_PandMaxdB > FFT_MAX_DB) m_PandMaxdB = FFT_MAX_DB; m_PandMindB = m_PandMaxdB - db_range; m_PeakHoldValid = false; emit pandapterRangeChanged(m_PandMindB, m_PandMaxdB); } else if (m_CursorCaptured == XAXIS) { zoomStepX(numDegrees < 0 ? 1.1 : 0.9, pt.x()); } else if (event->modifiers() & Qt::ControlModifier) { // filter width m_DemodLowCutFreq -= numSteps * m_ClickResolution; m_DemodHiCutFreq += numSteps * m_ClickResolution; clampDemodParameters(); emit newFilterFreq(m_DemodLowCutFreq, m_DemodHiCutFreq); } else if (event->modifiers() & Qt::ShiftModifier) { // filter shift m_DemodLowCutFreq += numSteps * m_ClickResolution; m_DemodHiCutFreq += numSteps * m_ClickResolution; clampDemodParameters(); emit newFilterFreq(m_DemodLowCutFreq, m_DemodHiCutFreq); } else { // inc/dec demod frequency m_DemodCenterFreq += (numSteps * m_ClickResolution); m_DemodCenterFreq = roundFreq(m_DemodCenterFreq, m_ClickResolution ); emit newDemodFreq(m_DemodCenterFreq, m_DemodCenterFreq-m_CenterFreq); } updateOverlay(); } // Called when screen size changes so must recalculate bitmaps void CPlotter::resizeEvent(QResizeEvent* ) { if (!pixSize().isValid()) return; if (m_Size != pixSize()) { // if changed, resize pixmaps to new screensize int fft_plot_height; m_Size = pixSize(); fft_plot_height = m_Percent2DScreen * m_Size.height() / 100; m_OverlayPixmap = QPixmap(m_Size.width(), fft_plot_height); m_OverlayPixmap.fill(Qt::black); m_2DPixmap = QPixmap(m_Size.width(), fft_plot_height); m_2DPixmap.fill(Qt::black); int height = (100 - m_Percent2DScreen) * m_Size.height() / 100; if (m_WaterfallPixmap.isNull()) { m_WaterfallPixmap = QPixmap(m_Size.width(), height); m_WaterfallPixmap.fill(Qt::black); } else { m_WaterfallPixmap = m_WaterfallPixmap.scaled(m_Size.width(), height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } m_PeakHoldValid = false; if (wf_span > 0) msec_per_wfline = wf_span / height; memset(m_wfbuf, 255, MAX_SCREENSIZE); } drawOverlay(); } // Called by QT when screen needs to be redrawn void CPlotter::paintEvent(QPaintEvent *) { QPainter painter(this); QSize sz = size(); if (sz.isValid()) { QRectF target(0,0,sz.width(),sz.height()*m_Percent2DScreen/100.0+.5); QRectF source(0,0,m_2DPixmap.width(),m_2DPixmap.height()); painter.drawPixmap(target,m_2DPixmap,source); } if (sz.isValid()) { QRectF target(0,sz.height()*m_Percent2DScreen/100.0+.5, sz.width(), sz.height()*(100-m_Percent2DScreen)/100.0+.5 ); QRectF source(0,0,m_WaterfallPixmap.width(),m_WaterfallPixmap.height()); painter.drawPixmap(target,m_WaterfallPixmap,source); } } // Called to update spectrum data for displaying on the screen void CPlotter::draw() { int i, n; int w; int h; int xmin, xmax; if (m_DrawOverlay) { drawOverlay(); m_DrawOverlay = false; } QPoint LineBuf[MAX_SCREENSIZE]; if (!m_Running) return; // get/draw the waterfall w = m_WaterfallPixmap.width(); h = m_WaterfallPixmap.height(); // no need to draw if pixmap is invisible if (w != 0 && h != 0) { quint64 tnow_ms = time_ms(); // get scaled FFT data n = qMin(w, MAX_SCREENSIZE); getScreenIntegerFFTData(255, n, m_WfMaxdB, m_WfMindB, m_FftCenter - (qint64)m_Span / 2, m_FftCenter + (qint64)m_Span / 2, m_wfData, m_fftbuf, &xmin, &xmax); if (msec_per_wfline > 0) { // not in "auto" mode, so accumulate waterfall data for (i = 0; i < n; i++) { // average //m_wfbuf[i] = (m_wfbuf[i] + m_fftbuf[i]) / 2; // peak (0..255 where 255 is min) if (m_fftbuf[i] < m_wfbuf[i]) m_wfbuf[i] = m_fftbuf[i]; } } // is it time to update waterfall? if (tnow_ms - tlast_wf_ms >= msec_per_wfline) { tlast_wf_ms = tnow_ms; // move current data down one line(must do before attaching a QPainter object) m_WaterfallPixmap.scroll(0, 1, 0, 0, w, h); QPainter painter1(&m_WaterfallPixmap); // draw new line of fft data at top of waterfall bitmap painter1.setPen(QColor(0, 0, 0)); for (i = 0; i < xmin; i++) painter1.drawPoint(i, 0); for (i = xmax; i < w; i++) painter1.drawPoint(i, 0); if (msec_per_wfline > 0) { // user set time span for (i = xmin; i < xmax; i++) { painter1.setPen(m_ColorTbl[255 - m_wfbuf[i]]); painter1.drawPoint(i, 0); m_wfbuf[i] = 255; } } else { for (i = xmin; i < xmax; i++) { painter1.setPen(m_ColorTbl[255 - m_fftbuf[i]]); painter1.drawPoint(i, 0); } } } } // get/draw the 2D spectrum w = m_2DPixmap.width(); h = m_2DPixmap.height(); if (w != 0 && h != 0) { // first copy into 2Dbitmap the overlay bitmap. m_2DPixmap = m_OverlayPixmap.copy(0,0,w,h); QPainter painter2(&m_2DPixmap); // workaround for "fixed" line drawing since Qt 5 // see http://stackoverflow.com/questions/16990326 #if QT_VERSION >= 0x050000 painter2.translate(0.5, 0.5); #endif // get new scaled fft data getScreenIntegerFFTData(h, qMin(w, MAX_SCREENSIZE), m_PandMaxdB, m_PandMindB, m_FftCenter - (qint64)m_Span/2, m_FftCenter + (qint64)m_Span/2, m_fftData, m_fftbuf, &xmin, &xmax); // draw the pandapter painter2.setPen(m_FftColor); n = xmax - xmin; for (i = 0; i < n; i++) { LineBuf[i].setX(i + xmin); LineBuf[i].setY(m_fftbuf[i + xmin]); } if (m_FftFill) { painter2.setBrush(QBrush(m_FftFillCol, Qt::SolidPattern)); if (n < MAX_SCREENSIZE-2) { LineBuf[n].setX(xmax-1); LineBuf[n].setY(h); LineBuf[n+1].setX(xmin); LineBuf[n+1].setY(h); painter2.drawPolygon(LineBuf, n+2); } else { LineBuf[MAX_SCREENSIZE-2].setX(xmax-1); LineBuf[MAX_SCREENSIZE-2].setY(h); LineBuf[MAX_SCREENSIZE-1].setX(xmin); LineBuf[MAX_SCREENSIZE-1].setY(h); painter2.drawPolygon(LineBuf, n); } } else { painter2.drawPolyline(LineBuf, n); } // Peak detection if (m_PeakDetection > 0) { m_Peaks.clear(); double mean = 0; double sum_of_sq = 0; for (i = 0; i < n; i++) { mean += m_fftbuf[i + xmin]; sum_of_sq += m_fftbuf[i + xmin] * m_fftbuf[i + xmin]; } mean /= n; double stdev= sqrt(sum_of_sq / n - mean * mean ); int lastPeak = -1; for (i = 0; i < n; i++) { //m_PeakDetection times the std over the mean or better than current peak double d = (lastPeak == -1) ? (mean - m_PeakDetection * stdev) : m_fftbuf[lastPeak + xmin]; if (m_fftbuf[i + xmin] < d) lastPeak=i; if (lastPeak != -1 && (i - lastPeak > PEAK_H_TOLERANCE || i == n-1)) { m_Peaks.insert(lastPeak + xmin, m_fftbuf[lastPeak + xmin]); painter2.drawEllipse(lastPeak + xmin - 5, m_fftbuf[lastPeak + xmin] - 5, 10, 10); lastPeak = -1; } } } // Peak hold if (m_PeakHoldActive) { for (i = 0; i < n; i++) { if(!m_PeakHoldValid || m_fftbuf[i] < m_fftPeakHoldBuf[i]) m_fftPeakHoldBuf[i] = m_fftbuf[i]; LineBuf[i].setX(i + xmin); LineBuf[i].setY(m_fftPeakHoldBuf[i + xmin]); } painter2.setPen(m_PeakHoldColor); painter2.drawPolyline(LineBuf, n); m_PeakHoldValid = true; } painter2.end(); } // trigger a new paintEvent update(); } /** * Set new FFT data. * @param fftData Pointer to the new FFT data (same data for pandapter and waterfall). * @param size The FFT size. * * When FFT data is set using this method, the same data will be used for both the * pandapter and the waterfall. */ void CPlotter::setNewFftData(const double *fftData, int size) { /** FIXME **/ if (!m_Running) m_Running = true; m_wfData = fftData; m_fftData = fftData; m_fftDataSize = size; draw(); } /** * Set new FFT data. * @param fftData Pointer to the new FFT data used on the pandapter. * @param wfData Pointer to the FFT data used in the waterfall. * @param size The FFT size. * * This method can be used to set different FFT data set for the pandapter and the * waterfall. */ void CPlotter::setNewFftData(const double *fftData, const double *wfData, int size) { /** FIXME **/ if (!m_Running) m_Running = true; m_wfData = wfData; m_fftData = fftData; m_fftDataSize = size; draw(); } void CPlotter::getScreenIntegerFFTData(qint32 plotHeight, qint32 plotWidth, double maxdB, double mindB, qint64 startFreq, qint64 stopFreq, const double *inBuf, qint32 *outBuf, int *xmin, int *xmax) { qint32 i; qint32 y; qint32 x; qint32 ymax = 10000; qint32 xprev = -1; qint32 minbin, maxbin; qint32 m_BinMin, m_BinMax; qint32 m_FFTSize = m_fftDataSize; const double *m_pFFTAveBuf = inBuf; double dBGainFactor = ((double)plotHeight) / fabs(maxdB - mindB); qint32* m_pTranslateTbl = new qint32[qMax(m_FFTSize, plotWidth)]; /** FIXME: qint64 -> qint32 **/ m_BinMin = (qint32)((double)startFreq * (double)m_FFTSize / m_SampleFreq); m_BinMin += (m_FFTSize/2); m_BinMax = (qint32)((double)stopFreq * (double)m_FFTSize / m_SampleFreq); m_BinMax += (m_FFTSize/2); minbin = m_BinMin < 0 ? 0 : m_BinMin; if (m_BinMin > m_FFTSize) m_BinMin = m_FFTSize - 1; if (m_BinMax <= m_BinMin) m_BinMax = m_BinMin + 1; maxbin = m_BinMax < m_FFTSize ? m_BinMax : m_FFTSize; bool largeFft = (m_BinMax-m_BinMin) > plotWidth; // true if more fft point than plot points if (largeFft) { // more FFT points than plot points for (i = minbin; i < maxbin; i++) m_pTranslateTbl[i] = ((qint64)(i-m_BinMin)*plotWidth) / (m_BinMax - m_BinMin); *xmin = m_pTranslateTbl[minbin]; *xmax = m_pTranslateTbl[maxbin - 1]; } else { // more plot points than FFT points for (i = 0; i < plotWidth; i++) m_pTranslateTbl[i] = m_BinMin + (i*(m_BinMax - m_BinMin)) / plotWidth; *xmin = 0; *xmax = plotWidth; } if (largeFft) { // more FFT points than plot points for (i = minbin; i < maxbin; i++ ) { y = (qint32)(dBGainFactor*(maxdB-m_pFFTAveBuf[i])); if (y > plotHeight) y = plotHeight; else if (y < 0) y = 0; x = m_pTranslateTbl[i]; //get fft bin to plot x coordinate transform if (x == xprev) // still mappped to same fft bin coordinate { if (y < ymax) // store only the max value { outBuf[x] = y; ymax = y; } } else { outBuf[x] = y; xprev = x; ymax = y; } } } else { // more plot points than FFT points for (x = 0; x < plotWidth; x++ ) { i = m_pTranslateTbl[x]; // get plot to fft bin coordinate transform if(i < 0 || i >= m_FFTSize) y = plotHeight; else y = (qint32)(dBGainFactor*(maxdB-m_pFFTAveBuf[i])); if (y > plotHeight) y = plotHeight; else if (y < 0) y = 0; outBuf[x] = y; } } delete [] m_pTranslateTbl; } void CPlotter::setFftRange(double min, double max) { setWaterfallRange(min, max); setPandapterRange(min, max); } void CPlotter::setPandapterRange(double min, double max) { if (out_of_range(min, max)) return; m_PandMindB = min; m_PandMaxdB = max; updateOverlay(); m_PeakHoldValid = false; } void CPlotter::setWaterfallRange(double min, double max) { if (out_of_range(min, max)) return; m_WfMindB = min; m_WfMaxdB = max; // no overlay change is necessary } // Called to draw an overlay bitmap containing grid and text that // does not need to be recreated every fft data update. void CPlotter::drawOverlay() { if (m_OverlayPixmap.isNull()) return; int w = m_OverlayPixmap.width(); int h = m_OverlayPixmap.height(); int x,y; double pixperdiv; double adjoffset; double dbstepsize; double mindbadj; QRect rect; QFontMetrics metrics(m_Font); QPainter painter(&m_OverlayPixmap); painter.begin(this); painter.setFont(m_Font); // solid background painter.setBrush(Qt::SolidPattern); painter.fillRect(0, 0, w, h, QColor(PLOTTER_BGD_COLOR)); #define HOR_MARGIN 5 #define VER_MARGIN 5 // X and Y axis areas m_YAxisWidth = metrics.horizontalAdvance("XXXX") + 2 * HOR_MARGIN; m_XAxisYCenter = h - metrics.height()/2; int xAxisHeight = metrics.height() + 2 * VER_MARGIN; int xAxisTop = h - xAxisHeight; int fLabelTop = xAxisTop + VER_MARGIN; if (m_BookmarksEnabled) { m_BookmarkTags.clear(); static const QFontMetrics fm(painter.font()); static const int fontHeight = fm.ascent() + 1; static const int slant = 5; static const int levelHeight = fontHeight + 5; static const int nLevels = 10; QList bookmarks = Bookmarks::Get().getBookmarksInRange(m_CenterFreq + m_FftCenter - m_Span / 2, m_CenterFreq + m_FftCenter + m_Span / 2); int tagEnd[nLevels] = {0}; for (int i = 0; i < bookmarks.size(); i++) { x = xFromFreq(bookmarks[i].frequency); #if defined(_WIN16) || defined(_WIN32) || defined(_WIN64) int nameWidth = fm.horizontalAdvance(bookmarks[i].name); #else int nameWidth = fm.boundingRect(bookmarks[i].name).width(); #endif int level = 0; while(level < nLevels && tagEnd[level] > x) level++; if(level == nLevels) level = 0; tagEnd[level] = x + nameWidth + slant - 1; m_BookmarkTags.append(qMakePair(QRect(x, level * levelHeight, nameWidth + slant, fontHeight), bookmarks[i].frequency)); QColor color = QColor(bookmarks[i].GetColor()); color.setAlpha(0x60); // Vertical line painter.setPen(QPen(color, 1, Qt::DashLine)); painter.drawLine(x, level * levelHeight + fontHeight + slant, x, xAxisTop); // Horizontal line painter.setPen(QPen(color, 1, Qt::SolidLine)); painter.drawLine(x + slant, level * levelHeight + fontHeight, x + nameWidth + slant - 1, level * levelHeight + fontHeight); // Diagonal line painter.drawLine(x + 1, level * levelHeight + fontHeight + slant - 1, x + slant - 1, level * levelHeight + fontHeight + 1); color.setAlpha(0xFF); painter.setPen(QPen(color, 2, Qt::SolidLine)); painter.drawText(x + slant, level * levelHeight, nameWidth, fontHeight, Qt::AlignVCenter | Qt::AlignHCenter, bookmarks[i].name); } } if (m_CenterLineEnabled) { x = xFromFreq(m_CenterFreq); if (x > 0 && x < w) { painter.setPen(QColor(PLOTTER_CENTER_LINE_COLOR)); painter.drawLine(x, 0, x, xAxisTop); } } // Frequency grid qint64 StartFreq = m_CenterFreq + m_FftCenter - m_Span / 2; QString label; label.setNum(double((StartFreq + m_Span) / m_FreqUnits), 'f', m_FreqDigits); calcDivSize(StartFreq, StartFreq + m_Span, qMin(w/(metrics.horizontalAdvance(label) + metrics.horizontalAdvance("O")), HORZ_DIVS_MAX), m_StartFreqAdj, m_FreqPerDiv, m_HorDivs); pixperdiv = (double)w * (double) m_FreqPerDiv / (double) m_Span; adjoffset = pixperdiv * double (m_StartFreqAdj - StartFreq) / (double) m_FreqPerDiv; painter.setPen(QPen(QColor(PLOTTER_GRID_COLOR), 1, Qt::DotLine)); for (int i = 0; i <= m_HorDivs; i++) { x = (int)((double)i * pixperdiv + adjoffset); if (x > m_YAxisWidth) painter.drawLine(x, 0, x, xAxisTop); } // draw frequency values (x axis) makeFrequencyStrs(); painter.setPen(QColor(PLOTTER_TEXT_COLOR)); for (int i = 0; i <= m_HorDivs; i++) { int tw = metrics.horizontalAdvance(m_HDivText[i]); x = (int)((double)i*pixperdiv + adjoffset); if (x > m_YAxisWidth) { rect.setRect(x - tw/2, fLabelTop, tw, metrics.height()); painter.drawText(rect, Qt::AlignHCenter|Qt::AlignBottom, m_HDivText[i]); } } // Level grid qint64 mindBAdj64 = 0; qint64 dbDivSize = 0; calcDivSize((qint64) m_PandMindB, (qint64) m_PandMaxdB, qMax(h/m_VdivDelta, VERT_DIVS_MIN), mindBAdj64, dbDivSize, m_VerDivs); dbstepsize = (double) dbDivSize; mindbadj = mindBAdj64; pixperdiv = (double) h * (double) dbstepsize / (m_PandMaxdB - m_PandMindB); adjoffset = (double) h * (mindbadj - m_PandMindB) / (m_PandMaxdB - m_PandMindB); #ifdef PLOTTER_DEBUG qDebug() << "minDb =" << m_PandMindB << "maxDb =" << m_PandMaxdB << "mindbadj =" << mindbadj << "dbstepsize =" << dbstepsize << "pixperdiv =" << pixperdiv << "adjoffset =" << adjoffset; #endif painter.setPen(QPen(QColor(PLOTTER_GRID_COLOR), 1, Qt::DotLine)); for (int i = 0; i <= m_VerDivs; i++) { y = h - (int)((double) i * pixperdiv + adjoffset); if (y < h - xAxisHeight) painter.drawLine(m_YAxisWidth, y, w, y); } // draw amplitude values (y axis) int dB = m_PandMaxdB; m_YAxisWidth = metrics.horizontalAdvance("-120 "); painter.setPen(QColor(PLOTTER_TEXT_COLOR)); for (int i = 0; i < m_VerDivs; i++) { y = h - (int)((double) i * pixperdiv + adjoffset); int th = metrics.height(); if (y < h -xAxisHeight) { dB = mindbadj + dbstepsize * i; rect.setRect(HOR_MARGIN, y - th / 2, m_YAxisWidth, th); painter.drawText(rect, Qt::AlignRight|Qt::AlignVCenter, QString::number(dB)); } } // Draw demod filter box if (m_FilterBoxEnabled) { m_DemodFreqX = xFromFreq(m_DemodCenterFreq); m_DemodLowCutFreqX = xFromFreq(m_DemodCenterFreq + m_DemodLowCutFreq); m_DemodHiCutFreqX = xFromFreq(m_DemodCenterFreq + m_DemodHiCutFreq); int dw = m_DemodHiCutFreqX - m_DemodLowCutFreqX; painter.setOpacity(0.3); painter.fillRect(m_DemodLowCutFreqX, 0, dw, h, QColor(PLOTTER_FILTER_BOX_COLOR)); painter.setOpacity(1.0); painter.setPen(QColor(PLOTTER_FILTER_LINE_COLOR)); painter.drawLine(m_DemodFreqX, 0, m_DemodFreqX, h); } if (!m_Running) { // if not running so is no data updates to draw to screen // copy into 2Dbitmap the overlay bitmap. m_2DPixmap = m_OverlayPixmap.copy(0,0,w,h); // trigger a new paintEvent update(); } painter.end(); } // Create frequency division strings based on start frequency, span frequency, // and frequency units. // Places in QString array m_HDivText // Keeps all strings the same fractional length void CPlotter::makeFrequencyStrs() { qint64 StartFreq = m_StartFreqAdj; double freq; int i,j; if ((1 == m_FreqUnits) || (m_FreqDigits == 0)) { // if units is Hz then just output integer freq for (int i = 0; i <= m_HorDivs; i++) { freq = (double)StartFreq/(double)m_FreqUnits; m_HDivText[i].setNum((int)freq); StartFreq += m_FreqPerDiv; } return; } // here if is fractional frequency values // so create max sized text based on frequency units for (int i = 0; i <= m_HorDivs; i++) { freq = (double)StartFreq / (double)m_FreqUnits; m_HDivText[i].setNum(freq,'f', m_FreqDigits); StartFreq += m_FreqPerDiv; } // now find the division text with the longest non-zero digit // to the right of the decimal point. int max = 0; for (i = 0; i <= m_HorDivs; i++) { int dp = m_HDivText[i].indexOf('.'); int l = m_HDivText[i].length()-1; for (j = l; j > dp; j--) { if (m_HDivText[i][j] != '0') break; } if ((j - dp) > max) max = j - dp; } // truncate all strings to maximum fractional length StartFreq = m_StartFreqAdj; for (i = 0; i <= m_HorDivs; i++) { freq = (double)StartFreq/(double)m_FreqUnits; m_HDivText[i].setNum(freq,'f', max); StartFreq += m_FreqPerDiv; } } // Convert from screen coordinate to frequency int CPlotter::xFromFreq(qint64 freq) { int w = m_OverlayPixmap.width(); qint64 StartFreq = m_CenterFreq + m_FftCenter - m_Span/2; int x = (int) w * ((double)freq - StartFreq)/(double)m_Span; if (x < 0) return 0; if (x > (int)w) return m_OverlayPixmap.width(); return x; } // Convert from frequency to screen coordinate qint64 CPlotter::freqFromX(int x) { int w = m_OverlayPixmap.width(); qint64 StartFreq = m_CenterFreq + m_FftCenter - m_Span / 2; qint64 f = (qint64)(StartFreq + (double)m_Span * (double)x / (double)w); return f; } /** Calculate time offset of a given line on the waterfall */ quint64 CPlotter::msecFromY(int y) { // ensure we are in the waterfall region if (y < m_OverlayPixmap.height()) return 0; int dy = y - m_OverlayPixmap.height(); if (msec_per_wfline > 0) return tlast_wf_ms - dy * msec_per_wfline; else return tlast_wf_ms - dy * 1000 / fft_rate; } // Round frequency to click resolution value qint64 CPlotter::roundFreq(qint64 freq, int resolution) { qint64 delta = resolution; qint64 delta_2 = delta / 2; if (freq >= 0) return (freq - (freq + delta_2) % delta + delta_2); else return (freq - (freq + delta_2) % delta - delta_2); } // Clamp demod freqeuency limits of m_DemodCenterFreq void CPlotter::clampDemodParameters() { if(m_DemodLowCutFreq < m_FLowCmin) m_DemodLowCutFreq = m_FLowCmin; if(m_DemodLowCutFreq > m_FLowCmax) m_DemodLowCutFreq = m_FLowCmax; if(m_DemodHiCutFreq < m_FHiCmin) m_DemodHiCutFreq = m_FHiCmin; if(m_DemodHiCutFreq > m_FHiCmax) m_DemodHiCutFreq = m_FHiCmax; } void CPlotter::setDemodRanges(int FLowCmin, int FLowCmax, int FHiCmin, int FHiCmax, bool symetric) { m_FLowCmin=FLowCmin; m_FLowCmax=FLowCmax; m_FHiCmin=FHiCmin; m_FHiCmax=FHiCmax; m_symetric=symetric; clampDemodParameters(); updateOverlay(); } void CPlotter::setCenterFreq(quint64 f) { if((quint64)m_CenterFreq == f) return; qint64 offset = m_CenterFreq - m_DemodCenterFreq; m_CenterFreq = f; m_DemodCenterFreq = m_CenterFreq - offset; updateOverlay(); m_PeakHoldValid = false; } // Ensure overlay is updated by either scheduling or forcing a redraw void CPlotter::updateOverlay() { if (m_Running) m_DrawOverlay = true; else drawOverlay(); } /** Reset horizontal zoom to 100% and centered around 0. */ void CPlotter::resetHorizontalZoom(void) { setFftCenterFreq(0); setSpanFreq((qint32)m_SampleFreq); } /** Center FFT plot around 0 (corresponds to center freq). */ void CPlotter::moveToCenterFreq(void) { setFftCenterFreq(0); updateOverlay(); m_PeakHoldValid = false; } /** Center FFT plot around the demodulator frequency. */ void CPlotter::moveToDemodFreq(void) { setFftCenterFreq(m_DemodCenterFreq-m_CenterFreq); updateOverlay(); m_PeakHoldValid = false; } /** Set FFT plot color. */ void CPlotter::setFftPlotColor(const QColor color) { m_FftColor = color; m_FftFillCol = color; m_FftFillCol.setAlpha(0x1A); m_PeakHoldColor = color; m_PeakHoldColor.setAlpha(60); } /** Enable/disable filling the area below the FFT plot. */ void CPlotter::setFftFill(bool enabled) { m_FftFill = enabled; } /** Set peak hold on or off. */ void CPlotter::setPeakHold(bool enabled) { m_PeakHoldActive = enabled; m_PeakHoldValid = false; } /** * Set peak detection on or off. * @param enabled The new state of peak detection. * @param c Minimum distance of peaks from mean, in multiples of standard deviation. */ void CPlotter::setPeakDetection(bool enabled, double c) { if(!enabled || c <= 0) m_PeakDetection = -1; else m_PeakDetection = c; } void CPlotter::calcDivSize (qint64 low, qint64 high, int divswanted, qint64 &adjlow, qint64 &step, int& divs) { #ifdef PLOTTER_DEBUG qDebug() << "low: " << low; qDebug() << "high: " << high; qDebug() << "divswanted: " << divswanted; #endif if (divswanted == 0) return; static const qint64 stepTable[] = { 1, 2, 5 }; static const int stepTableSize = sizeof (stepTable) / sizeof (stepTable[0]); qint64 multiplier = 1; step = 1; divs = high - low; int index = 0; adjlow = (low / step) * step; while (divs > divswanted) { step = stepTable[index] * multiplier; divs = int ((high - low) / step); adjlow = (low / step) * step; index = index + 1; if (index == stepTableSize) { index = 0; multiplier = multiplier * 10; } } if (adjlow < low) adjlow += step; #ifdef PLOTTER_DEBUG qDebug() << "adjlow: " << adjlow; qDebug() << "step: " << step; qDebug() << "divs: " << divs; #endif }