1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-11-25 09:18:54 -05:00
sdrangel/plugins/channelrx/heatmap/heatmapgui.cpp

1655 lines
56 KiB
C++

///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2023 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2023 Mohamed <mohamedadlyi@github.com> //
// //
// 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 as 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 V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <limits>
#include <ctype.h>
#include <QMainWindow>
#include <QDebug>
#include <QMessageBox>
#include <QAction>
#include <QClipboard>
#include <QBuffer>
#include "heatmapgui.h"
#include "device/deviceuiset.h"
#include "device/deviceapi.h"
#include "dsp/dspengine.h"
#include "dsp/dspcommands.h"
#include "ui_heatmapgui.h"
#include "plugin/pluginapi.h"
#include "util/astronomy.h"
#include "util/csv.h"
#include "util/colormap.h"
#include "util/db.h"
#include "util/units.h"
#include "gui/basicchannelsettingsdialog.h"
#include "gui/dialpopup.h"
#include "gui/dialogpositioner.h"
#include "dsp/dspengine.h"
#include "maincore.h"
#include "heatmap.h"
#include "SWGMapItem.h"
HeatMapGUI* HeatMapGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel)
{
HeatMapGUI* gui = new HeatMapGUI(pluginAPI, deviceUISet, rxChannel);
return gui;
}
void HeatMapGUI::destroy()
{
delete this;
}
void HeatMapGUI::resetToDefaults()
{
m_settings.resetToDefaults();
displaySettings();
applySettings(true);
}
QByteArray HeatMapGUI::serialize() const
{
return m_settings.serialize();
}
bool HeatMapGUI::deserialize(const QByteArray& data)
{
if(m_settings.deserialize(data)) {
displaySettings();
applySettings(true);
return true;
} else {
resetToDefaults();
return false;
}
}
bool HeatMapGUI::handleMessage(const Message& message)
{
if (HeatMap::MsgConfigureHeatMap::match(message))
{
qDebug("HeatMapGUI::handleMessage: HeatMap::MsgConfigureHeatMap");
const HeatMap::MsgConfigureHeatMap& cfg = (HeatMap::MsgConfigureHeatMap&) message;
m_settings = cfg.getSettings();
blockApplySettings(true);
ui->scopeGUI->updateSettings();
m_channelMarker.updateSettings(static_cast<const ChannelMarker*>(m_settings.m_channelMarker));
displaySettings();
blockApplySettings(false);
return true;
}
else if (DSPSignalNotification::match(message))
{
DSPSignalNotification& notif = (DSPSignalNotification&) message;
m_deviceCenterFrequency = notif.getCenterFrequency();
m_basebandSampleRate = notif.getSampleRate();
ui->deltaFrequency->setValueRange(false, 7, -m_basebandSampleRate/2, m_basebandSampleRate/2);
ui->deltaFrequencyLabel->setToolTip(tr("Range %1 %L2 Hz").arg(QChar(0xB1)).arg(m_basebandSampleRate/2));
updateAbsoluteCenterFrequency();
return true;
}
return false;
}
void HeatMapGUI::handleInputMessages()
{
Message* message;
while ((message = getInputMessageQueue()->pop()) != 0)
{
if (handleMessage(*message))
{
delete message;
}
}
}
void HeatMapGUI::channelMarkerChangedByCursor()
{
ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency());
m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
applySettings();
}
void HeatMapGUI::channelMarkerHighlightedByCursor()
{
setHighlighted(m_channelMarker.getHighlighted());
}
void HeatMapGUI::on_deltaFrequency_changed(qint64 value)
{
m_channelMarker.setCenterFrequency(value);
m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
updateAbsoluteCenterFrequency();
applySettings();
}
void HeatMapGUI::on_rfBW_valueChanged(int value)
{
float bw = value * 100.0f;
ui->rfBWText->setText(QString("%1k").arg(value / 10.0, 0, 'f', 1));
m_channelMarker.setBandwidth(bw);
m_settings.m_rfBandwidth = bw;
applySettings();
}
void HeatMapGUI::on_minPower_valueChanged(double value)
{
m_settings.m_minPower = (float)value;
plotMap();
if (m_powerYAxis) {
m_powerYAxis->setMin(m_settings.m_minPower);
}
applySettings();
}
void HeatMapGUI::on_maxPower_valueChanged(double value)
{
m_settings.m_maxPower = (float)value;
plotMap();
if (m_powerYAxis) {
m_powerYAxis->setMax(m_settings.m_maxPower);
}
applySettings();
}
void HeatMapGUI::on_colorMap_currentIndexChanged(int index)
{
if (index >= 0)
{
m_settings.m_colorMapName = ui->colorMap->currentText();
m_colorMap = ColorMap::getColorMap(m_settings.m_colorMapName);
}
plotMap();
applySettings();
}
void HeatMapGUI::on_pulseTH_valueChanged(int value)
{
m_settings.m_pulseThreshold = (float)value;
ui->pulseTHText->setText(QString::number(value));
applySettings();
}
const QStringList HeatMapGUI::m_averagePeriodTexts = {
"10us", "100us", "1ms", "10ms", "100ms", "1s", "10s"
};
void HeatMapGUI::on_averagePeriod_valueChanged(int value)
{
m_settings.m_averagePeriodUS = (int)std::pow(10.0f, (float)value);
ui->averagePeriodText->setText(m_averagePeriodTexts[value-1]);
applySettings();
}
const QStringList HeatMapGUI::m_sampleRateTexts = {
"100", "1k", "10k", "100k", "1M", "10M"
};
void HeatMapGUI::on_sampleRate_valueChanged(int value)
{
m_settings.m_sampleRate = (int)std::pow(10.0f, (float)value);
ui->sampleRateText->setText(m_sampleRateTexts[value-2]); // value range is [2,7]
ui->averagePeriod->setMinimum(std::max(1, static_cast<int> (m_averagePeriodTexts.size()) - value));
ui->rfBW->setMaximum(m_settings.m_sampleRate/100);
m_scopeVis->setLiveRate(m_settings.m_sampleRate);
applySettings();
}
void HeatMapGUI::on_mode_currentIndexChanged(int index)
{
if (index >= 0)
{
m_settings.m_mode = (HeatMapSettings::Mode)index;
bool none = m_settings.m_mode == HeatMapSettings::None;
ui->writeImage->setEnabled(!none);
ui->writeCSV->setEnabled(!none);
ui->readCSV->setEnabled(!none);
ui->colorMapLabel->setEnabled(!none);
ui->colorMap->setEnabled(!none);
if (none)
{
deleteFromMap();
}
else
{
if (m_image.isNull()) {
createImage(m_width, m_height);
}
plotMap();
}
applySettings();
}
}
void HeatMapGUI::displayPowerChart()
{
if (m_settings.m_displayChart)
{
ui->chartContainer->setVisible(true);
plotPowerVsTimeChart();
}
else
{
ui->chartContainer->setVisible(false);
QChart *emptyChart = new QChart();
emptyChart->setTheme(QChart::ChartThemeDark);
ui->powerChart->setChart(emptyChart); // Can't set to nullptr
delete m_powerChart;
m_powerChart = emptyChart;
m_powerAverageSeries = nullptr; // Deleted when chart deleted?
}
}
void HeatMapGUI::on_displayChart_clicked(bool checked)
{
m_settings.m_displayChart = checked;
displayPowerChart();
applySettings();
}
void HeatMapGUI::on_clearHeatMap_clicked()
{
// Clear heat map
m_heatMap->resetMagLevels();
clearPower();
plotMap();
if (m_powerAverageSeries)
{
m_powerAverageSeries->clear();
m_powerMaxPeakSeries->clear();
m_powerMinPeakSeries->clear();
m_powerPulseAverageSeries->clear();
m_powerPathLossSeries->clear();
}
}
bool HeatMapGUI::pixelValid(int x, int y) const
{
return (y >= 0) && (x >= 0) && (y < m_height) && (x < m_width);
}
void HeatMapGUI::coordsToPixel(double latitude, double longitude, int& x, int& y) const
{
y = m_height - (latitude - m_south) / m_degreesLatPerPixel;
x = (longitude - m_west) / m_degreesLonPerPixel;
}
void HeatMapGUI::pixelToCoords(int x, int y, double& latitude, double& longitude) const
{
latitude = m_north - y * m_degreesLatPerPixel;
longitude = m_west + x * m_degreesLonPerPixel;
}
void HeatMapGUI::on_writeCSV_clicked()
{
m_csvFileDialog.setAcceptMode(QFileDialog::AcceptSave);
m_csvFileDialog.setNameFilter("*.csv");
if (m_csvFileDialog.exec())
{
QStringList fileNames = m_csvFileDialog.selectedFiles();
if (fileNames.size() > 0)
{
QFile file(fileNames[0]);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QMessageBox::critical(this, "Heat Map", QString("Failed to open file %1").arg(fileNames[0]));
return;
}
QTextStream out(&file);
out.setRealNumberPrecision(9); // https://bugreports.qt.io/browse/QTBUG-110775 - set to 9, so we get at least 6 fractional digits
out << "Latitude,Longitude," << ui->mode->currentText() << " Power (dB)\n";
float *power = getCurrentModePowerData();
for (int y = 0; y < m_height; y++)
{
for (int x = 0; x < m_width; x++)
{
float pow = power[y*m_width+x];
if (!std::isnan(pow))
{
double latitude, longitude;
pixelToCoords(x, y, latitude, longitude);
out << latitude << "," << longitude << "," << pow << "\n";
}
}
}
}
}
}
void HeatMapGUI::on_readCSV_clicked()
{
m_csvFileDialog.setAcceptMode(QFileDialog::AcceptOpen);
m_csvFileDialog.setNameFilter("*.csv");
if (m_csvFileDialog.exec())
{
QStringList fileNames = m_csvFileDialog.selectedFiles();
if (fileNames.size() > 0)
{
QFile file(fileNames[0]);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QMessageBox::critical(this, "Heat Map", QString("Failed to open file %1").arg(fileNames[0]));
return;
}
QTextStream in(&file);
QString powerColName = ui->mode->currentText() + " Power (dB)";
QString error;
QStringList colNames = {"Latitude", "Longitude", powerColName};
QHash<QString, int> colIndexes = CSV::readHeader(in, colNames, error);
if (error.isEmpty())
{
clearPower(getCurrentModePowerData());
clearImage();
int latitudeCol = colIndexes.value("Latitude");
int longitudeCol = colIndexes.value("Longitude");
int powerCol = colIndexes.value(powerColName);
QStringList cols;
while (CSV::readRow(in, &cols))
{
if (cols.size() >= 3)
{
double latitude = cols[latitudeCol].toDouble();
double longitude = cols[longitudeCol].toDouble();
float power = cols[powerCol].toFloat();
updatePower(latitude, longitude, power);
}
}
}
else
{
QString actualColNames = colIndexes.keys().join(" ");
QString expectedColNames = colNames.join(" ");
QMessageBox::critical(this, "Heat Map", QString("Failed to read expected header in CSV file. %1 != %2").arg(actualColNames).arg(expectedColNames));
return;
}
}
}
}
void HeatMapGUI::on_writeImage_clicked()
{
m_imageFileDialog.setAcceptMode(QFileDialog::AcceptSave);
m_imageFileDialog.setNameFilter("*.png *.jpg *.jpeg *.bmp *.ppm *.xbm *.xpm");
if (m_imageFileDialog.exec())
{
QStringList fileNames = m_imageFileDialog.selectedFiles();
if (fileNames.size() > 0)
{
if (!m_image.save(fileNames[0]))
{
QMessageBox::critical(this, "Heat Map", QString("Failed to save image to %1").arg(fileNames[0]));
return;
}
}
}
}
void HeatMapGUI::displayTXPosition(bool enabled)
{
ui->txLatitudeLabel->setEnabled(enabled);
ui->txLatitude->setEnabled(enabled);
ui->txLongitude->setEnabled(enabled);
ui->txLongitudeLabel->setEnabled(enabled);
ui->txPower->setEnabled(enabled);
ui->txPowerLabel->setEnabled(enabled);
ui->txPowerUnits->setEnabled(enabled);
ui->txPositionSet->setEnabled(enabled);
ui->rangeLabel->setEnabled(enabled);
ui->range->setEnabled(enabled);
ui->rangeUnits->setEnabled(enabled);
ui->pathLossLabel->setEnabled(enabled);
ui->pathLoss->setEnabled(enabled);
ui->pathLossUnits->setEnabled(enabled);
if (enabled) {
sendTxToMap();
} else {
deleteTxFromMap();
}
}
void HeatMapGUI::on_txPosition_clicked(bool checked)
{
m_settings.m_txPosValid = checked;
displayTXPosition(checked);
applySettings();
}
void HeatMapGUI::on_txLatitude_editingFinished()
{
m_settings.m_txLatitude = ui->txLatitude->text().toFloat();
updateRange();
sendTxToMap();
applySettings();
}
void HeatMapGUI::on_txLongitude_editingFinished()
{
m_settings.m_txLongitude = ui->txLongitude->text().toFloat();
updateRange();
sendTxToMap();
applySettings();
}
void HeatMapGUI::on_txPower_valueChanged(double value)
{
m_settings.m_txPower = (float)value;
sendTxToMap();
applySettings();
}
void HeatMapGUI::on_txPositionSet_clicked(bool checked)
{
(void) checked;
ui->txLatitude->setText(QString::number(m_latitude));
ui->txLongitude->setText(QString::number(m_longitude));
m_settings.m_txLatitude = m_latitude;
m_settings.m_txLongitude = m_longitude;
updateRange();
sendTxToMap();
applySettings();
}
void HeatMapGUI::onWidgetRolled(QWidget* widget, bool rollDown)
{
(void) widget;
(void) rollDown;
getRollupContents()->saveState(m_rollupState);
applySettings();
}
void HeatMapGUI::onMenuDialogCalled(const QPoint &p)
{
if (m_contextMenuType == ContextMenuType::ContextMenuChannelSettings)
{
BasicChannelSettingsDialog dialog(&m_channelMarker, this);
dialog.setUseReverseAPI(m_settings.m_useReverseAPI);
dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress);
dialog.setReverseAPIPort(m_settings.m_reverseAPIPort);
dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex);
dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex);
dialog.setDefaultTitle(m_displayedName);
if (m_deviceUISet->m_deviceMIMOEngine)
{
dialog.setNumberOfStreams(m_heatMap->getNumberOfDeviceStreams());
dialog.setStreamIndex(m_settings.m_streamIndex);
}
dialog.move(p);
new DialogPositioner(&dialog, true);
dialog.exec();
m_settings.m_rgbColor = m_channelMarker.getColor().rgb();
m_settings.m_title = m_channelMarker.getTitle();
m_settings.m_useReverseAPI = dialog.useReverseAPI();
m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress();
m_settings.m_reverseAPIPort = dialog.getReverseAPIPort();
m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex();
m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex();
setWindowTitle(m_settings.m_title);
setTitle(m_channelMarker.getTitle());
setTitleColor(m_settings.m_rgbColor);
if (m_deviceUISet->m_deviceMIMOEngine)
{
m_settings.m_streamIndex = dialog.getSelectedStreamIndex();
m_channelMarker.clearStreamIndexes();
m_channelMarker.addStreamIndex(m_settings.m_streamIndex);
updateIndexLabel();
}
applySettings();
}
resetContextMenuType();
}
HeatMapGUI::HeatMapGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) :
ChannelGUI(parent),
ui(new Ui::HeatMapGUI),
m_pluginAPI(pluginAPI),
m_deviceUISet(deviceUISet),
m_channelMarker(this),
m_deviceCenterFrequency(0),
m_basebandSampleRate(1),
m_doApplySettings(true),
m_tickCount(0),
m_powerChart(nullptr),
m_powerAverageSeries(nullptr),
m_powerMaxPeakSeries(nullptr),
m_powerMinPeakSeries(nullptr),
m_powerPulseAverageSeries(nullptr),
m_powerPathLossSeries(nullptr)
{
setAttribute(Qt::WA_DeleteOnClose, true);
m_helpURL = "plugins/channelrx/heatmap/readme.md";
RollupContents *rollupContents = getRollupContents();
ui->setupUi(rollupContents);
setSizePolicy(rollupContents->sizePolicy());
rollupContents->arrangeRollups();
connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));
connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &)));
m_heatMap = reinterpret_cast<HeatMap*>(rxChannel);
m_heatMap->setMessageQueueToGUI(getInputMessageQueue());
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
// Disable 256MB limit on image size
QImageReader::setAllocationLimit(0);
#endif
connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms
m_scopeVis = m_heatMap->getScopeSink();
m_scopeVis->setGLScope(ui->glScope);
ui->glScope->connectTimer(MainCore::instance()->getMasterTimer());
ui->scopeGUI->setBuddies(m_scopeVis->getInputMessageQueue(), m_scopeVis, ui->glScope);
// Scope settings to display magdB
ui->scopeGUI->setPreTrigger(1);
GLScopeSettings::TraceData traceDataI, traceDataQ;
traceDataI.m_projectionType = Projector::ProjectionMagDB;
traceDataI.m_amp = 1.0; // for 0 -100 dB
traceDataI.m_ofs = 0.0;
ui->scopeGUI->changeTrace(0, traceDataI);
ui->scopeGUI->setDisplayMode(GLScopeSettings::DisplayX);
ui->scopeGUI->focusOnTrace(0); // re-focus to take changes into account in the GUI
GLScopeSettings::TriggerData triggerData;
triggerData.m_triggerLevel = 0.1;
triggerData.m_triggerLevelCoarse = 10;
triggerData.m_triggerPositiveEdge = true;
ui->scopeGUI->changeTrigger(0, triggerData);
ui->scopeGUI->focusOnTrigger(0); // re-focus to take changes into account in the GUI
m_scopeVis->setLiveRate(m_settings.m_sampleRate);
ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03)));
ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold));
ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999);
ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue);
m_channelMarker.blockSignals(true);
m_channelMarker.setColor(Qt::yellow);
m_channelMarker.setBandwidth(m_settings.m_rfBandwidth);
m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset);
m_channelMarker.setTitle("Heat Map");
m_channelMarker.blockSignals(false);
m_channelMarker.setVisible(true); // activate signal on the last setting only
setTitleColor(m_channelMarker.getColor());
m_settings.setChannelMarker(&m_channelMarker);
m_settings.setScopeGUI(ui->scopeGUI);
m_settings.setRollupState(&m_rollupState);
m_deviceUISet->addChannelMarker(&m_channelMarker);
connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor()));
connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor()));
connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
m_latitude = MainCore::instance()->getSettings().getLatitude();
m_longitude = MainCore::instance()->getSettings().getLongitude();
m_altitude = MainCore::instance()->getSettings().getAltitude();
ui->latitude->setText(QString::number(m_latitude));
ui->longitude->setText(QString::number(m_longitude));
QStringList colorMapNames = ColorMap::getColorMapNames();
for (const auto& color : colorMapNames) {
ui->colorMap->addItem(color);
}
m_colorMap = ColorMap::getColorMap(m_settings.m_colorMapName);
m_pen.setColor(Qt::black);
m_painter.setPen(m_pen);
createMap();
// Get updated when position changes
connect(&MainCore::instance()->getSettings(), &MainSettings::preferenceChanged, this, &HeatMapGUI::preferenceChanged);
ui->scopeContainer->setVisible(false);
displaySettings();
makeUIConnections();
applySettings(true);
DialPopup::addPopupsToChildDials(this);
m_resizer.enableChildMouseTracking();
plotPowerVsTimeChart();
}
HeatMapGUI::~HeatMapGUI()
{
disconnect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick()));
deleteFromMap();
deleteTxFromMap();
deleteMap();
delete ui;
}
void HeatMapGUI::blockApplySettings(bool block)
{
m_doApplySettings = !block;
}
void HeatMapGUI::applySettings(bool force)
{
if (m_doApplySettings)
{
HeatMap::MsgConfigureHeatMap* message = HeatMap::MsgConfigureHeatMap::create(m_settings, force);
m_heatMap->getInputMessageQueue()->push(message);
}
}
void HeatMapGUI::displaySettings()
{
m_channelMarker.blockSignals(true);
m_channelMarker.setBandwidth(m_settings.m_rfBandwidth);
m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset);
m_channelMarker.setTitle(m_settings.m_title);
m_channelMarker.blockSignals(false);
m_channelMarker.setColor(m_settings.m_rgbColor); // activate signal on the last setting only
setTitleColor(m_settings.m_rgbColor);
setWindowTitle(m_channelMarker.getTitle());
setTitle(m_channelMarker.getTitle());
blockApplySettings(true);
ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency());
ui->rfBWText->setText(QString("%1k").arg(m_settings.m_rfBandwidth / 1000.0, 0, 'f', 1));
ui->rfBW->setValue(m_settings.m_rfBandwidth / 100.0);
ui->minPower->setValue(m_settings.m_minPower);
ui->maxPower->setValue(m_settings.m_maxPower);
ui->colorMap->setCurrentText(m_settings.m_colorMapName);
m_colorMap = ColorMap::getColorMap(m_settings.m_colorMapName);
ui->mode->setCurrentIndex((int)m_settings.m_mode);
ui->pulseTH->setValue(m_settings.m_pulseThreshold);
ui->pulseTHText->setText(QString::number((int)m_settings.m_pulseThreshold));
int value = (int)std::log10(m_settings.m_averagePeriodUS);
ui->averagePeriod->setValue(value);
ui->averagePeriodText->setText(m_averagePeriodTexts[value-1]);
value = (int)std::log10(m_settings.m_sampleRate);
ui->sampleRate->setValue(value);
int idx = std::min(std::max(0, value-2), (int)m_sampleRateTexts.size() - 1);
ui->sampleRateText->setText(m_sampleRateTexts[idx]);
ui->averagePeriod->setMinimum(std::max(1, static_cast<int> (m_averagePeriodTexts.size()) - value));
ui->txPosition->setChecked(m_settings.m_txPosValid);
displayTXPosition(m_settings.m_txPosValid);
ui->txLatitude->setText(QString::number(m_settings.m_txLatitude));
ui->txLongitude->setText(QString::number(m_settings.m_txLongitude));
ui->txPower->setValue(m_settings.m_txPower);
ui->displayChart->setChecked(m_settings.m_displayChart);
displayPowerChart();
ui->displayAverage->setChecked(m_settings.m_displayAverage);
ui->displayMax->setChecked(m_settings.m_displayMax);
ui->displayMin->setChecked(m_settings.m_displayMin);
ui->displayPulseAverage->setChecked(m_settings.m_displayPulseAverage);
ui->displayPathLoss->setChecked(m_settings.m_displayPathLoss);
ui->displayMins->setValue(m_settings.m_displayMins);
ui->recordAverage->setChecked(m_settings.m_recordAverage);
ui->recordMax->setChecked(m_settings.m_recordMax);
ui->recordMin->setChecked(m_settings.m_recordMin);
ui->recordPulseAverage->setChecked(m_settings.m_recordPulseAverage);
ui->recordPathLoss->setChecked(m_settings.m_recordPathLoss);
m_scopeVis->setLiveRate(m_settings.m_sampleRate);
updateIndexLabel();
updateRange();
getRollupContents()->restoreState(m_rollupState);
updateAbsoluteCenterFrequency();
blockApplySettings(false);
}
void HeatMapGUI::leaveEvent(QEvent* event)
{
m_channelMarker.setHighlighted(false);
ChannelGUI::leaveEvent(event);
}
void HeatMapGUI::enterEvent(EnterEventType* event)
{
m_channelMarker.setHighlighted(true);
ChannelGUI::enterEvent(event);
}
void HeatMapGUI::tick()
{
// Update power meter widget
double magsqAvg, magsqpeak;
int nbMagsqSamples;
m_heatMap->getMagSqLevels(magsqAvg, magsqpeak, nbMagsqSamples);
double powDbAvg = CalcDb::dbPower(magsqAvg);
double powDbMaxPeak = CalcDb::dbPower(magsqpeak);
ui->channelPowerMeter->levelChanged(
(100.0f + powDbAvg) / 100.0f,
(100.0f + powDbMaxPeak) / 100.0f,
nbMagsqSamples);
if (m_tickCount % 4 == 0) {
ui->channelPower->setText(QString::number(powDbAvg, 'f', 1));
}
// Get power measurements and plot on map
if ((m_y >= 0) && (m_x >= 0) && (m_y < m_height) && (m_x < m_width))
{
double magAvg, magPulseAvg, magMaxPeak, magMinPeak;
m_heatMap->getMagLevels(magAvg, magPulseAvg, magMaxPeak, magMinPeak);
double powDbPulseAvg, powDbMinPeak, powDbPathLoss;
powDbAvg = std::numeric_limits<double>::quiet_NaN();
powDbPulseAvg = std::numeric_limits<double>::quiet_NaN();
powDbMaxPeak = std::numeric_limits<double>::quiet_NaN();
powDbMinPeak = std::numeric_limits<double>::quiet_NaN();
powDbPathLoss = std::numeric_limits<double>::quiet_NaN();
int idx = m_y * m_width + m_x;
if (!std::isnan(magAvg))
{
powDbAvg = CalcDb::dbPower(magAvg * magAvg);
if (m_powerAverage) {
m_powerAverage[idx] = powDbAvg;
}
if (m_tickCount % 4 == 0) {
ui->average->setText(QString::number(powDbAvg, 'f', 1));
}
}
else
{
ui->average->setText("");
}
if (!std::isnan(magPulseAvg))
{
powDbPulseAvg = CalcDb::dbPower(magPulseAvg * magPulseAvg);
if (m_powerPulseAverage) {
m_powerPulseAverage[idx] = powDbPulseAvg;
}
if (m_tickCount % 4 == 0) {
ui->pulseAverage->setText(QString::number(powDbPulseAvg, 'f', 1));
}
}
else
{
ui->pulseAverage->setText("");
}
if (magMaxPeak != -std::numeric_limits<double>::max())
{
powDbMaxPeak = CalcDb::dbPower(magMaxPeak * magMaxPeak);
if (m_powerMaxPeak) {
m_powerMaxPeak[idx] = powDbMaxPeak;
}
if (m_tickCount % 4 == 0) {
ui->maxPeak->setText(QString::number(powDbMaxPeak, 'f', 1));
}
}
else
{
ui->maxPeak->setText("");
}
if (magMinPeak != std::numeric_limits<double>::max())
{
powDbMinPeak = CalcDb::dbPower(magMinPeak * magMinPeak);
if (m_powerMinPeak) {
m_powerMinPeak[idx] = powDbMinPeak;
}
if (m_tickCount % 4 == 0) {
ui->minPeak->setText(QString::number(powDbMinPeak, 'f', 1));
}
}
else
{
ui->minPeak->setText("");
}
double range = calcRange(m_latitude, m_longitude);
double frequency = m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset;
powDbPathLoss = m_settings.m_txPower - calcFreeSpacePathLoss(range, frequency);
if (m_powerPathLoss) {
m_powerPathLoss[idx] = powDbPathLoss;
}
if (m_heatMap->getDeviceAPI()->state(0) == DeviceAPI::StRunning)
{
addToPowerSeries(QDateTime::currentDateTime(), powDbAvg, powDbPulseAvg, powDbMaxPeak, powDbMinPeak, powDbPathLoss);
if (m_settings.m_mode != HeatMapSettings::None)
{
// Plot newest measurement on map
float *power = getCurrentModePowerData();
if (power)
{
double powDb = power[idx];
if (!std::isnan(powDb)) {
plotPixel(m_x, m_y, powDb);
}
}
}
if (m_tickCount % 15 == 0)
{
trimPowerSeries(QDateTime::currentDateTime().addSecs(-10*60));
// Updating axis range causes chart to be redrawn, so don't call for every sample
updateAxis();
}
}
}
if (m_tickCount % 25 == 0) {
sendToMap();
}
m_tickCount++;
}
float *HeatMapGUI::getCurrentModePowerData()
{
switch (m_settings.m_mode)
{
case HeatMapSettings::None:
case HeatMapSettings::Average:
return m_powerAverage;
case HeatMapSettings::PulseAverage:
return m_powerPulseAverage;
case HeatMapSettings::Max:
return m_powerMaxPeak;
case HeatMapSettings::Min:
return m_powerMinPeak;
case HeatMapSettings::PathLoss:
return m_powerPathLoss;
default:
return nullptr;
}
}
void HeatMapGUI::updatePower(double latitude, double longitude, float power)
{
int x, y;
coordsToPixel(latitude, longitude, x, y);
if (!pixelValid(x, y))
{
resizeMap(x, y);
coordsToPixel(latitude, longitude, x, y);
}
float *powerArray = getCurrentModePowerData();
powerArray[y*m_width+x] = power;
plotPixel(x, y, power);
}
void HeatMapGUI::plotMap()
{
if ((m_settings.m_mode != HeatMapSettings::None) && !m_image.isNull())
{
clearImage();
float *data = getCurrentModePowerData();
if (data) {
plotMap(data);
}
sendToMap();
}
}
void HeatMapGUI::clearPower()
{
clearPower(m_powerAverage);
clearPower(m_powerPulseAverage);
clearPower(m_powerMaxPeak);
clearPower(m_powerMinPeak);
clearPower(m_powerPathLoss);
}
void HeatMapGUI::clearPower(float *power)
{
clearPower(power, m_width * m_height);
}
void HeatMapGUI::clearPower(float *power, int size)
{
if (power) {
std::fill_n(power, size, std::numeric_limits<float>::quiet_NaN());
}
}
void HeatMapGUI::createImage(int width, int height)
{
if (!m_image.isNull()) {
m_painter.end();
}
try
{
if (m_settings.m_mode != HeatMapSettings::None)
{
qDebug() << "HeatMapGUI::createImage" << width << "*" << height;
m_image = QImage(width, height, QImage::Format_ARGB32);
m_painter.begin(&m_image);
}
else
{
m_image = QImage();
}
}
catch (std::bad_alloc&)
{
m_image = QImage();
QMessageBox::critical(this, "Heat Map", QString("Failed to allocate memory (width=%1 height=%2)").arg(m_width).arg(m_height));
}
}
void HeatMapGUI::clearImage()
{
m_image.fill(Qt::transparent);
//m_pen.setColor(Qt::black);
//m_painter.drawEllipse(QPoint(m_width/2, m_height/2), m_width/2-1, m_height/2-1);
}
void HeatMapGUI::plotMap(float *power)
{
for (int y = 0; y < m_height; y++)
{
for (int x = 0; x < m_width; x++)
{
float pow = power[y * m_width + x];
if (!std::isnan(pow)) {
plotPixel(x, y, pow);
}
}
}
}
void HeatMapGUI::plotPixel(int x, int y, double power)
{
if (m_image.isNull()) {
return;
}
// Normalise to [0,1]
float powNorm = (power - m_settings.m_minPower) / (m_settings.m_maxPower - m_settings.m_minPower);
if (powNorm < 0) {
return; // Don't plot below min, so we can easily see where we're below the min limit
}
powNorm = std::max(0.0f, powNorm);
powNorm = std::min(1.0f, powNorm);
int index = std::round(powNorm * 255);
QColor color = QColor::fromRgbF(m_colorMap[index*3], m_colorMap[index*3+1], m_colorMap[index*3+2]);
m_pen.setColor(color);
m_painter.setPen(m_pen);
m_painter.drawPoint(QPoint(x, y));
}
void HeatMapGUI::updateRange()
{
if (m_settings.m_txPosValid)
{
// Calculate range
qreal range = calcRange(m_latitude, m_longitude);
if (range < 1000)
{
ui->range->setText(QString::number(std::round(range)));
ui->rangeUnits->setText("m");
}
else
{
ui->range->setText(QString::number(range / 1000.0, 'f', 1));
ui->rangeUnits->setText("km");
}
double frequency = m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset;
double loss = calcFreeSpacePathLoss(range, frequency);
ui->pathLoss->setText(QString::number(loss, 'f', 1));
}
else
{
ui->range->setText("");
ui->pathLoss->setText("");
}
}
qreal HeatMapGUI::calcRange(double latitude, double longitude)
{
QGeoCoordinate pos(latitude, longitude);
QGeoCoordinate tx(m_settings.m_txLatitude, m_settings.m_txLongitude);
return tx.distanceTo(pos);
}
double HeatMapGUI::calcFreeSpacePathLoss(double range, double frequency)
{
// In dB
if ((range == 0.0) || (frequency == 0.0)) {
return 0.0;
} else {
return 20.0 * log10(range) + 20 * log10(frequency) + 20 * log10(4.0 * M_PI / Astronomy::m_speedOfLight);
}
}
void HeatMapGUI::makeUIConnections()
{
QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &HeatMapGUI::on_deltaFrequency_changed);
QObject::connect(ui->rfBW, &QSlider::valueChanged, this, &HeatMapGUI::on_rfBW_valueChanged);
QObject::connect(ui->minPower, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &HeatMapGUI::on_minPower_valueChanged);
QObject::connect(ui->maxPower, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &HeatMapGUI::on_maxPower_valueChanged);
QObject::connect(ui->colorMap, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &HeatMapGUI::on_colorMap_currentIndexChanged);
QObject::connect(ui->pulseTH, QOverload<int>::of(&QDial::valueChanged), this, &HeatMapGUI::on_pulseTH_valueChanged);
QObject::connect(ui->averagePeriod, QOverload<int>::of(&QDial::valueChanged), this, &HeatMapGUI::on_averagePeriod_valueChanged);
QObject::connect(ui->sampleRate, QOverload<int>::of(&QDial::valueChanged), this, &HeatMapGUI::on_sampleRate_valueChanged);
QObject::connect(ui->mode, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &HeatMapGUI::on_mode_currentIndexChanged);
QObject::connect(ui->displayChart, &QPushButton::clicked, this, &HeatMapGUI::on_displayChart_clicked);
QObject::connect(ui->clearHeatMap, &QPushButton::clicked, this, &HeatMapGUI::on_clearHeatMap_clicked);
QObject::connect(ui->writeCSV, &QPushButton::clicked, this, &HeatMapGUI::on_writeCSV_clicked);
QObject::connect(ui->readCSV, &QPushButton::clicked, this, &HeatMapGUI::on_readCSV_clicked);
QObject::connect(ui->writeImage, &QPushButton::clicked, this, &HeatMapGUI::on_writeImage_clicked);
QObject::connect(ui->txPosition, &QPushButton::clicked, this, &HeatMapGUI::on_txPosition_clicked);
QObject::connect(ui->txLatitude, &QLineEdit::editingFinished, this, &HeatMapGUI::on_txLatitude_editingFinished);
QObject::connect(ui->txLongitude, &QLineEdit::editingFinished, this, &HeatMapGUI::on_txLongitude_editingFinished);
QObject::connect(ui->txPower, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &HeatMapGUI::on_txPower_valueChanged);
QObject::connect(ui->txPositionSet, &QPushButton::clicked, this, &HeatMapGUI::on_txPositionSet_clicked);
QObject::connect(ui->displayAverage, &QCheckBox::clicked, this, &HeatMapGUI::on_displayAverage_clicked);
QObject::connect(ui->displayMax, &QCheckBox::clicked, this, &HeatMapGUI::on_displayMax_clicked);
QObject::connect(ui->displayMin, &QCheckBox::clicked, this, &HeatMapGUI::on_displayMin_clicked);
QObject::connect(ui->displayPulseAverage, &QCheckBox::clicked, this, &HeatMapGUI::on_displayPulseAverage_clicked);
QObject::connect(ui->displayPathLoss, &QCheckBox::clicked, this, &HeatMapGUI::on_displayPathLoss_clicked);
QObject::connect(ui->displayMins, QOverload<int>::of(&QSpinBox::valueChanged), this, &HeatMapGUI::on_displayMins_valueChanged);
QObject::connect(ui->recordAverage, &QCheckBox::clicked, this, &HeatMapGUI::on_recordAverage_clicked);
QObject::connect(ui->recordMax, &QCheckBox::clicked, this, &HeatMapGUI::on_recordMax_clicked);
QObject::connect(ui->recordMin, &QCheckBox::clicked, this, &HeatMapGUI::on_recordMin_clicked);
QObject::connect(ui->recordPulseAverage, &QCheckBox::clicked, this, &HeatMapGUI::on_recordPulseAverage_clicked);
QObject::connect(ui->recordPathLoss, &QCheckBox::clicked, this, &HeatMapGUI::on_recordPathLoss_clicked);
}
void HeatMapGUI::updateAbsoluteCenterFrequency()
{
setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset);
}
void HeatMapGUI::sendToMap()
{
if (m_settings.m_mode != HeatMapSettings::None)
{
// Send to Map feature
QList<ObjectPipe*> mapPipes;
MainCore::instance()->getMessagePipes().getMessagePipes(m_heatMap, "mapitems", mapPipes);
if (mapPipes.size() > 0)
{
// Encode image as base64 PNG
QByteArray ba;
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
m_image.save(&buffer, "PNG");
QByteArray data = ba.toBase64();
for (const auto& pipe : mapPipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
swgMapItem->setName(new QString("Heat Map"));
swgMapItem->setImage(new QString(data));
swgMapItem->setAltitude(0);
swgMapItem->setType(1);
swgMapItem->setImageTileEast(m_east);
swgMapItem->setImageTileWest(m_west);
swgMapItem->setImageTileNorth(m_north);
swgMapItem->setImageTileSouth(m_south);
swgMapItem->setImageZoomLevel((float)m_zoomLevel);
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_heatMap, swgMapItem);
messageQueue->push(msg);
}
}
}
}
void HeatMapGUI::deleteFromMap()
{
QList<ObjectPipe*> mapPipes;
MainCore::instance()->getMessagePipes().getMessagePipes(m_heatMap, "mapitems", mapPipes);
for (const auto& pipe : mapPipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
swgMapItem->setName(new QString("Heat Map"));
swgMapItem->setImage(new QString()); // Set image to "" to delete it
swgMapItem->setType(1);
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_heatMap, swgMapItem);
messageQueue->push(msg);
}
}
void HeatMapGUI::sendTxToMap()
{
// Send to Map feature
QList<ObjectPipe*> mapPipes;
MainCore::instance()->getMessagePipes().getMessagePipes(m_heatMap, "mapitems", mapPipes);
if (mapPipes.size() > 0)
{
QString text = QString("Heat Map Transmitter\nPower: %1 dB").arg(m_settings.m_txPower);
for (const auto& pipe : mapPipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
swgMapItem->setName(new QString("TX"));
swgMapItem->setLatitude(m_settings.m_txLatitude);
swgMapItem->setLongitude(m_settings.m_txLongitude);
swgMapItem->setAltitude(0);
swgMapItem->setImage(new QString("antenna.png"));
swgMapItem->setText(new QString(text));
swgMapItem->setModel(new QString("antenna.glb"));
swgMapItem->setFixedPosition(true);
swgMapItem->setLabel(new QString("TX"));
swgMapItem->setLabelAltitudeOffset(4.5);
swgMapItem->setAltitudeReference(1);
swgMapItem->setType(0);
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_heatMap, swgMapItem);
messageQueue->push(msg);
}
}
}
void HeatMapGUI::deleteTxFromMap()
{
QList<ObjectPipe*> mapPipes;
MainCore::instance()->getMessagePipes().getMessagePipes(m_heatMap, "mapitems", mapPipes);
for (const auto& pipe : mapPipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
swgMapItem->setName(new QString("TX"));
swgMapItem->setImage(new QString()); // Set image to "" to delete it
swgMapItem->setType(0);
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_heatMap, swgMapItem);
messageQueue->push(msg);
}
}
void HeatMapGUI::preferenceChanged(int elementType)
{
Preferences::ElementType pref = (Preferences::ElementType)elementType;
if ((pref == Preferences::Latitude) || (pref == Preferences::Longitude) || (pref == Preferences::Altitude))
{
// Get new position
m_latitude = MainCore::instance()->getSettings().getLatitude();
m_longitude = MainCore::instance()->getSettings().getLongitude();
m_altitude = MainCore::instance()->getSettings().getAltitude();
// Display new position in GUI
ui->latitude->setText(QString::number(m_latitude));
ui->longitude->setText(QString::number(m_longitude));
updateRange();
// Map position to pixel
int x, y;
coordsToPixel(m_latitude, m_longitude, x, y);
if ((x != m_x) || (y != m_y))
{
m_x = x;
m_y = y;
m_heatMap->resetMagLevels();
if (!pixelValid(x, y)) {
resizeMap(x, y);
}
}
}
}
void HeatMapGUI::createMap()
{
// https://wiki.openstreetmap.org/wiki/Zoom_levels
double earthCircumference = 40075016.686;
double scale = cos(Units::degreesToRadians(m_latitude));
m_resolution = earthCircumference * scale / pow(2.0, m_zoomLevel+8.0); // metres per pixel
m_width = m_blockSize;
m_height = m_blockSize;
m_degreesLonPerPixel = m_resolution / scale / (earthCircumference / 360.0);
m_degreesLatPerPixel = m_resolution / (earthCircumference / 360.0);
int size = m_width * m_height;
try
{
if (m_settings.m_recordAverage) {
m_powerAverage = new float[size];
} else {
m_powerAverage = nullptr;
}
if (m_settings.m_recordPulseAverage) {
m_powerPulseAverage = new float[size];
} else {
m_powerPulseAverage = nullptr;
}
if (m_settings.m_recordMax) {
m_powerMaxPeak = new float[size];
} else {
m_powerMaxPeak = nullptr;
}
if (m_settings.m_recordMin) {
m_powerMinPeak = new float[size];
} else {
m_powerMinPeak = nullptr;
}
if (m_settings.m_recordPathLoss) {
m_powerPathLoss = new float[size];
} else {
m_powerPathLoss = nullptr;
}
m_north = m_latitude + m_degreesLatPerPixel * m_height / 2;
m_south = m_latitude - m_degreesLatPerPixel * m_height / 2;
m_east = m_longitude + m_degreesLonPerPixel * m_width / 2;
m_west = m_longitude - m_degreesLonPerPixel * m_width / 2;
m_x = m_width / 2;
m_y = m_height / 2;
createImage(m_width, m_height);
}
catch (std::bad_alloc&)
{
deleteMap();
QMessageBox::critical(this, "Heat Map", QString("Failed to allocate memory (width=%1 height=%2)").arg(m_width).arg(m_height));
}
on_clearHeatMap_clicked();
}
void HeatMapGUI::deleteMap()
{
deleteFromMap();
delete[] m_powerAverage;
m_powerAverage = nullptr;
delete[] m_powerPulseAverage;
m_powerPulseAverage = nullptr;
delete[] m_powerMaxPeak;
m_powerMaxPeak = nullptr;
delete[] m_powerMinPeak;
m_powerMinPeak = nullptr;
delete[] m_powerPathLoss;
m_powerPathLoss = nullptr;
if (!m_image.isNull()) {
m_painter.end();
}
}
void HeatMapGUI::resizeMap(int x, int y)
{
if ((x <= -m_blockSize) || (x >= m_width+m_blockSize) || (y <= -m_blockSize) || (y >= m_height+m_blockSize))
{
// Position has moved a long way - restart from scratch to avoid map being too big
qDebug() << "HeatMapGUI::resizeMap: Position has moved significantly. Recreating map";
deleteMap();
createMap();
}
else
{
// Expand map
int newWidth = m_width;
int newHeight = m_height;
int xOffset = 0;
int yOffset = 0;
if (x < 0)
{
newWidth += m_blockSize;
xOffset = m_blockSize;
m_west -= m_blockSize * m_degreesLonPerPixel;
}
if (x >= m_width)
{
newWidth += m_blockSize;
m_east += m_blockSize * m_degreesLonPerPixel;
}
if (y < 0)
{
newHeight += m_blockSize;
yOffset = m_blockSize * newWidth;
m_north += m_blockSize * m_degreesLatPerPixel;
}
if (y >= m_height)
{
newHeight += m_blockSize;
m_south -= m_blockSize * m_degreesLatPerPixel;
}
float *powerAverage = nullptr;
float *powerPulseAverage = nullptr;
float *powerMaxPeak = nullptr;
float *powerMinPeak = nullptr;
float *powerPathLoss = nullptr;
int newSize = newWidth * newHeight;
qDebug() << "HeatMapGUI::resizeMap:" << m_width << "*" << m_height << "to" << newWidth << "*" << newHeight;
try
{
// Allocate new memory
if (m_settings.m_recordAverage) {
powerAverage = new float[newSize];
}
if (m_settings.m_recordPulseAverage) {
powerPulseAverage = new float[newSize];
}
if (m_settings.m_recordMax) {
powerMaxPeak = new float[newSize];
}
if (m_settings.m_recordMin) {
powerMinPeak = new float[newSize];
}
if (m_settings.m_recordPathLoss) {
powerPathLoss = new float[newSize];
}
clearPower(powerAverage, newSize);
clearPower(powerPulseAverage, newSize);
clearPower(powerMaxPeak, newSize);
clearPower(powerMinPeak, newSize);
clearPower(powerPathLoss, newSize);
// Copy across old data
for (int j = 0; j < m_height; j++)
{
int srcStart = j * m_width;
int srcEnd = (j + 1) * m_width;
int destStart = j * newWidth + yOffset + xOffset;
//qDebug() << srcStart << srcEnd << destStart;
if (powerAverage && m_powerAverage) {
std::copy(m_powerAverage + srcStart, m_powerAverage + srcEnd, powerAverage + destStart);
}
if (powerPulseAverage && m_powerPulseAverage) {
std::copy(m_powerPulseAverage + srcStart, m_powerPulseAverage + srcEnd, powerPulseAverage + destStart);
}
if (powerMaxPeak && m_powerMaxPeak) {
std::copy(m_powerMaxPeak + srcStart, m_powerMaxPeak + srcEnd, powerMaxPeak + destStart);
}
if (powerMinPeak && m_powerMinPeak) {
std::copy(m_powerMinPeak + srcStart, m_powerMinPeak + srcEnd, powerMinPeak + destStart);
}
if (powerPathLoss && m_powerPathLoss) {
std::copy(m_powerPathLoss + srcStart, m_powerPathLoss + srcEnd, powerPathLoss + destStart);
}
}
createImage(newWidth, newHeight);
m_width = newWidth;
m_height = newHeight;
// Delete old memory
delete[] m_powerAverage;
delete[] m_powerPulseAverage;
delete[] m_powerMaxPeak;
delete[] m_powerMinPeak;
m_powerAverage = powerAverage;
m_powerPulseAverage = powerPulseAverage;
m_powerMaxPeak = powerMaxPeak;
m_powerMinPeak = powerMinPeak;
m_powerPathLoss = powerPathLoss;
plotMap();
}
catch (std::bad_alloc&)
{
// Delete partially allocated memory
delete[] powerAverage;
delete[] powerPulseAverage;
delete[] powerMaxPeak;
delete[] powerMinPeak;
delete[] powerPathLoss;
QMessageBox::critical(this, "Heat Map", QString("Failed to allocate memory (width=%1 height=%2)").arg(newWidth).arg(newHeight));
}
}
}
void HeatMapGUI::plotPowerVsTimeChart()
{
QChart *oldChart = m_powerChart;
m_powerChart = new QChart();
m_powerChart->layout()->setContentsMargins(0, 0, 0, 0);
m_powerChart->setMargins(QMargins(1, 1, 1, 1));
m_powerChart->setTheme(QChart::ChartThemeDark);
m_powerChart->legend()->setAlignment(Qt::AlignBottom);
m_powerChart->legend()->setVisible(true);
// Create measurement data series
m_powerAverageSeries = new QLineSeries();
m_powerAverageSeries->setVisible(m_settings.m_displayAverage);
m_powerAverageSeries->setName("Average");
m_powerMaxPeakSeries = new QLineSeries();
m_powerMaxPeakSeries->setVisible(m_settings.m_displayMax);
m_powerMaxPeakSeries->setName("Max");
m_powerMinPeakSeries = new QLineSeries();
m_powerMinPeakSeries->setVisible(m_settings.m_displayMin);
m_powerMinPeakSeries->setName("Min");
m_powerPulseAverageSeries = new QLineSeries();
m_powerPulseAverageSeries->setVisible(m_settings.m_displayPulseAverage);
m_powerPulseAverageSeries->setName("Pulse Average");
m_powerPathLossSeries = new QLineSeries();
m_powerPathLossSeries->setVisible(m_settings.m_displayPathLoss);
m_powerPathLossSeries->setName("Path Loss");
// Create X axis
m_powerXAxis = new QDateTimeAxis();
QString dateTimeFormat = "hh:mm:ss";
m_powerXAxis->setFormat(dateTimeFormat);
m_powerXAxis->setTitleText("Time");
// Create Y axis
m_powerYAxis = new QValueAxis();
m_powerYAxis->setRange(m_settings.m_minPower, m_settings.m_maxPower);
m_powerYAxis->setTitleText("Power (dB)");
m_powerChart->addAxis(m_powerXAxis, Qt::AlignBottom);
m_powerChart->addAxis(m_powerYAxis, Qt::AlignLeft);
m_powerChart->addSeries(m_powerAverageSeries);
m_powerAverageSeries->attachAxis(m_powerXAxis);
m_powerAverageSeries->attachAxis(m_powerYAxis);
m_powerChart->addSeries(m_powerMaxPeakSeries);
m_powerMaxPeakSeries->attachAxis(m_powerXAxis);
m_powerMaxPeakSeries->attachAxis(m_powerYAxis);
m_powerChart->addSeries(m_powerMinPeakSeries);
m_powerMinPeakSeries->attachAxis(m_powerXAxis);
m_powerMinPeakSeries->attachAxis(m_powerYAxis);
m_powerChart->addSeries(m_powerPulseAverageSeries);
m_powerPulseAverageSeries->attachAxis(m_powerXAxis);
m_powerPulseAverageSeries->attachAxis(m_powerYAxis);
m_powerChart->addSeries(m_powerPathLossSeries);
m_powerPathLossSeries->attachAxis(m_powerXAxis);
m_powerPathLossSeries->attachAxis(m_powerYAxis);
ui->powerChart->setChart(m_powerChart);
delete oldChart;
}
void HeatMapGUI::addToPowerSeries(QDateTime dateTime, double average, double pulseAverage, double max, double min, double pathLoss)
{
if (m_powerAverageSeries)
{
try
{
qint64 msecs = dateTime.toMSecsSinceEpoch();
if (!std::isnan(average)) {
m_powerAverageSeries->append(msecs, average);
}
if (!std::isnan(pulseAverage)) {
m_powerPulseAverageSeries->append(msecs, pulseAverage);
}
if (!std::isnan(max)) {
m_powerMaxPeakSeries->append(msecs, max);
}
if (!std::isnan(min)) {
m_powerMinPeakSeries->append(msecs, min);
}
if (!std::isnan(pathLoss)) {
m_powerPathLossSeries->append(msecs, pathLoss);
}
}
catch (std::bad_alloc&)
{
QMessageBox::critical(this, "Heat Map", QString("Failed to allocate memory for chart series"));
ui->displayChart->setChecked(false);
}
}
}
void HeatMapGUI::updateAxis()
{
if (!m_powerAverageSeries || !m_powerPathLossSeries || (m_powerPathLossSeries->count() <= 1)) {
return;
}
QDateTime current = QDateTime::currentDateTime();
QDateTime first = QDateTime::fromMSecsSinceEpoch(m_powerPathLossSeries->at(0).x());
QDateTime min = current.addSecs(-60 * m_settings.m_displayMins);
if (first > min) {
min = first;
}
m_powerXAxis->setRange(min, current);
}
void HeatMapGUI::trimPowerSeries(QLineSeries *series, QDateTime dateTime)
{
qint64 msecs = dateTime.toMSecsSinceEpoch();
for (int i = 0; i < series->count(); i++)
{
const QPointF& p = series->at(i);
if (p.x() >= msecs)
{
if (i > 0) {
series->removePoints(0, i);
}
break;
}
}
}
// Remove all points in series before the specified date and time
void HeatMapGUI::trimPowerSeries(QDateTime dateTime)
{
if (m_powerAverageSeries)
{
trimPowerSeries(m_powerAverageSeries, dateTime);
trimPowerSeries(m_powerMaxPeakSeries, dateTime);
trimPowerSeries(m_powerMinPeakSeries, dateTime);
trimPowerSeries(m_powerPulseAverageSeries, dateTime);
trimPowerSeries(m_powerPathLossSeries, dateTime);
}
}
void HeatMapGUI::on_displayAverage_clicked(bool checked)
{
m_settings.m_displayAverage = checked;
m_powerAverageSeries->setVisible(checked);
applySettings();
}
void HeatMapGUI::on_displayMax_clicked(bool checked)
{
m_settings.m_displayMax = checked;
m_powerMaxPeakSeries->setVisible(checked);
applySettings();
}
void HeatMapGUI::on_displayMin_clicked(bool checked)
{
m_settings.m_displayMin = checked;
m_powerMinPeakSeries->setVisible(checked);
applySettings();
}
void HeatMapGUI::on_displayPulseAverage_clicked(bool checked)
{
m_settings.m_displayPulseAverage = checked;
m_powerPulseAverageSeries->setVisible(checked);
applySettings();
}
void HeatMapGUI::on_displayPathLoss_clicked(bool checked)
{
m_settings.m_displayPathLoss = checked;
m_powerPathLossSeries->setVisible(checked);
applySettings();
}
void HeatMapGUI::on_displayMins_valueChanged(int value)
{
m_settings.m_displayMins = value;
updateAxis();
applySettings();
}
void HeatMapGUI::on_recordAverage_clicked(bool checked)
{
m_settings.m_recordAverage = checked;
resizeMap(0, 0);
applySettings();
}
void HeatMapGUI::on_recordMax_clicked(bool checked)
{
m_settings.m_recordMax = checked;
resizeMap(0, 0);
applySettings();
}
void HeatMapGUI::on_recordMin_clicked(bool checked)
{
m_settings.m_recordMin = checked;
resizeMap(0, 0);
applySettings();
}
void HeatMapGUI::on_recordPulseAverage_clicked(bool checked)
{
m_settings.m_recordPulseAverage = checked;
resizeMap(0, 0);
applySettings();
}
void HeatMapGUI::on_recordPathLoss_clicked(bool checked)
{
m_settings.m_recordPathLoss = checked;
resizeMap(0, 0);
applySettings();
}