mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
876 lines
22 KiB
C++
876 lines
22 KiB
C++
#include "plotter.h"
|
|
#include <QDebug>
|
|
#include <QMouseEvent>
|
|
#include <QPainter>
|
|
#include <QPen>
|
|
#include <QToolTip>
|
|
#include <QWheelEvent>
|
|
#include "commons.h"
|
|
#include "moc_plotter.cpp"
|
|
#include <algorithm>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
|
|
#include "DriftingDateTime.h"
|
|
#include "varicode.h"
|
|
|
|
extern "C" {
|
|
void flat4_(float swide[], int* iz, int* nflatten);
|
|
void plotsave_(float swide[], int* m_w , int* m_h1, int* irow);
|
|
}
|
|
|
|
namespace
|
|
{
|
|
constexpr std::size_t VERT_DIVS = 7;
|
|
|
|
qint32
|
|
freqPerDiv(float const fSpan)
|
|
{
|
|
if (fSpan > 2500) { return 500; }
|
|
if (fSpan > 1000) { return 200; }
|
|
if (fSpan > 500) { return 100; }
|
|
if (fSpan > 250) { return 50; }
|
|
if (fSpan > 100) { return 20; }
|
|
return 10;
|
|
}
|
|
|
|
double
|
|
fftBinWidth(qint32 const nsps)
|
|
{
|
|
switch (nsps)
|
|
{
|
|
case 252000: return 1500.0 / 32768.0;
|
|
case 82944: return 1500.0 / 12288.0;
|
|
case 40960: return 1500.0 / 6144.0;
|
|
default: return 1500.0 / 2048.0;
|
|
}
|
|
}
|
|
|
|
float
|
|
bw(qint32 const nSubMode)
|
|
{
|
|
switch (nSubMode)
|
|
{
|
|
case Varicode::JS8CallNormal: return 8.0 * (double)RX_SAMPLE_RATE / (double)JS8A_SYMBOL_SAMPLES;
|
|
case Varicode::JS8CallFast: return 8.0 * (double)RX_SAMPLE_RATE / (double)JS8B_SYMBOL_SAMPLES;
|
|
case Varicode::JS8CallTurbo: return 8.0 * (double)RX_SAMPLE_RATE / (double)JS8C_SYMBOL_SAMPLES;
|
|
case Varicode::JS8CallSlow: return 8.0 * (double)RX_SAMPLE_RATE / (double)JS8E_SYMBOL_SAMPLES;
|
|
case Varicode::JS8CallUltra: return 8.0 * (double)RX_SAMPLE_RATE / (double)JS8I_SYMBOL_SAMPLES;
|
|
default: return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
CPlotter::CPlotter(QWidget *parent) : //CPlotter Constructor
|
|
QFrame {parent},
|
|
m_set_freq_action {new QAction {tr ("&Set Rx && Tx Offset"), this}},
|
|
m_bScaleOK {false},
|
|
m_bReference {false},
|
|
m_bReference0 {false},
|
|
m_fSpan {2000.0},
|
|
m_plotZero {0},
|
|
m_plotGain {0},
|
|
m_plot2dGain {0},
|
|
m_plot2dZero {0},
|
|
m_nSubMode {0},
|
|
m_filterEnabled{false},
|
|
m_filterCenter {0},
|
|
m_filterWidth {0},
|
|
m_turbo {false},
|
|
m_Running {false},
|
|
m_paintEventBusy {false},
|
|
m_fftBinWidth {1500.0/2048.0},
|
|
m_dialFreq {0.},
|
|
m_sum {},
|
|
m_points {},
|
|
m_line {0},
|
|
m_nsps {6912},
|
|
m_Percent2DScreen {0}, //percent of screen used for 2D display
|
|
m_Percent2DScreen0 {0},
|
|
m_rxFreq {1020},
|
|
m_txFreq {0},
|
|
m_startFreq {0},
|
|
m_lastMouseX {-1}
|
|
{
|
|
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);
|
|
m_bReplot=false;
|
|
|
|
setMouseTracking(true);
|
|
}
|
|
|
|
CPlotter::~CPlotter() = default;
|
|
|
|
QSize CPlotter::minimumSizeHint() const
|
|
{
|
|
return QSize(50, 50);
|
|
}
|
|
|
|
QSize CPlotter::sizeHint() const
|
|
{
|
|
return QSize(180, 180);
|
|
}
|
|
|
|
void CPlotter::resizeEvent(QResizeEvent *) //resizeEvent()
|
|
{
|
|
if (!size().isValid()) return;
|
|
|
|
if ((m_Size != size()) ||
|
|
(m_bReference != m_bReference0) ||
|
|
(m_Percent2DScreen != m_Percent2DScreen0))
|
|
{
|
|
m_Size = size();
|
|
m_w = m_Size.width();
|
|
m_h = m_Size.height();
|
|
m_h2 = m_Percent2DScreen * m_h / 100.0;
|
|
|
|
if (m_h2 > m_h - 30) m_h2 = m_h - 30;
|
|
if (m_bReference ) m_h2 = m_h - 30;
|
|
if (m_h2 < 1) m_h2 = 1;
|
|
|
|
m_h1 = m_h - m_h2;
|
|
// m_line=0;
|
|
|
|
m_FilterOverlayPixmap = QPixmap(m_Size);
|
|
m_FilterOverlayPixmap.fill(Qt::transparent);
|
|
|
|
m_DialOverlayPixmap = QPixmap(m_Size);
|
|
m_DialOverlayPixmap.fill(Qt::transparent);
|
|
|
|
m_HoverOverlayPixmap = QPixmap(m_Size);
|
|
m_HoverOverlayPixmap.fill(Qt::transparent);
|
|
|
|
m_2DPixmap = QPixmap(m_w, m_h2);
|
|
m_2DPixmap.fill(Qt::black);
|
|
|
|
m_WaterfallPixmap = QPixmap(m_w, m_h1);
|
|
m_WaterfallPixmap.fill(Qt::black);
|
|
|
|
m_OverlayPixmap = QPixmap(m_w, m_h2);
|
|
m_OverlayPixmap.fill(Qt::black);
|
|
|
|
// Address scale font looking terrible, since it's drawn into this
|
|
// intermediate pixmap, so if we don't scale it to match the device,
|
|
// the text will look pixelated.
|
|
//
|
|
// Same is true of the decode lines in the waterfall; they look
|
|
// pixelated, but the fix doesn't appear to be straightforward, and
|
|
// it's arguably an effect there, a bit like a Tektronix display.
|
|
|
|
m_ScalePixmap = QPixmap(QSize(m_w, 30) * devicePixelRatio());
|
|
m_ScalePixmap.setDevicePixelRatio(devicePixelRatio());
|
|
m_ScalePixmap.fill(Qt::white);
|
|
|
|
m_Percent2DScreen0 = m_Percent2DScreen;
|
|
}
|
|
DrawOverlay();
|
|
}
|
|
|
|
void CPlotter::paintEvent(QPaintEvent *) // paintEvent()
|
|
{
|
|
if (m_paintEventBusy) return;
|
|
|
|
m_paintEventBusy = true;
|
|
|
|
QPainter painter(this);
|
|
painter.drawPixmap(0,0,m_ScalePixmap);
|
|
painter.drawPixmap(0,30,m_WaterfallPixmap);
|
|
painter.drawPixmap(0,m_h1,m_2DPixmap);
|
|
|
|
int const x = XfromFreq(m_rxFreq);
|
|
painter.drawPixmap(x,0,m_DialOverlayPixmap);
|
|
|
|
if (m_lastMouseX >= 0 && m_lastMouseX != x)
|
|
{
|
|
painter.drawPixmap(m_lastMouseX, 0, m_HoverOverlayPixmap);
|
|
}
|
|
|
|
if (m_filterEnabled && m_filterWidth > 0)
|
|
{
|
|
painter.drawPixmap(0, 0, m_FilterOverlayPixmap);
|
|
}
|
|
|
|
m_paintEventBusy = false;
|
|
}
|
|
|
|
void CPlotter::draw(float swide[], bool bScroll, bool)
|
|
{
|
|
if(m_bReference != m_bReference0) resizeEvent(nullptr);
|
|
m_bReference0 = m_bReference;
|
|
|
|
// Move current data down one line (must do this before attaching a QPainter object)
|
|
|
|
if(bScroll && !m_bReplot)
|
|
{
|
|
m_WaterfallPixmap.scroll(0, 1, m_WaterfallPixmap.rect());
|
|
}
|
|
|
|
QPainter painter1(&m_WaterfallPixmap);
|
|
m_2DPixmap = m_OverlayPixmap.copy();
|
|
QPainter painter2D(&m_2DPixmap);
|
|
if(!painter2D.isActive()) return;
|
|
painter2D.setFont({"Arial", 12});
|
|
|
|
if (m_bLinearAvg) { painter2D.setPen(Qt::yellow); }
|
|
else if(m_bReference) { painter2D.setPen(Qt::blue); }
|
|
else { painter2D.setPen(Qt::green); }
|
|
|
|
int iz = XfromFreq(5000.0);
|
|
m_fMax = FreqfromX(iz);
|
|
|
|
if(bScroll && swide[0] < 1.e29)
|
|
{
|
|
flat4_(swide, &iz, &m_Flatten);
|
|
}
|
|
|
|
if(swide[0] > 1.e29 && swide[0] < 1.5e30) painter1.setPen(Qt::green); // horizontal line
|
|
if(swide[0] > 1.4e30 ) painter1.setPen(Qt::yellow);
|
|
|
|
if(!m_bReplot)
|
|
{
|
|
m_j = 0;
|
|
int irow = -1;
|
|
|
|
plotsave_(swide, &m_w, &m_h1, &irow);
|
|
}
|
|
|
|
double const fac = sqrt(m_binsPerPixel * m_waterfallAvg / 15.0);
|
|
double const gain = fac * pow(10.0, 0.015 * m_plotGain);
|
|
double const gain2d = pow(10.0, 0.02 * m_plot2dGain);
|
|
int const j0 = int(m_startFreq/m_fftBinWidth + 0.5);
|
|
int j = 0;
|
|
float ymin = 1.e30f;
|
|
|
|
for(int i = 0; i < iz; i++)
|
|
{
|
|
float const y = swide[i];
|
|
if (y < ymin) ymin = y;
|
|
if (swide[i] < 1.e29) painter1.setPen(g_ColorTbl[std::clamp(static_cast<int>(10.0 * gain * y + m_plotZero), 0, 254)]);
|
|
painter1.drawPoint(i, m_j);
|
|
}
|
|
|
|
m_line++;
|
|
|
|
for (int i = 0; i < iz; i++)
|
|
{
|
|
float const y = swide[i] - ymin;
|
|
float y2 = 0;
|
|
|
|
if (m_bCurrent) y2 = gain2d * y + m_plot2dZero; //Current
|
|
|
|
if(bScroll)
|
|
{
|
|
float sum = 0.0f;
|
|
int k = j0 + m_binsPerPixel * i;
|
|
for (int l = 0; l < m_binsPerPixel; l++)
|
|
{
|
|
sum += dec_data.savg[k++];
|
|
}
|
|
|
|
m_sum[i] = sum;
|
|
}
|
|
|
|
if (m_bCumulative ) y2 = gain2d * (m_sum[i] / m_binsPerPixel + m_plot2dZero);
|
|
if (m_Flatten == 0) y2 += 15; //### could do better! ###
|
|
|
|
if (m_bLinearAvg) //Linear Avg (yellow)
|
|
{
|
|
float sum = 0.0f;
|
|
int k = j0 + m_binsPerPixel * i;
|
|
|
|
for (int l = 0; l < m_binsPerPixel; l++)
|
|
{
|
|
sum+=spectra_.syellow[k++];
|
|
}
|
|
|
|
y2 = gain2d * sum / m_binsPerPixel + m_plot2dZero;
|
|
}
|
|
|
|
if(m_bReference) //Reference (red)
|
|
{
|
|
float const df_ref = 12000.0f / 6912.0f;
|
|
int const k = FreqfromX(i) / df_ref + 0.5;
|
|
|
|
y2 = spectra_.ref[k] + m_plot2dZero;
|
|
|
|
}
|
|
|
|
if (i == iz - 1)
|
|
{
|
|
painter2D.drawPolyline(m_points, j);
|
|
}
|
|
|
|
m_points[j].setX(i);
|
|
m_points[j].setY(int(0.9 * m_h2 - y2 * m_h2 / 70.0));
|
|
|
|
j++;
|
|
}
|
|
|
|
if(m_bReplot) return;
|
|
|
|
if (swide[0] > 1.0e29) m_line = 0;
|
|
if (m_line == painter1.fontMetrics().height())
|
|
{
|
|
qint64 const ms = DriftingDateTime::currentMSecsSinceEpoch() % 86400000;
|
|
int const n = (ms/1000) % m_TRperiod;
|
|
auto const t1 = DriftingDateTime::currentDateTimeUtc().addSecs(-n);
|
|
auto const ts = t1.toString(m_TRperiod < 60 ? "hh:mm:ss" : "hh:mm");
|
|
|
|
painter1.setPen(Qt::white);
|
|
painter1.drawText(5,
|
|
painter1.fontMetrics().ascent(),
|
|
QString("%1 %2").arg(ts).arg(m_rxBand));
|
|
}
|
|
|
|
if(m_mode == "JT4" || m_mode== "QRA64") //Mark freqs of JT4 single-tone msgs
|
|
{
|
|
int const y = 0.2 * m_h2;
|
|
|
|
painter2D.setFont({"Arial", 12, QFont::Bold});
|
|
painter2D.setPen(Qt::yellow);
|
|
|
|
painter2D.drawText(XfromFreq(m_rxFreq ) - 4, y, "T");
|
|
painter2D.drawText(XfromFreq(m_rxFreq + 250) - 4, y, "M");
|
|
painter2D.drawText(XfromFreq(m_rxFreq + 500) - 4, y, "R");
|
|
painter2D.drawText(XfromFreq(m_rxFreq + 750) - 4, y, "73");
|
|
}
|
|
|
|
update(); //trigger a new paintEvent
|
|
|
|
m_bScaleOK = true;
|
|
}
|
|
|
|
void CPlotter::drawDecodeLine(const QColor &color, int ia, int ib)
|
|
{
|
|
int const x1 = XfromFreq(ia);
|
|
int const x2 = XfromFreq(ib);
|
|
|
|
QPainter painter1(&m_WaterfallPixmap);
|
|
|
|
painter1.setPen(color);
|
|
painter1.drawLine(qMin(x1, x2), 4, qMax(x1, x2), 4);
|
|
painter1.drawLine(qMin(x1, x2), 0, qMin(x1, x2), 9);
|
|
painter1.drawLine(qMax(x1, x2), 0, qMax(x1, x2), 9);
|
|
}
|
|
|
|
void CPlotter::drawHorizontalLine(const QColor &color, int x, int width)
|
|
{
|
|
QPainter painter1(&m_WaterfallPixmap);
|
|
|
|
painter1.setPen(color);
|
|
painter1.drawLine(x, 0, width <= 0 ? m_w : x + width, 0);
|
|
}
|
|
|
|
void CPlotter::replot()
|
|
{
|
|
float swide[m_w];
|
|
|
|
m_bReplot = true;
|
|
|
|
for (int irow = 0; irow < m_h1; irow++)
|
|
{
|
|
m_j = irow;
|
|
plotsave_(swide, &m_w, &m_h1, &irow);
|
|
draw(swide, false, false);
|
|
}
|
|
|
|
update(); //trigger a new paintEvent
|
|
|
|
m_bReplot = false;
|
|
}
|
|
|
|
void CPlotter::DrawOverlay()
|
|
{
|
|
if (m_OverlayPixmap.isNull() ||
|
|
m_WaterfallPixmap.isNull()) return;
|
|
|
|
QLinearGradient gradient(0, 0, 0, m_h2); //fill background with gradient
|
|
|
|
gradient.setColorAt(1, Qt::black);
|
|
gradient.setColorAt(0, Qt::darkBlue);
|
|
|
|
QPainter p(&m_OverlayPixmap);
|
|
|
|
p.setBrush(gradient);
|
|
p.drawRect(0, 0, m_w, m_h2);
|
|
p.setBrush(Qt::SolidPattern);
|
|
|
|
double const df = m_binsPerPixel * m_fftBinWidth;
|
|
|
|
m_fSpan = m_w * df;
|
|
m_freqPerDiv = freqPerDiv(m_fSpan);
|
|
|
|
float const ppdV = m_freqPerDiv / df;
|
|
float const ppdH = (float)m_h2 / (float)VERT_DIVS;
|
|
std::size_t const hdivs = m_fSpan / m_freqPerDiv + 1.9999;
|
|
|
|
float xx0 = float(m_startFreq) /float(m_freqPerDiv);
|
|
xx0 = xx0 - int(xx0);
|
|
int x0 = xx0 * ppdV + 0.5;
|
|
|
|
p.setPen(QPen(Qt::white, 1, Qt::DotLine));
|
|
|
|
for (std::size_t i = 1; i < hdivs; i++) //draw vertical grids
|
|
{
|
|
if (int const x = (int)((float)i * ppdV) - x0;
|
|
x >= 0 &&
|
|
x <= m_w)
|
|
{
|
|
p.drawLine(x, 0, x , m_h2);
|
|
}
|
|
}
|
|
|
|
for (std::size_t i = 1; i < VERT_DIVS; i++) //draw horizontal grids
|
|
{
|
|
int const y = (int)( (float)i * ppdH );
|
|
p.drawLine(0, y, m_w, y);
|
|
}
|
|
|
|
DrawOverlayScale(df, ppdV);
|
|
|
|
// paint dials and filter overlays
|
|
if (m_mode == "FT8")
|
|
{
|
|
int const fwidth = XfromFreq(m_rxFreq + bw(m_nSubMode)) - XfromFreq(m_rxFreq);
|
|
|
|
DrawOverlayDial(fwidth);
|
|
DrawOverlayHover(fwidth);
|
|
DrawOverlayFilter();
|
|
}
|
|
}
|
|
|
|
void
|
|
CPlotter::DrawOverlayScale(double const df,
|
|
float const ppdV)
|
|
{
|
|
QPen const penOrange (QColor(230, 126, 34), 3);
|
|
QPen const penGray (QColor(149, 165, 166), 3);
|
|
QPen const penLightGreen (QColor( 46, 204, 113), 3);
|
|
QPen const penLightYellow(QColor(241, 196, 15), 3);
|
|
|
|
m_ScalePixmap.fill(Qt::white);
|
|
QPainter p(&m_ScalePixmap);
|
|
|
|
p.setFont({"Arial"});
|
|
p.setPen(Qt::black);
|
|
p.drawRect(0, 0, m_w, 30);
|
|
|
|
int const fOffset = ((m_startFreq + m_freqPerDiv - 1) / m_freqPerDiv) * m_freqPerDiv;
|
|
double const xOffset = double(fOffset - m_startFreq) / m_freqPerDiv;
|
|
std::size_t const nMinor = m_freqPerDiv == 200 ? 4: 5;
|
|
std::size_t const nHDivs = m_w * df / m_freqPerDiv + 0.9999;
|
|
float const ppdVM = ppdV / nMinor;
|
|
float const ppdVL = ppdV / 2;
|
|
|
|
// Draw ticks and labels.
|
|
|
|
for (std::size_t iMajor = 0; iMajor < nHDivs; iMajor++)
|
|
{
|
|
auto const rMajor = (xOffset + iMajor) * ppdV;
|
|
auto const xMajor = static_cast<int>(rMajor);
|
|
p.drawLine(xMajor, 18, xMajor, 30);
|
|
|
|
for (std::size_t iMinor = 1; iMinor < nMinor; iMinor++)
|
|
{
|
|
auto const xMinor = static_cast<int>(rMajor + iMinor * ppdVM);
|
|
p.drawLine(xMinor, 22, xMinor, 30);
|
|
}
|
|
|
|
if (xMajor > 70)
|
|
{
|
|
p.drawText(QRect(xMajor - static_cast<int>(ppdVL), 0, static_cast<int>(ppdV), 20),
|
|
Qt::AlignCenter,
|
|
QString::number(fOffset + iMajor * m_freqPerDiv));
|
|
}
|
|
}
|
|
|
|
// If our scale incldues the WSPR range, display it.
|
|
|
|
if (m_dialFreq > 10.13 &&
|
|
m_dialFreq < 10.15 && !m_mode.startsWith(QLatin1StringView("WSPR")))
|
|
{
|
|
float const wspr = 1.0e6f * (10.1401 - m_dialFreq);
|
|
int const x1 = XfromFreq(wspr);
|
|
int const x2 = XfromFreq(wspr + 200.0f);
|
|
|
|
if (x1 <= m_w && x2 >= 0)
|
|
{
|
|
p.setPen(penOrange); //Mark WSPR sub-band orange
|
|
p.drawLine(x1, 9, x2, 9);
|
|
}
|
|
}
|
|
|
|
// Colorize the JS8 sub-bands.
|
|
|
|
for (std::size_t i = 0; i <= 3500; i += 500)
|
|
{
|
|
int const x1 = XfromFreq(static_cast<float>(i));
|
|
int const x2 = XfromFreq(static_cast<float>(i + 500));
|
|
|
|
if (x1 <= m_w && x2 > 0)
|
|
{
|
|
switch (i) {
|
|
case 500:
|
|
case 2500:
|
|
p.setPen(penLightYellow);
|
|
break;
|
|
case 1000:
|
|
case 1500:
|
|
case 2000:
|
|
p.setPen(penLightGreen);
|
|
break;
|
|
default:
|
|
p.setPen(penGray);
|
|
break;
|
|
}
|
|
p.drawLine(x1 + 1, 26, x2 - 2, 26);
|
|
p.drawLine(x1 + 1, 28, x2 - 2, 28);
|
|
}
|
|
}
|
|
|
|
p.setPen(Qt::black);
|
|
p.drawLine(0, 29, m_w, 29);
|
|
}
|
|
|
|
// Paint the dial overlay, showing the chunk of the frequency spectrum
|
|
// presently in use.
|
|
|
|
void
|
|
CPlotter::DrawOverlayDial(int const fwidth)
|
|
{
|
|
QPainter p(&m_DialOverlayPixmap);
|
|
|
|
p.setCompositionMode(QPainter::CompositionMode_Source);
|
|
p.fillRect(rect(), Qt::transparent);
|
|
p.setPen(Qt::red);
|
|
p.fillRect(0, 26, fwidth + 2, 4, Qt::red);
|
|
p.fillRect(0, m_h - 4, fwidth + 2, 4, Qt::red);
|
|
p.drawLine(0, 30, 0, m_h); // first slot, left line
|
|
p.drawLine(fwidth + 1, 30, fwidth + 1, m_h); // first slot, right line
|
|
|
|
#if TEST_FOX_WAVE_GEN
|
|
if (m_turbo)
|
|
{
|
|
auto const bwValue = bw(m_nSubMode);
|
|
int const offset = XfromFreq(m_rxFreq + bwValue + TEST_FOX_WAVE_GEN_OFFSET) -
|
|
XfromFreq(m_rxFreq + bwValue + 4; // + 4 for the line padding
|
|
|
|
for (int i = 1; i < TEST_FOX_WAVE_GEN_SLOTS; i++)
|
|
{
|
|
p.fillRect(i*(fwidth + offset), 26, i*(fwidth + offset) + fwidth + 3, 4, Qt::Red);
|
|
p.drawLine(i*(fwidth + offset), 30, i*(fwidth + offset), m_h); // n slot, left line
|
|
p.drawLine(i*(fwidth + offset) + fwidth + 2, 30, i*(fwidth + offset) + fwidth + 2, m_h); // n slot, right line
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Paint the hover overlay, showing the prospective chunk of frequency
|
|
// spectrum under the mouse.
|
|
|
|
void
|
|
CPlotter::DrawOverlayHover(int const fwidth)
|
|
{
|
|
QPainter p(&m_HoverOverlayPixmap);
|
|
|
|
p.setCompositionMode(QPainter::CompositionMode_Source);
|
|
p.fillRect(rect(), Qt::transparent);
|
|
p.setPen(Qt::white);
|
|
p.drawLine(0, 30, 0, m_h); // first slot, left line hover
|
|
p.drawLine(fwidth, 30, fwidth, m_h); // first slot, right line hover
|
|
|
|
#if TEST_FOX_WAVE_GEN
|
|
if (m_turbo)
|
|
{
|
|
for(int i = 1; i < TEST_FOX_WAVE_GEN_SLOTS; i++)
|
|
{
|
|
hoverPainter.drawLine(i*(fwidth + offset), 30, i*(fwidth + offset), m_h); // n slot, left line
|
|
hoverPainter.drawLine(i*(fwidth + offset) + fwidth + 2, 30, i*(fwidth + offset) + fwidth + 2, m_h); // n slot, right line
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if DRAW_FREQ_OVERLAY
|
|
hoverPainter.setFont({"Arial"});
|
|
hoverPainter.drawText(fwidth + 5, m_h, QString("%1").arg(FreqfromX(m_lastMouseX)));
|
|
#endif
|
|
}
|
|
|
|
// Paint the filter overlay pixmap, if the filter is enabled and has a width
|
|
// greater than zero. Note that we could be more clever here and ensure the
|
|
// filter is actually visible prior to painting, but what we're doing here
|
|
// is reasonably trivial, so probably not worth the effort.
|
|
|
|
void
|
|
CPlotter::DrawOverlayFilter()
|
|
{
|
|
if (m_filterEnabled && m_filterWidth > 0)
|
|
{
|
|
QPainter p(&m_FilterOverlayPixmap);
|
|
|
|
p.setCompositionMode(QPainter::CompositionMode_Source);
|
|
p.fillRect(rect(), Qt::transparent);
|
|
|
|
int const start = XfromFreq(static_cast<float>(m_filterCenter - m_filterWidth / 2));
|
|
int const end = XfromFreq(static_cast<float>(m_filterCenter + m_filterWidth / 2));
|
|
|
|
// Yellow vertical line, showing the filter location.
|
|
|
|
p.setPen(Qt::yellow);
|
|
p.drawLine(start, 30, start, m_h);
|
|
p.drawLine(end, 30, end, m_h);
|
|
|
|
// Put a mask over everything outside the bandpass.
|
|
|
|
QColor const blackMask(0, 0, 0, std::clamp(m_filterOpacity, 0, 255));
|
|
|
|
p.fillRect(0, 30, start, m_h, blackMask);
|
|
p.fillRect(end + 1, 30, m_w, m_h, blackMask);
|
|
}
|
|
}
|
|
|
|
int CPlotter::XfromFreq(float const f) const //XfromFreq()
|
|
{
|
|
return std::clamp(static_cast<int>(m_w * (f - m_startFreq) / m_fSpan + 0.5), 0, m_w);
|
|
}
|
|
|
|
float CPlotter::FreqfromX(int const x) const //FreqfromX()
|
|
{
|
|
return float(m_startFreq + x * m_binsPerPixel * m_fftBinWidth);
|
|
}
|
|
|
|
void CPlotter::setPlot2dGain(int const n) //setPlot2dGain
|
|
{
|
|
m_plot2dGain = n;
|
|
update();
|
|
}
|
|
|
|
void CPlotter::setStartFreq(int const f) //SetStartFreq()
|
|
{
|
|
m_startFreq = f;
|
|
resizeEvent(nullptr);
|
|
update();
|
|
}
|
|
|
|
void CPlotter::UpdateOverlay() //UpdateOverlay
|
|
{
|
|
DrawOverlay();
|
|
}
|
|
|
|
void CPlotter::setBinsPerPixel(int const n) //setBinsPerPixel
|
|
{
|
|
m_binsPerPixel = n < 1 ? 1 : n;
|
|
DrawOverlay(); //Redraw scales and ticks
|
|
update(); //trigger a new paintEvent}
|
|
}
|
|
|
|
void CPlotter::setRxFreq(int const x) //setRxFreq
|
|
{
|
|
m_rxFreq = x; // x is freq in Hz
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void CPlotter::leaveEvent(QEvent *)
|
|
{
|
|
m_lastMouseX = -1;
|
|
}
|
|
|
|
void CPlotter::wheelEvent(QWheelEvent * event)
|
|
{
|
|
auto const delta = event->angleDelta();
|
|
|
|
if (delta.isNull())
|
|
{
|
|
event->ignore();
|
|
return;
|
|
}
|
|
|
|
int const dir = delta.y() > 0 ? 1 : -1;
|
|
int newFreq = rxFreq();
|
|
|
|
if(event->modifiers() & Qt::ControlModifier)
|
|
{
|
|
newFreq += dir;
|
|
}
|
|
else
|
|
{
|
|
newFreq = newFreq / 10 * 10 + dir * 10;
|
|
}
|
|
|
|
emit setFreq1(newFreq, newFreq);
|
|
}
|
|
|
|
void CPlotter::mouseMoveEvent(QMouseEvent * event)
|
|
{
|
|
auto const x = std::clamp(static_cast<int>(event->position().x()), 0, m_Size.width());
|
|
|
|
m_lastMouseX = x;
|
|
#if DRAW_FREQ_OVERLAY
|
|
DrawOverlay();
|
|
#endif
|
|
update();
|
|
event->ignore();
|
|
|
|
QToolTip::showText(event->globalPosition().toPoint(),
|
|
QString::number(int(FreqfromX(x))));
|
|
}
|
|
|
|
void CPlotter::mouseReleaseEvent(QMouseEvent * event)
|
|
{
|
|
if (Qt::LeftButton == event->button())
|
|
{
|
|
auto const x = std::clamp(static_cast<int>(event->position().x()), 0, m_Size.width());
|
|
bool const ctrl = event->modifiers() & Qt::ControlModifier;
|
|
bool const shift = event->modifiers() & Qt::ShiftModifier;
|
|
auto const newFreq = int(FreqfromX(x)+0.5);
|
|
int const oldTxFreq = m_txFreq;
|
|
int const oldRxFreq = m_rxFreq;
|
|
|
|
if (ctrl) { emit setFreq1(newFreq, newFreq ); }
|
|
else if (shift) { emit setFreq1(oldRxFreq, newFreq ); }
|
|
else { emit setFreq1(newFreq, oldTxFreq); }
|
|
|
|
emit freezeDecode1(ctrl ? 101 : 1);
|
|
}
|
|
else
|
|
{
|
|
event->ignore(); // let parent handle
|
|
}
|
|
}
|
|
|
|
void CPlotter::mouseDoubleClickEvent(QMouseEvent * event)
|
|
{
|
|
if (Qt::LeftButton == event->button())
|
|
{
|
|
emit freezeDecode1(event->modifiers() & Qt::ControlModifier ? 102 : 2);
|
|
}
|
|
else
|
|
{
|
|
event->ignore(); // let parent handle
|
|
}
|
|
}
|
|
|
|
void CPlotter::setNsps(int const ntrperiod, int const nsps) //setNsps
|
|
{
|
|
m_TRperiod = ntrperiod;
|
|
m_nsps = nsps;
|
|
m_fftBinWidth = fftBinWidth(nsps);
|
|
|
|
DrawOverlay(); //Redraw scales and ticks
|
|
update(); //trigger a new paintEvent}
|
|
}
|
|
|
|
void CPlotter::setTxFreq(int const n) //setTxFreq
|
|
{
|
|
m_txFreq = n;
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void CPlotter::setDialFreq(double const d)
|
|
{
|
|
m_dialFreq = d;
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void CPlotter::setRxBand(QString const & band)
|
|
{
|
|
m_rxBand = band;
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void CPlotter::setTurbo(bool const turbo)
|
|
{
|
|
m_turbo = turbo;
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void CPlotter::setFilterCenter(int const center)
|
|
{
|
|
m_filterCenter = center;
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void CPlotter::setFilterWidth(int const width)
|
|
{
|
|
m_filterWidth = width;
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void CPlotter::setFilterEnabled(bool const enabled)
|
|
{
|
|
m_filterEnabled = enabled;
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void CPlotter::setFilterOpacity(int const alpha)
|
|
{
|
|
m_filterOpacity = alpha;
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void
|
|
CPlotter::setRxRange(int const fMin)
|
|
{
|
|
m_fMin = fMin;
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void
|
|
CPlotter::setMode(QString const & mode)
|
|
{
|
|
m_mode = mode;
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void
|
|
CPlotter::setModeTx(QString const & modeTx)
|
|
{
|
|
m_modeTx = modeTx;
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void
|
|
CPlotter::setSubMode(int const nSubMode)
|
|
{
|
|
m_nSubMode = nSubMode;
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void
|
|
CPlotter::setTol(int const n)
|
|
{
|
|
m_tol = n; // XXX this is never referenced
|
|
DrawOverlay();
|
|
update();
|
|
}
|
|
|
|
void CPlotter::setFlatten(bool const b1, bool const b2)
|
|
{
|
|
m_Flatten = 0;
|
|
if (b1) m_Flatten = 1;
|
|
if (b2) m_Flatten = 2;
|
|
}
|
|
|
|
void CPlotter::SetPercent2DScreen(int percent)
|
|
{
|
|
m_Percent2DScreen=percent;
|
|
resizeEvent(nullptr);
|
|
update();
|
|
}
|