js8call/plotter.cpp

824 lines
21 KiB
C++
Raw Normal View History

2018-02-08 21:28:33 -05:00
#include "plotter.h"
#include <algorithm>
2024-10-26 06:31:47 -07:00
#include <cmath>
2024-11-10 10:52:40 -08:00
#include <numeric>
2024-10-26 06:31:47 -07:00
#include <type_traits>
2024-10-24 07:16:12 -07:00
#include <utility>
2018-02-08 21:28:33 -05:00
#include <QDebug>
2024-09-01 20:03:35 -07:00
#include <QMouseEvent>
#include <QPainter>
#include <QPen>
2024-09-01 08:01:23 -07:00
#include <QToolTip>
2024-09-01 20:03:35 -07:00
#include <QWheelEvent>
2018-02-08 21:28:33 -05:00
#include "commons.h"
#include "moc_plotter.cpp"
#include "DriftingDateTime.h"
#include "JS8Submode.hpp"
2018-03-05 14:49:51 -05:00
extern "C" {
void flat4_(float swide[], int* iz, bool* bflatten);
2018-03-05 14:49:51 -05:00
}
namespace
{
// 30 meter band: 10.130-10.140 RTTY
// 10.140-10.150 Packet
2024-10-24 21:25:49 -07:00
constexpr float BAND_30M_START = 10.13f;
constexpr float BAND_30M_END = 10.15f;
2024-10-24 21:25:49 -07:00
// The WSPR range starts at 10.1401 MHz and runs for 200 Hz.
2024-10-24 21:25:49 -07:00
constexpr float WSPR_START = 10.1401f;
constexpr int WSPR_RANGE = 200;
// FFT bin width, as with NSPS, a constant; see the JT9 documentation
// for the reasoning behind the values used here, but in short, since
// NSPS is always 6912, 1500 for nsps2 and 2048 for nfft3 are optimal.
constexpr double FFT_BIN_WIDTH = 1500.0 / 2048.0;
// Vertical divisions in the spectrum display.
constexpr std::size_t VERT_DIVS = 7;
2024-11-10 14:36:44 -08:00
// Band colors, always drawn with a 3-pixel pen.
constexpr auto BAND_EDGE = QColor{149, 165, 166}; // Gray
constexpr auto BAND_GOOD = QColor{ 46, 204, 113}; // Green
constexpr auto BAND_WARN = QColor{241, 196, 15}; // Yellow
constexpr auto BAND_WSPR = QColor{230, 126, 34}; // Orange
2024-10-26 06:31:47 -07:00
// Given a floating point value, return the fractional portion of the
// value e.g., 42.7 -> 0.7.
template <typename T,
typename = std::enable_if_t<std::is_floating_point_v<T>>>
constexpr auto
2024-10-26 06:31:47 -07:00
fractionalPart(T const v)
{
T integralPart;
return std::modf(v, &integralPart);
}
// Given the frequency span of the entire viewable plot region, return
// the frequency span that each division should occupy.
auto
2024-10-25 13:32:16 -07:00
freqPerDiv(double const fSpan)
{
2024-09-08 20:13:33 -07:00
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;
}
// Return text for a decode line that occurred now.
auto
decodeLineText(int const period,
QString const & band)
{
auto const now = DriftingDateTime::currentDateTimeUtc();
auto const ms = now.toMSecsSinceEpoch() % 86400000;
auto const ts = now.addSecs(-(ms / 1000) % period);
return QString("%1 %2")
.arg(ts.toString(period < 60 ? "hh:mm:ss" : "hh:mm"))
.arg(band);
}
// Given a spectrum, return an appropriate pen to draw it.
auto
spectrumPen(CPlotter::Spectrum const spectrum)
{
switch (spectrum)
{
case CPlotter::Spectrum::Current: return Qt::green;
case CPlotter::Spectrum::Cumulative: return Qt::cyan;
case CPlotter::Spectrum::LinearAvg: return Qt::yellow;
}
}
}
2024-11-09 06:59:38 -08:00
// Our paint event is going to completely paint over our entire areaa with
// opaque content, i.e., it's going to blit 3 pixmaps for scale, waterfall,
// and spectrum, all of which begin life being filled with an opaque color.
// We therefore set the Qt::WA_OpaquePaintEvent, attribute, avoiding any
// unnecessary overhead associated with repainting the background.
2024-10-19 13:44:02 -07:00
CPlotter::CPlotter(QWidget * parent)
: QWidget {parent}
2024-11-08 07:12:52 -08:00
, m_freqPerPixel {m_binsPerPixel * FFT_BIN_WIDTH}
2018-02-08 21:28:33 -05:00
{
2024-11-09 07:09:41 -08:00
setAttribute(Qt::WA_OpaquePaintEvent);
2018-02-08 21:28:33 -05:00
setFocusPolicy(Qt::StrongFocus);
setMouseTracking(true);
2018-02-08 21:28:33 -05:00
}
2024-09-08 12:43:10 -07:00
CPlotter::~CPlotter() = default;
2018-02-08 21:28:33 -05:00
2024-10-19 09:19:01 -07:00
QSize
CPlotter::minimumSizeHint() const
2018-02-08 21:28:33 -05:00
{
return QSize(50, 50);
}
2024-10-19 09:19:01 -07:00
QSize
CPlotter::sizeHint() const
2018-02-08 21:28:33 -05:00
{
return QSize(180, 180);
}
2024-10-19 09:19:01 -07:00
void
2024-10-20 21:25:26 -07:00
CPlotter::resizeEvent(QResizeEvent *)
2018-02-08 21:28:33 -05:00
{
2024-09-08 10:00:57 -07:00
if (!size().isValid()) return;
2024-11-07 07:31:29 -08:00
auto const makePixmap = [dpr = devicePixelRatio()](QSize const & size,
QColor const & fill)
{
auto pixmap = QPixmap(size * dpr);
pixmap.setDevicePixelRatio(dpr);
pixmap.fill(fill);
return pixmap;
};
if ((m_size != size()) ||
2024-10-20 21:25:26 -07:00
(m_percent2DScreen != m_percent2DScreen0))
2024-09-08 10:00:57 -07:00
{
2024-10-19 13:49:37 -07:00
m_size = size();
m_w = m_size.width();
2024-11-10 13:45:10 -08:00
m_h2 = m_percent2DScreen * (m_size.height() - 30) / 100.0;
m_h1 = m_size.height() - m_h2;
2024-11-10 06:43:50 -08:00
// We want our 3 main pixmaps sized to occupy our entire height,
// and to be completely filled with an opaque color, since we're
// going to take the opaque paint even optimization path. If this
// is a high-DPI display, scale the pixmaps to avoid text looking
// pixelated.
2024-11-07 17:51:23 -08:00
m_ScalePixmap = makePixmap({m_w, 30}, Qt::white);
m_WaterfallPixmap = makePixmap({m_w, m_h1}, Qt::black);
m_OverlayPixmap = makePixmap({m_w, m_h2}, Qt::black);
2024-09-08 14:17:32 -07:00
2024-11-10 06:43:50 -08:00
// The replot circular buffer should have capacity to hold the full
// height of the waterfall pixmap, in device, not logical, pixels.
2024-11-10 06:43:50 -08:00
m_replot = Replot(m_WaterfallPixmap.size().height());
2024-11-10 14:36:44 -08:00
// The dials, filter, scale and overlay pixmaps don't depend on
// inbound data, so we can draw them now.
2024-11-10 06:43:50 -08:00
drawDials();
2024-11-07 17:51:23 -08:00
drawFilter();
2024-11-10 14:36:44 -08:00
drawMetrics();
2024-10-31 07:08:48 -07:00
// The overlay pixmap acts as a prototype for the spectrum pixmap;
// each time we draw the spectrum, we do so by first making a copy
// of the overlay, then drawing the spectrum line into it.
2024-11-07 07:31:29 -08:00
m_SpectrumPixmap = m_OverlayPixmap.copy();
2024-10-20 21:25:26 -07:00
m_percent2DScreen0 = m_percent2DScreen;
2018-02-08 21:28:33 -05:00
}
}
2024-10-19 09:19:01 -07:00
void
2024-10-20 21:25:26 -07:00
CPlotter::paintEvent(QPaintEvent *)
2018-02-08 21:28:33 -05:00
{
QPainter p(this);
p.drawPixmap(0, 0, m_ScalePixmap);
p.drawPixmap(0, 30, m_WaterfallPixmap);
2024-10-31 06:53:30 -07:00
p.drawPixmap(0, m_h1, m_SpectrumPixmap);
p.drawPixmap(xFromFreq(m_freq), 30, m_DialPixmap[0]);
if (m_lastMouseX >= 0)
{
p.drawPixmap(m_lastMouseX, 30, m_DialPixmap[1]);
}
if (m_filterEnabled && m_filterWidth > 0)
{
p.drawPixmap( 0, 0, m_FilterPixmap[0]);
p.drawPixmap(m_w - m_FilterPixmap[1].deviceIndependentSize().width(), 0, m_FilterPixmap[1]);
}
2018-02-08 21:28:33 -05:00
}
2024-09-13 14:08:06 -07:00
void
CPlotter::draw(float swide[],
2024-11-09 22:47:20 -08:00
bool const bReplot)
2018-02-08 21:28:33 -05:00
{
2024-10-31 06:53:30 -07:00
// Move current data down one line; we must do this before
// attaching a QPainter.
m_WaterfallPixmap.scroll(0, 1, m_WaterfallPixmap.rect());
2024-09-08 09:00:09 -07:00
2024-10-31 06:53:30 -07:00
QPainter p(&m_WaterfallPixmap);
2024-09-08 20:09:29 -07:00
if (!bReplot && swide[0] < 1.e29f)
2024-09-08 20:09:29 -07:00
{
flat4_(swide, &m_w, &m_flatten);
2018-02-08 21:28:33 -05:00
}
if(swide[0] > 1.e29f) p.setPen(Qt::green); // horizontal line
2024-09-08 20:09:29 -07:00
2024-11-09 22:47:20 -08:00
if (!bReplot)
2024-09-08 20:09:29 -07:00
{
2024-11-10 04:10:38 -08:00
m_replot.push_back({swide, swide + m_w});
2018-03-05 14:49:51 -05:00
}
2024-09-08 20:09:29 -07:00
2024-11-10 13:03:44 -08:00
auto const fac = std::sqrt(m_binsPerPixel * m_waterfallAvg / 15.0f);
auto const gain = 10.0f * fac * std::pow(10.0f, 0.015f * m_plotGain);
auto ymin = 1.e30f;
2024-09-13 14:08:06 -07:00
// First loop; draws points into the waterfall and determines the
// minimum y extent.
for (int i = 0; i < m_w; i++)
2024-09-08 20:01:56 -07:00
{
float const y = swide[i];
if (y < ymin ) ymin = y;
if (y < 1.e29f) p.setPen(m_colors[std::clamp(m_plotZero + static_cast<int>(gain * y), 0, 254)]);
p.drawPoint(i, 0);
2018-02-08 21:28:33 -05:00
}
2024-11-10 15:01:58 -08:00
// If this is a replot, then we're done here; our mission was just
// to redraw the waterfall. Note that we're not going to restore any
// decode line text, which isn't ideal.
if (bReplot) return;
// If we've just drawn a decode line, compute the number of lines required
// before we need to draw the decode text. If that wasn't a decode line,
// see if we've reached the point where we should draw the decode text.
if (swide[0] > 1.e29f)
{
m_line = p.fontMetrics().height() * devicePixelRatio();
m_text = decodeLineText(m_period, m_band);
}
else if (--m_line == 0)
{
m_line = std::numeric_limits<int>::max();
p.setPen(Qt::white);
p.drawText(5, p.fontMetrics().ascent(), m_text);
}
2024-11-10 14:58:20 -08:00
// Our spectrum might be of zero height, in which case our overlay pixmap
// isn't going to be usable; proceed to spectrum work only if it's usable.
2024-11-10 14:58:20 -08:00
if (!m_OverlayPixmap.isNull())
{
2024-11-10 14:58:20 -08:00
// Summarization method, used for computation of cumulative and
// linear average data.
2024-09-13 14:08:06 -07:00
2024-11-10 14:58:20 -08:00
auto const sum = [base = static_cast<int>(m_startFreq / FFT_BIN_WIDTH + 0.5),
bins = m_binsPerPixel](float const * const data,
auto const index)
{
auto const offset = data + base + bins * index;
2024-09-13 14:08:06 -07:00
2024-11-10 14:58:20 -08:00
return std::accumulate(offset, offset + bins, 0.0f) / bins;
};
2024-11-09 13:15:00 -08:00
2024-11-10 14:58:20 -08:00
// Clear the current points and ensure space exists to add all the
// points we require without reallocation.
2024-11-09 13:15:00 -08:00
2024-11-10 14:58:20 -08:00
m_points.clear();
m_points.reserve(m_w);
2024-11-10 14:58:20 -08:00
// Compute gain for the spectrum.
2024-11-10 14:58:20 -08:00
auto const gain2d = std::pow(10.0f, 0.02f * m_plot2dGain);
2024-09-13 14:08:06 -07:00
2024-11-10 14:58:20 -08:00
// Second loop, determines how we're going to draw the spectrum.
2024-09-13 13:03:50 -07:00
2024-11-10 14:58:20 -08:00
for (int i = 0; i < m_w; i++)
2024-09-08 20:09:29 -07:00
{
2024-11-10 14:58:20 -08:00
float y = 0;
switch (m_spectrum)
{
case Spectrum::Current:
y = gain2d * (swide[i] - ymin) + m_plot2dZero + (m_flatten ? 0 : 15);
break;
case Spectrum::Cumulative:
y = gain2d * (sum(dec_data.savg, i) + m_plot2dZero) + (m_flatten ? 0 : 15);
break;
case Spectrum::LinearAvg:
y = gain2d * sum(spectra_.syellow, i) + m_plot2dZero;
break;
}
m_points.emplace_back(i, static_cast<int>(0.9f * m_h2 - y * m_h2 / 70.0f));
2018-02-08 21:28:33 -05:00
}
2024-11-10 15:01:58 -08:00
// Draw the spectrum by copying the overlay prototype and drawing
// the points into it.
2018-02-08 21:28:33 -05:00
2024-11-10 14:58:20 -08:00
m_SpectrumPixmap = m_OverlayPixmap.copy();
2024-10-31 06:53:30 -07:00
2024-11-10 14:58:20 -08:00
QPainter p(&m_SpectrumPixmap);
2024-10-31 06:53:30 -07:00
2024-11-10 14:58:20 -08:00
p.setRenderHint(QPainter::Antialiasing);
p.setPen(spectrumPen(m_spectrum));
p.drawPolyline(m_points);
}
update();
2024-10-31 06:53:30 -07:00
}
2024-10-19 09:19:01 -07:00
void
CPlotter::drawDecodeLine(QColor const & color,
int const ia,
int const ib)
2018-02-08 21:28:33 -05:00
{
auto const x1 = xFromFreq(ia);
auto const x2 = xFromFreq(ib);
2020-05-06 11:15:41 -04:00
2024-10-25 20:41:55 -07:00
QPainter p(&m_WaterfallPixmap);
2024-09-08 17:53:35 -07:00
2024-10-25 20:41:55 -07:00
p.setPen(color);
p.drawLine(qMin(x1, x2), 4, qMax(x1, x2), 4);
p.drawLine(qMin(x1, x2), 0, qMin(x1, x2), 9);
p.drawLine(qMax(x1, x2), 0, qMax(x1, x2), 9);
2018-02-08 21:28:33 -05:00
}
2024-10-19 09:19:01 -07:00
void
CPlotter::drawHorizontalLine(QColor const & color,
int const x,
int const width)
{
2024-10-25 20:41:55 -07:00
QPainter p(&m_WaterfallPixmap);
2024-09-08 19:54:14 -07:00
2024-10-25 20:41:55 -07:00
p.setPen(color);
p.drawLine(x, 0, width <= 0 ? m_w : x + width, 0);
}
void
2024-11-10 14:36:44 -08:00
CPlotter::drawMetrics()
2024-09-09 07:50:19 -07:00
{
2024-11-10 14:42:37 -08:00
if (m_ScalePixmap.isNull()) return;
m_ScalePixmap.fill(Qt::white);
2024-11-10 14:36:44 -08:00
2024-09-09 07:50:19 -07:00
QPainter p(&m_ScalePixmap);
p.setPen(Qt::black);
2024-09-12 13:04:50 -07:00
p.drawRect(0, 0, m_w, 30);
2024-09-09 07:50:19 -07:00
2024-11-10 14:36:44 -08:00
auto const fSpan = m_w * m_freqPerPixel;
auto const fpd = freqPerDiv(fSpan);
float const ppdV = fpd / m_freqPerPixel;
std::size_t const hdivs = fSpan / fpd + 1.9999;
int const fOffset = ((m_startFreq + fpd - 1) / fpd) * fpd;
double const xOffset = double(fOffset - m_startFreq) / fpd;
2024-10-25 13:32:16 -07:00
std::size_t const nMajor = hdivs - 1;
std::size_t const nMinor = fpd == 200 ? 4: 5;
2024-09-12 19:00:35 -07:00
float const ppdVM = ppdV / nMinor;
float const ppdVL = ppdV / 2;
2024-09-12 19:00:35 -07:00
// Draw ticks and labels.
2024-09-09 07:50:19 -07:00
2024-10-25 13:32:16 -07:00
for (std::size_t iMajor = 0; iMajor < nMajor; iMajor++)
{
2024-09-12 19:00:35 -07:00
auto const rMajor = (xOffset + iMajor) * ppdV;
auto const xMajor = static_cast<int>(rMajor);
p.drawLine(xMajor, 18, xMajor, 30);
2024-09-12 19:00:35 -07:00
for (std::size_t iMinor = 1; iMinor < nMinor; iMinor++)
{
2024-09-12 19:00:35 -07:00
auto const xMinor = static_cast<int>(rMajor + iMinor * ppdVM);
p.drawLine(xMinor, 22, xMinor, 30);
}
2024-09-12 19:00:35 -07:00
if (xMajor > 70)
{
p.drawText(QRect(xMajor - static_cast<int>(ppdVL), 0, static_cast<int>(ppdV), 20),
Qt::AlignCenter,
QString::number(fOffset + iMajor * fpd));
2024-09-12 19:00:35 -07:00
}
2018-02-08 21:28:33 -05:00
}
2024-10-24 07:16:12 -07:00
// Given a starting frequency and range to cover, return corresponding
// X values for the sub-band.
auto const bandX = [this](float const start,
int const range)
{
return std::make_pair(xFromFreq(start),
xFromFreq(start + range));
};
// Given a pair of X values, draw a band line, if visible.
2024-10-24 07:16:12 -07:00
auto const drawBand = [this, &p](auto const & bandX)
{
2024-10-24 07:16:12 -07:00
auto const [x1, x2] = bandX;
2024-09-10 06:36:41 -07:00
2024-09-10 06:26:15 -07:00
if (x1 <= m_w && x2 > 0)
2024-09-08 21:28:23 -07:00
{
2024-09-09 07:50:19 -07:00
p.drawLine(x1 + 1, 26, x2 - 2, 26);
p.drawLine(x1 + 1, 28, x2 - 2, 28);
2024-09-08 21:28:23 -07:00
}
2024-10-24 07:16:12 -07:00
};
// Colorize the JS8 sub-bands.
2024-11-10 14:36:44 -08:00
p.setPen(QPen(BAND_EDGE, 3)); drawBand(bandX( 0.0f, 4000));
p.setPen(QPen(BAND_WARN, 3)); drawBand(bandX( 500.0f, 2500));
p.setPen(QPen(BAND_GOOD, 3)); drawBand(bandX(1000.0f, 1500));
// If we're in the 30 meter band, we'd rather that the WSPR sub-band not
// get stomped on; draw an orange indicator in the scale to denote the
// WSPR portion of the band.
//
// Note that given the way XfromFreq() works, we're always going to see
// clamped X values here, either 0 or m_w, if the frequency is outside
// of the range, so we're always going to draw. If the WSPR range is not
// in the displayed range, the effect will be, given the pen size, that
// an orange indicator will indicate in which direction the WSPR range
// lies.
if (in30MBand())
{
2024-10-24 21:31:34 -07:00
auto const wspr = bandX(1.0e6f * (WSPR_START - m_dialFreq), WSPR_RANGE);
2024-11-10 15:56:07 -08:00
auto font = QFont();
2024-11-10 15:56:07 -08:00
font.setBold(true);
font.setPointSize(10);
p.setFont(font);
2024-11-10 14:36:44 -08:00
p.setPen(QPen(BAND_WSPR, 3));
2024-10-24 07:16:12 -07:00
drawBand(wspr);
p.drawText(QRect(wspr.first, 0, wspr.second - wspr.first, 25),
Qt::AlignHCenter|Qt::AlignBottom,
"WSPR");
}
2024-11-10 14:58:20 -08:00
// Our spectrum might be of zero height, in which case our overlay pixmap
// isn't going to be usable; proceed only if it's usable.
2024-11-10 14:58:20 -08:00
if (!m_OverlayPixmap.isNull())
{
QLinearGradient gradient(0, 0, 0, m_h2);
2024-11-10 14:36:44 -08:00
2024-11-10 14:58:20 -08:00
gradient.setColorAt(1, Qt::black);
gradient.setColorAt(0, Qt::darkBlue);
2024-11-10 14:36:44 -08:00
2024-11-10 14:58:20 -08:00
QPainter p(&m_OverlayPixmap);
2024-11-10 14:36:44 -08:00
2024-11-10 14:58:20 -08:00
p.setBrush(gradient);
p.drawRect(0, 0, m_w, m_h2);
p.setBrush(Qt::SolidPattern);
p.setPen(QPen(Qt::darkGray, 1, Qt::DotLine));
2024-11-10 14:36:44 -08:00
2024-11-10 14:58:20 -08:00
// Draw vertical grids.
2024-11-10 14:36:44 -08:00
2024-11-10 14:58:20 -08:00
auto const x0 = static_cast<int>(fractionalPart((double)m_startFreq / fpd) * ppdV + 0.5);
2024-11-10 14:36:44 -08:00
2024-11-10 14:58:20 -08:00
for (std::size_t i = 1; i < hdivs; i++)
2024-11-10 14:36:44 -08:00
{
2024-11-10 14:58:20 -08:00
if (auto const x = static_cast<int>(i * ppdV) - x0;
x >= 0 &&
x <= m_w)
{
p.drawLine(x, 0, x , m_h2);
}
2024-11-10 14:36:44 -08:00
}
2024-11-10 14:58:20 -08:00
// Draw horizontal grids.
float const ppdH = (float)m_h2 / VERT_DIVS;
2024-11-10 14:36:44 -08:00
2024-11-10 14:58:20 -08:00
for (std::size_t i = 1; i < VERT_DIVS; i++)
{
auto const y = static_cast<int>(i * ppdH);
p.drawLine(0, y, m_w, y);
}
2024-11-10 14:36:44 -08:00
}
}
// Draw the filter overlay pixmaps, 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
2024-11-07 17:51:23 -08:00
CPlotter::drawFilter()
{
2024-11-08 07:08:09 -08:00
if (m_filterEnabled && m_filterWidth > 0 && !size().isEmpty())
{
2024-11-08 07:08:09 -08:00
auto const filterPixmap = [height = size().height(),
fill = QColor(0, 0, 0, std::clamp(m_filterOpacity, 0, 255)),
dpr = devicePixelRatio()](int const width,
int const lineX)
{
2024-11-08 07:51:56 -08:00
// Ending up with an unusable size here is expected, as in the case
// where the combination of the filter center and width shifts one
// or both ends of the filter out of the displayed range. Thus, no
// matter what, we're going to return a pixmap here, though it may
// be an empty one.
2024-11-08 07:08:09 -08:00
if (auto const size = QSize(width, height);
2024-11-08 07:51:56 -08:00
size.isEmpty())
{
return QPixmap();
}
else
2024-11-08 07:08:09 -08:00
{
QPixmap pixmap = QPixmap(size * dpr);
pixmap.setDevicePixelRatio(dpr);
pixmap.fill(fill);
2024-11-08 07:08:09 -08:00
QPainter p(&pixmap);
2024-11-08 07:08:09 -08:00
p.setPen(Qt::yellow);
p.drawLine(lineX, 1, lineX, height);
2024-11-07 07:00:04 -08:00
2024-11-08 07:08:09 -08:00
return pixmap;
}
};
2024-11-07 07:00:04 -08:00
auto const width = m_filterWidth / 2.0f;
auto const start = xFromFreq(m_filterCenter - width);
auto const end = xFromFreq(m_filterCenter + width);
m_FilterPixmap = {
filterPixmap(start, start),
filterPixmap(size().width() - end, 0)
};
}
2018-02-08 21:28:33 -05:00
}
// Draw the two dials, the first of which will be used to display the selected
// offset and bandwith, the second prospective offset and bandwidth. These are
// not reliant on anything but height, submode, and bins per pixel.
void
CPlotter::drawDials()
{
2024-11-08 07:08:09 -08:00
if (auto const height = size().height() - 30;
height > 0)
{
2024-11-08 07:08:09 -08:00
auto const width = static_cast<int>(JS8::Submode::bandwidth(m_nSubMode) / m_freqPerPixel + 0.5);
auto const dialPixmap = [size = QSize(width, height),
rect = QRect(1, 1, width - 2, height - 2),
dpr = devicePixelRatio()](QColor const & color,
QBrush const & brush)
{
QPixmap pixmap = QPixmap(size * dpr);
pixmap.setDevicePixelRatio(dpr);
pixmap.fill(Qt::transparent);
2024-11-08 07:08:09 -08:00
QPainter p(&pixmap);
2024-11-08 07:08:09 -08:00
p.setBrush(brush);
p.setPen(QPen(QBrush(color), 2, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin));
p.drawRect(rect);
2024-11-08 07:08:09 -08:00
return pixmap;
};
2024-11-08 07:08:09 -08:00
m_DialPixmap = {
dialPixmap(Qt::red, QBrush(QColor(255, 255, 255, 75), Qt::Dense4Pattern)),
dialPixmap(Qt::white, Qt::transparent)
};
}
}
void
CPlotter::replot()
{
m_WaterfallPixmap.fill(Qt::black);
for (auto & entry : m_replot)
{
draw(entry.data(), true);
}
update();
}
bool
CPlotter::in30MBand() const
{
return (m_dialFreq >= BAND_30M_START &&
m_dialFreq <= BAND_30M_END);
}
int
2024-10-20 21:20:32 -07:00
CPlotter::xFromFreq(float const f) const
2018-02-08 21:28:33 -05:00
{
2024-10-26 04:30:16 -07:00
return std::clamp(static_cast<int>((f - m_startFreq) / m_freqPerPixel + 0.5), 0, m_w);
2018-02-08 21:28:33 -05:00
}
float
2024-10-20 21:20:32 -07:00
CPlotter::freqFromX(int const x) const
2018-02-08 21:28:33 -05:00
{
2024-10-26 04:30:16 -07:00
return m_startFreq + x * m_freqPerPixel;
2018-02-08 21:28:33 -05:00
}
void
CPlotter::leaveEvent(QEvent * event)
{
2024-10-19 09:19:01 -07:00
m_lastMouseX = -1;
event->ignore();
}
void
CPlotter::wheelEvent(QWheelEvent * event)
2024-09-08 19:54:14 -07:00
{
2024-10-20 21:20:32 -07:00
auto const y = event->angleDelta().y();
2024-09-08 19:54:14 -07:00
2024-10-20 21:20:32 -07:00
if (auto const d = ((y > 0) - (y < 0)))
2024-09-08 19:54:14 -07:00
{
2024-10-20 21:20:32 -07:00
emit changeFreq(event->modifiers() & Qt::ControlModifier
? freq() + d
: freq() / 10 * 10 + d * 10);
2024-09-08 19:54:14 -07:00
}
else
{
2024-10-20 21:20:32 -07:00
event->ignore();
}
}
void
CPlotter::mouseMoveEvent(QMouseEvent * event)
{
2024-10-19 13:49:37 -07:00
m_lastMouseX = std::clamp(static_cast<int>(event->position().x()), 0, m_w);
2024-09-08 09:00:09 -07:00
update();
event->ignore();
2018-02-08 21:28:33 -05:00
2024-09-08 09:00:09 -07:00
QToolTip::showText(event->globalPosition().toPoint(),
QString::number(static_cast<int>(freqFromX(m_lastMouseX))));
2024-09-08 09:00:09 -07:00
}
void
CPlotter::mouseReleaseEvent(QMouseEvent * event)
2024-09-08 09:00:09 -07:00
{
if (Qt::LeftButton == event->button())
{
emit changeFreq(static_cast<int>(freqFromX(m_lastMouseX)));
2018-02-08 21:28:33 -05:00
}
2024-09-08 09:00:09 -07:00
else
{
2024-10-20 21:20:32 -07:00
event->ignore();
2018-02-08 21:28:33 -05:00
}
}
void
2024-10-26 04:30:16 -07:00
CPlotter::setBinsPerPixel(int const binsPerPixel)
2018-02-08 21:28:33 -05:00
{
2024-11-07 13:44:04 -08:00
if (m_binsPerPixel != binsPerPixel)
{
m_binsPerPixel = std::max(1, binsPerPixel);
m_freqPerPixel = m_binsPerPixel * FFT_BIN_WIDTH;
2024-11-10 14:36:44 -08:00
drawMetrics();
2024-11-07 17:51:23 -08:00
drawFilter();
2024-11-07 17:54:31 -08:00
drawDials();
2024-11-07 13:44:04 -08:00
update();
}
2018-02-08 21:28:33 -05:00
}
void
CPlotter::setColors(Colors const & colors)
{
if (m_colors != colors)
{
m_colors = colors;
replot();
}
}
void
2024-10-24 21:25:49 -07:00
CPlotter::setDialFreq(float const dialFreq)
2018-02-08 21:28:33 -05:00
{
2024-11-07 13:44:04 -08:00
if (m_dialFreq != dialFreq)
{
m_dialFreq = dialFreq;
2024-11-10 14:36:44 -08:00
drawMetrics();
2024-11-07 13:44:04 -08:00
update();
}
2018-02-08 21:28:33 -05:00
}
void
2024-11-08 07:31:19 -08:00
CPlotter::setFilter(int const filterCenter,
int const filterWidth)
2018-02-08 21:28:33 -05:00
{
2024-11-08 07:31:19 -08:00
if (m_filterCenter != filterCenter ||
m_filterWidth != filterWidth)
2024-11-07 13:44:04 -08:00
{
m_filterCenter = filterCenter;
2024-11-08 07:31:19 -08:00
m_filterWidth = filterWidth;
2024-11-07 17:51:23 -08:00
drawFilter();
2024-11-07 13:44:04 -08:00
update();
}
2018-11-04 22:37:14 -05:00
}
void
2024-11-07 13:44:04 -08:00
CPlotter::setFilterEnabled(bool const filterEnabled)
2024-09-08 09:00:09 -07:00
{
2024-11-07 13:44:04 -08:00
if (m_filterEnabled != filterEnabled)
{
m_filterEnabled = filterEnabled;
2024-11-07 17:51:23 -08:00
drawFilter();
2024-11-07 13:44:04 -08:00
update();
}
2024-10-26 04:30:16 -07:00
}
void
2024-11-07 13:44:04 -08:00
CPlotter::setFilterOpacity(int const filterOpacity)
2024-10-26 04:30:16 -07:00
{
2024-11-07 13:44:04 -08:00
if (m_filterOpacity != filterOpacity)
{
m_filterOpacity = filterOpacity;
2024-11-07 17:51:23 -08:00
drawFilter();
2024-11-07 13:44:04 -08:00
update();
}
}
void
2024-10-26 04:30:16 -07:00
CPlotter::setFreq(int const freq)
2024-09-08 09:00:09 -07:00
{
2024-11-07 13:44:04 -08:00
if (m_freq != freq)
{
m_freq = freq;
2024-11-10 14:36:44 -08:00
drawMetrics();
2024-11-07 13:44:04 -08:00
update();
}
}
void
2024-11-07 13:44:04 -08:00
CPlotter::setPercent2DScreen(int percent2DScreen)
2024-09-08 09:00:09 -07:00
{
2024-11-07 13:44:04 -08:00
if (m_percent2DScreen != percent2DScreen)
{
m_percent2DScreen = percent2DScreen;
resizeEvent(nullptr);
update();
}
}
void
2024-10-26 04:30:16 -07:00
CPlotter::setPeriod(int const period)
{
2024-11-07 13:44:04 -08:00
if (m_period != period)
{
m_period = period;
update();
}
}
void
CPlotter::setPlotGain(int const plotGain)
2018-02-08 21:28:33 -05:00
{
if (m_plotGain != plotGain)
2024-11-07 13:44:04 -08:00
{
m_plotGain = plotGain;
replot();
}
}
void
CPlotter::setPlotZero(int const plotZero)
{
if (m_plotZero != plotZero)
{
m_plotZero = plotZero;
replot();
2024-11-07 13:44:04 -08:00
}
2024-10-26 04:30:16 -07:00
}
void
CPlotter::setStartFreq(int const startFreq)
{
2024-11-07 13:44:04 -08:00
if (m_startFreq != startFreq)
{
m_startFreq = startFreq;
2024-11-10 14:36:44 -08:00
drawMetrics();
drawFilter();
2024-11-07 13:44:04 -08:00
update();
}
2024-10-26 04:30:16 -07:00
}
void
CPlotter::setSubMode(int const nSubMode)
{
if (m_nSubMode != nSubMode)
{
m_nSubMode = nSubMode;
drawDials();
update();
}
2018-02-08 21:28:33 -05:00
}