1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-11-17 13:51:47 -05:00
sdrangel/plugins/channelrx/radioastronomy/radioastronomygui.cpp
2022-09-26 12:46:52 +01:00

6255 lines
230 KiB
C++

///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2016 Edouard Griffiths, F4EXB //
// Copyright (C) 2021 Jon Beniston, M7RCE //
// //
// 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 <numeric>
#include <algorithm>
#include <functional>
#include <ctype.h>
#include <QDockWidget>
#include <QMainWindow>
#include <QDebug>
#include <QMessageBox>
#include <QAction>
#include <QRegExp>
#include <QClipboard>
#include <QFileDialog>
#include <QImage>
#include <QTimer>
#include "radioastronomygui.h"
#include "device/deviceuiset.h"
#include "dsp/dspengine.h"
#include "dsp/dspcommands.h"
#include "ui_radioastronomygui.h"
#include "plugin/pluginapi.h"
#include "util/simpleserializer.h"
#include "util/db.h"
#include "util/astronomy.h"
#include "util/interpolation.h"
#include "util/png.h"
#include "util/units.h"
#include "gui/basicchannelsettingsdialog.h"
#include "gui/devicestreamselectiondialog.h"
#include "dsp/dspengine.h"
#include "gui/crightclickenabler.h"
#include "gui/timedelegate.h"
#include "gui/decimaldelegate.h"
#include "channel/channelwebapiutils.h"
#include "maincore.h"
#include "feature/featurewebapiutils.h"
#include "feature/feature.h"
#include "feature/featureset.h"
#include "radioastronomy.h"
#include "radioastronomysink.h"
#include "radioastronomysensordialog.h"
#include "radioastronomycalibrationdialog.h"
#include "SWGMapItem.h"
#include "SWGStarTrackerTarget.h"
#include "SWGStarTrackerDisplaySettings.h"
#include "SWGStarTrackerDisplayLoSSettings.h"
// Time value is in milliseconds - Displays hh:mm:ss or d hh:mm:ss
class TimeDeltaDelegate : public QStyledItemDelegate {
public:
virtual QString displayText(const QVariant &value, const QLocale &locale) const override
{
(void) locale;
qint64 v = value.toLongLong(); // In milliseconds
bool neg = v < 0;
v = abs(v);
qint64 days = v / (1000*60*60*24);
v = v % (1000*60*60*24);
qint64 hours = v / (1000*60*60);
v = v % (1000*60*60);
qint64 minutes = v / (1000*60);
v = v % (1000*60);
qint64 seconds = v / (1000);
//qint64 msec = v % 1000;
if (days > 0) {
return QString("%1%2 %3:%4:%5").arg(neg ? "-" : "").arg(days).arg(hours, 2, 10, QChar('0')).arg(minutes, 2, 10, QChar('0')).arg(seconds, 2, 10, QChar('0'));
} else {
return QString("%1%2:%3:%4").arg(neg ? "-" : "").arg(hours, 2, 10, QChar('0')).arg(minutes, 2, 10, QChar('0')).arg(seconds, 2, 10, QChar('0'));
}
}
private:
QString m_format;
};
// Delegate for table to display hours, minutes and seconds
class HMSDelegate : public QStyledItemDelegate {
public:
virtual QString displayText(const QVariant &value, const QLocale &locale) const override
{
(void) locale;
return Units::decimalHoursToHoursMinutesAndSeconds(value.toDouble());
}
};
// Delegate for table to display degrees, minutes and seconds
class DMSDelegate : public QStyledItemDelegate {
public:
virtual QString displayText(const QVariant &value, const QLocale &locale) const override
{
(void) locale;
return Units::decimalDegreesToDegreeMinutesAndSeconds(value.toDouble());
}
};
void RadioAstronomyGUI::LABData::read(QFile* file, float l, float b)
{
m_l = l;
m_b = b;
m_vlsr.clear();
m_temp.clear();
QTextStream in(file);
while (!in.atEnd())
{
QString line = in.readLine().trimmed();
if (!line.startsWith("%") && (line.size() > 0)) // Lines starting with % are comments
{
// 4 cols: v_lsr [km/s], T_B [K], freq. [Mhz], wavel. [cm]
line = line.simplified();
QStringList cols = line.split(" ");
if (cols.size() == 4)
{
m_vlsr.append(cols[0].toFloat());
m_temp.append(cols[1].toFloat());
}
else
{
qDebug() << "RadioAstronomyGUI::parseLAB: Unexpected number of columns";
}
}
}
}
void RadioAstronomyGUI::LABData::toSeries(QLineSeries *series)
{
series->clear();
series->setName(QString("LAB l=%1 b=%2").arg(m_l).arg(m_b));
for (int i = 0; i < m_vlsr.size(); i++) {
series->append(m_vlsr[i], m_temp[i]);
}
}
void RadioAstronomyGUI::SensorMeasurements::init(const QString& name, bool visible)
{
m_series = new QLineSeries();
m_series->setName(name);
m_series->setVisible(visible);
m_yAxis = new QValueAxis();
m_yAxis->setTitleText(name);
m_yAxis->setVisible(visible);
m_min = std::numeric_limits<double>::max();
m_max = -std::numeric_limits<double>::max();
}
void RadioAstronomyGUI::SensorMeasurements::setName(const QString& name)
{
if (m_series) {
m_series->setName(name);
}
if (m_yAxis) {
m_yAxis->setTitleText(name);
}
}
void RadioAstronomyGUI::SensorMeasurements::clicked(bool checked)
{
if (m_series) {
m_series->setVisible(checked);
}
if (m_yAxis) {
m_yAxis->setVisible(checked);
}
}
void RadioAstronomyGUI::SensorMeasurements::append(SensorMeasurement *measurement)
{
m_measurements.append(measurement);
addToSeries(measurement);
}
void RadioAstronomyGUI::SensorMeasurements::addToSeries(SensorMeasurement *measurement)
{
m_series->append(measurement->m_dateTime.toMSecsSinceEpoch(), measurement->m_value);
m_max = std::max(m_max, measurement->m_value);
m_min = std::min(m_min, measurement->m_value);
if (m_min == m_max) {
// Axis isn't drawn properly if min and max are the same
m_yAxis->setRange(m_min*0.9, m_max*1.1);
} else {
m_yAxis->setRange(m_min, m_max);
}
}
void RadioAstronomyGUI::SensorMeasurements::addAllToSeries()
{
for (int i = 0; i < m_measurements.size(); i++) {
addToSeries(m_measurements[i]);
}
}
void RadioAstronomyGUI::SensorMeasurements::clear()
{
m_series->clear();
qDeleteAll(m_measurements);
m_measurements.clear();
}
void RadioAstronomyGUI::SensorMeasurements::addToChart(QChart* chart, QDateTimeAxis* xAxis)
{
chart->addSeries(m_series);
m_series->attachAxis(xAxis);
m_series->attachAxis(m_yAxis);
}
void RadioAstronomyGUI::SensorMeasurements::setPen(const QPen& pen)
{
m_series->setPen(pen);
}
QValueAxis* RadioAstronomyGUI::SensorMeasurements::yAxis() const
{
return m_yAxis;
}
qreal RadioAstronomyGUI::SensorMeasurements::lastValue()
{
if (m_measurements.size() > 0) {
return m_measurements.last()->m_value;
} else {
return 0.0;
}
}
void RadioAstronomyGUI::resizePowerTable()
{
// Fill table with a row of dummy data that will size the columns nicely
// Trailing spaces are for sort arrow
int row = ui->powerTable->rowCount();
ui->powerTable->setRowCount(row + 1);
ui->powerTable->setItem(row, POWER_COL_DATE, new QTableWidgetItem("15/04/2016"));
ui->powerTable->setItem(row, POWER_COL_TIME, new QTableWidgetItem("10:17:00"));
ui->powerTable->setItem(row, POWER_COL_POWER, new QTableWidgetItem("1.235-e5"));
ui->powerTable->setItem(row, POWER_COL_POWER_DB, new QTableWidgetItem("-100.0"));
ui->powerTable->setItem(row, POWER_COL_POWER_DBM, new QTableWidgetItem("-100.0"));
ui->powerTable->setItem(row, POWER_COL_TSYS, new QTableWidgetItem("3000"));
ui->powerTable->setItem(row, POWER_COL_TSYS0, new QTableWidgetItem("100"));
ui->powerTable->setItem(row, POWER_COL_TSOURCE, new QTableWidgetItem("300"));
ui->powerTable->setItem(row, POWER_COL_TB, new QTableWidgetItem("100000"));
ui->powerTable->setItem(row, POWER_COL_TSKY, new QTableWidgetItem("300"));
ui->powerTable->setItem(row, POWER_COL_FLUX, new QTableWidgetItem("100000.00"));
ui->powerTable->setItem(row, POWER_COL_SIGMA_T, new QTableWidgetItem("0.01"));
ui->powerTable->setItem(row, POWER_COL_SIGMA_S, new QTableWidgetItem("1000.0"));
ui->powerTable->setItem(row, POWER_COL_OMEGA_A, new QTableWidgetItem("0.000001"));
ui->powerTable->setItem(row, POWER_COL_OMEGA_S, new QTableWidgetItem("0.000001"));
ui->powerTable->setItem(row, POWER_COL_RA, new QTableWidgetItem("12h59m59.10s"));
ui->powerTable->setItem(row, POWER_COL_DEC, new QTableWidgetItem("-90d59\'59.00\""));
ui->powerTable->setItem(row, POWER_COL_GAL_LAT, new QTableWidgetItem("-90.0"));
ui->powerTable->setItem(row, POWER_COL_GAL_LON, new QTableWidgetItem("359.0"));
ui->powerTable->setItem(row, POWER_COL_AZ, new QTableWidgetItem("359.0"));
ui->powerTable->setItem(row, POWER_COL_EL, new QTableWidgetItem("90.0"));
ui->powerTable->setItem(row, POWER_COL_VBCRS, new QTableWidgetItem("10.0"));
ui->powerTable->setItem(row, POWER_COL_VLSR, new QTableWidgetItem("10.0"));
ui->powerTable->setItem(row, POWER_COL_SOLAR_FLUX, new QTableWidgetItem("60.0"));
ui->powerTable->setItem(row, POWER_COL_AIR_TEMP, new QTableWidgetItem("20.0"));
ui->powerTable->setItem(row, POWER_COL_SENSOR_1, new QTableWidgetItem("1.0000000"));
ui->powerTable->setItem(row, POWER_COL_SENSOR_2, new QTableWidgetItem("1.0000000"));
ui->powerTable->resizeColumnsToContents();
ui->powerTable->removeRow(row);
}
void RadioAstronomyGUI::resizePowerMarkerTable()
{
// Fill table with a row of dummy data that will size the columns nicely
int row = ui->powerMarkerTable->rowCount();
ui->powerMarkerTable->setRowCount(row + 1);
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_NAME, new QTableWidgetItem("Max"));
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_DATE, new QTableWidgetItem("15/04/2016"));
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_TIME, new QTableWidgetItem("10:17:00"));
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_VALUE, new QTableWidgetItem("1000.0"));
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_DELTA_X, new QTableWidgetItem("1 23:59:59"));
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_DELTA_Y, new QTableWidgetItem("1000.0"));
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_DELTA_TO, new QTableWidgetItem("Max"));
ui->powerMarkerTable->resizeColumnsToContents();
ui->powerMarkerTable->removeRow(row);
}
void RadioAstronomyGUI::resizeSpectrumMarkerTable()
{
// Fill table with a row of dummy data that will size the columns nicely
int row = ui->spectrumMarkerTable->rowCount();
ui->spectrumMarkerTable->setRowCount(row + 1);
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_NAME, new QTableWidgetItem("Max"));
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_FREQ, new QTableWidgetItem("1420.405000"));
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_VALUE, new QTableWidgetItem("1000.0"));
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_DELTA_X, new QTableWidgetItem("1420.405000"));
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_DELTA_Y, new QTableWidgetItem("1000.0"));
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_DELTA_TO, new QTableWidgetItem("M1"));
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_VR, new QTableWidgetItem("-100.0"));
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_R, new QTableWidgetItem("10.0"));
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_D, new QTableWidgetItem("10.0/10.0"));
QTableWidgetItem* check = new QTableWidgetItem();
check->setFlags(Qt::ItemIsUserCheckable);
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_PLOT_MAX, check);
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_R_MIN, new QTableWidgetItem("10.0"));
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_V, new QTableWidgetItem("250.0"));
ui->spectrumMarkerTable->resizeColumnsToContents();
ui->spectrumMarkerTable->removeRow(row);
}
void RadioAstronomyGUI::calcSpectrumMarkerDelta()
{
if (m_spectrumM1Valid && m_spectrumM2Valid)
{
qreal dx = m_spectrumM2X - m_spectrumM1X;
qreal dy = m_spectrumM2Y - m_spectrumM1Y;
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_M2, SPECTRUM_MARKER_COL_DELTA_X)->setData(Qt::DisplayRole, dx);
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_M2, SPECTRUM_MARKER_COL_DELTA_Y)->setData(Qt::DisplayRole, dy);
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_M2, SPECTRUM_MARKER_COL_DELTA_TO)->setData(Qt::DisplayRole, "M1");
}
}
void RadioAstronomyGUI::calcPowerMarkerDelta()
{
if (m_powerM1Valid && m_powerM2Valid)
{
qreal dx = m_powerM2X - m_powerM1X;
qreal dy = m_powerM2Y - m_powerM1Y;
ui->powerMarkerTable->item(POWER_MARKER_ROW_M2, POWER_MARKER_COL_DELTA_X)->setData(Qt::DisplayRole, dx);
ui->powerMarkerTable->item(POWER_MARKER_ROW_M2, POWER_MARKER_COL_DELTA_Y)->setData(Qt::DisplayRole, dy);
ui->powerMarkerTable->item(POWER_MARKER_ROW_M2, POWER_MARKER_COL_DELTA_TO)->setData(Qt::DisplayRole, "M1");
}
}
void RadioAstronomyGUI::calcPowerPeakDelta()
{
qreal dx = m_powerMaxX - m_powerMinX;
qreal dy = m_powerMaxY - m_powerMinY;
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MIN, POWER_MARKER_COL_DELTA_X)->setData(Qt::DisplayRole, dx);
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MIN, POWER_MARKER_COL_DELTA_Y)->setData(Qt::DisplayRole, dy);
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MIN, POWER_MARKER_COL_DELTA_TO)->setData(Qt::DisplayRole, "Max");
}
void RadioAstronomyGUI::addToPowerSeries(FFTMeasurement *fft, bool skipCalcs)
{
if ( ((m_settings.m_powerYUnits == RadioAstronomySettings::PY_DBFS) || fft->m_temp) // Only dBFS valid if no temp was calculated
&& !((m_settings.m_powerYUnits == RadioAstronomySettings::PY_DBM) && (fft->m_tSys == 0.0f)) // dBm value not valid if temp is 0
)
{
qreal power;
switch (m_settings.m_powerYData)
{
case RadioAstronomySettings::PY_POWER:
switch (m_settings.m_powerYUnits)
{
case RadioAstronomySettings::PY_DBFS:
power = fft->m_totalPowerdBFS;
break;
case RadioAstronomySettings::PY_DBM:
power = fft->m_totalPowerdBm;
break;
case RadioAstronomySettings::PY_WATTS:
power = fft->m_totalPowerWatts;
break;
default:
break;
}
break;
case RadioAstronomySettings::PY_TSYS:
power = fft->m_tSys;
break;
case RadioAstronomySettings::PY_TSOURCE:
power = fft->m_tSource;
break;
case RadioAstronomySettings::PY_FLUX:
switch (m_settings.m_powerYUnits)
{
case RadioAstronomySettings::PY_SFU:
power = Units::wattsPerMetrePerHertzToSolarFluxUnits(fft->m_flux);
break;
case RadioAstronomySettings::PY_JANSKY:
power = Units::wattsPerMetrePerHertzToJansky(fft->m_flux);
break;
default:
break;
}
break;
default:
break;
}
QDateTime dateTime = fft->m_dateTime;
if (m_powerSeries->count() == 0)
{
m_powerMin = power;
m_powerMax = power;
}
else
{
m_powerMin = std::min(power, m_powerMin);
m_powerMax = std::max(power, m_powerMax);
}
m_powerSeries->append(dateTime.toMSecsSinceEpoch(), power);
addToPowerFilter(dateTime.toMSecsSinceEpoch(), power);
if (!skipCalcs)
{
if (m_settings.m_powerAutoscale)
{
blockApplySettings(true);
powerAutoscaleY(false);
blockApplySettings(false);
}
}
if (m_settings.m_powerYUnits == RadioAstronomySettings::PY_KELVIN) {
m_powerTsys0Series->append(dateTime.toMSecsSinceEpoch(), fft->m_tSys0);
} else if (m_settings.m_powerYUnits == RadioAstronomySettings::PY_DBM) {
m_powerTsys0Series->append(dateTime.toMSecsSinceEpoch(), Astronomy::noisePowerdBm(fft->m_tSys0, fft->m_sampleRate));
} else if (m_settings.m_powerYUnits == RadioAstronomySettings::PY_WATTS) {
m_powerTsys0Series->append(dateTime.toMSecsSinceEpoch(), Astronomy::m_boltzmann * fft->m_tSys0 * fft->m_sampleRate);
}
if (!m_powerPeakValid)
{
m_powerPeakValid = true;
m_powerMinY = power;
m_powerMinX = dateTime.toMSecsSinceEpoch();
m_powerMaxY = power;
m_powerMaxX = dateTime.toMSecsSinceEpoch();
m_powerPeakSeries->clear();
m_powerPeakSeries->append(m_powerMaxX, m_powerMaxY);
m_powerPeakSeries->append(m_powerMaxX, m_powerMaxY);
QDateTime dt = QDateTime::fromMSecsSinceEpoch(m_powerMaxX);
if (!skipCalcs)
{
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MAX, POWER_MARKER_COL_DATE)->setData(Qt::DisplayRole, dt.date());
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MAX, POWER_MARKER_COL_TIME)->setData(Qt::DisplayRole, dt.time());
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MAX, POWER_MARKER_COL_VALUE)->setData(Qt::DisplayRole, m_powerMaxY);
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MIN, POWER_MARKER_COL_DATE)->setData(Qt::DisplayRole, dt.date());
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MIN, POWER_MARKER_COL_TIME)->setData(Qt::DisplayRole, dt.time());
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MIN, POWER_MARKER_COL_VALUE)->setData(Qt::DisplayRole, m_powerMinY);
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MIN, POWER_MARKER_COL_DELTA_X)->setData(Qt::DisplayRole, 0.0);
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MIN, POWER_MARKER_COL_DELTA_Y)->setData(Qt::DisplayRole, 0.0);
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MIN, POWER_MARKER_COL_DELTA_TO)->setData(Qt::DisplayRole, "Max");
}
}
if (power > m_powerMaxY)
{
m_powerMaxY = power;
m_powerMaxX = dateTime.toMSecsSinceEpoch();
m_powerPeakSeries->replace(0, m_powerMaxX, m_powerMaxY);
QDateTime dt = QDateTime::fromMSecsSinceEpoch(m_powerMaxX);
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MAX, POWER_MARKER_COL_DATE)->setData(Qt::DisplayRole, dt.date());
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MAX, POWER_MARKER_COL_TIME)->setData(Qt::DisplayRole, dt.time());
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MAX, POWER_MARKER_COL_VALUE)->setData(Qt::DisplayRole, m_powerMaxY);
calcPowerPeakDelta();
}
else if (power < m_powerMinY)
{
m_powerMinY = power;
m_powerMinX = dateTime.toMSecsSinceEpoch();
m_powerPeakSeries->replace(1, m_powerMinX, m_powerMinY);
QDateTime dt = QDateTime::fromMSecsSinceEpoch(m_powerMinX);
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MIN, POWER_MARKER_COL_DATE)->setData(Qt::DisplayRole, dt.date());
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MIN, POWER_MARKER_COL_TIME)->setData(Qt::DisplayRole, dt.time());
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MIN, POWER_MARKER_COL_VALUE)->setData(Qt::DisplayRole, m_powerMinY);
calcPowerPeakDelta();
}
// Update markers (E.g. if scale changes)
int c = m_powerSeries->count();
if (c >= 2)
{
QPointF p1 = m_powerSeries->at(c-2);
QPointF p2 = m_powerSeries->at(c-1);
if (m_powerM1Valid && (m_powerM1X >= p1.x()) && (m_powerM1X < p2.x()))
{
m_powerM1Y = Interpolation::interpolate(p1.x(), p1.y(), p2.x(), p2.y(), m_powerM1X);
m_powerMarkerSeries->insert(0, QPointF(dateTime.toMSecsSinceEpoch(), m_powerM1Y));
ui->powerMarkerTable->item(POWER_MARKER_ROW_M1, POWER_MARKER_COL_VALUE)->setData(Qt::DisplayRole, m_powerM1Y);
calcPowerMarkerDelta();
}
if (m_powerM2Valid && (m_powerM2X >= p1.x()) && (m_powerM2X < p2.x()))
{
m_powerM2Y = Interpolation::interpolate(p1.x(), p1.y(), p2.x(), p2.y(), m_powerM2X);
m_powerMarkerSeries->append(dateTime.toMSecsSinceEpoch(), m_powerM2Y);
ui->powerMarkerTable->item(POWER_MARKER_ROW_M2, POWER_MARKER_COL_VALUE)->setData(Qt::DisplayRole, m_powerM2Y);
calcPowerMarkerDelta();
}
}
if (!skipCalcs)
{
// Set X axis format to include date if measurements span over different days
// Seems there's a QT bug here, if we call m_powerXAxis->setFormat, the chart isn't
// redrawn properly, so we have to redraw the whole thing
QDateTime minDateTime = m_powerXAxis->min();
QDateTime maxDateTime = m_powerXAxis->max();
bool sameDay = minDateTime.date() == maxDateTime.date();
if (!sameDay && m_powerXAxisSameDay)
{
m_powerXAxisSameDay = true;
QTimer::singleShot(100, this, SLOT(plotPowerChart()));
}
}
if (!skipCalcs && ui->powerShowAvg->isChecked()) {
calcAverages();
}
}
if (m_powerSeries->count() <= 1) // Don't check skipCalcs here, as that will be set for first data
{
ui->powerStartTime->setMinimumDateTime(fft->m_dateTime);
ui->powerEndTime->setMinimumDateTime(fft->m_dateTime);
if (m_settings.m_powerAutoscale)
{
ui->powerStartTime->setDateTime(fft->m_dateTime);
ui->powerEndTime->setDateTime(fft->m_dateTime);
}
}
if (!skipCalcs)
{
ui->powerStartTime->setMaximumDateTime(fft->m_dateTime);
ui->powerEndTime->setMaximumDateTime(fft->m_dateTime);
if (m_settings.m_powerAutoscale) {
ui->powerEndTime->setDateTime(fft->m_dateTime);
}
}
}
double RadioAstronomyGUI::degreesToSteradian(double deg) const
{
// https://en.wikipedia.org/wiki/Steradian - Other properties
double s = sin(Units::degreesToRadians(deg) / 4.0);
return 4.0 * M_PI * s * s;
}
double RadioAstronomyGUI::hpbwToSteradians(double hpbw) const
{
// https://www.cv.nrao.edu/~sransom/web/Ch3.html#E118
double theta = Units::degreesToRadians(hpbw);
return theta * theta * M_PI / (4.0 * M_LN2);
}
double RadioAstronomyGUI::calcOmegaA() const
{
return hpbwToSteradians(m_beamWidth);
}
double RadioAstronomyGUI::calcOmegaS() const
{
if (m_settings.m_sourceType == RadioAstronomySettings::UNKNOWN)
{
return 0.0;
}
else if (m_settings.m_sourceType == RadioAstronomySettings::EXTENDED)
{
return calcOmegaA();
}
else
{
return m_settings.m_omegaSUnits == RadioAstronomySettings::STERRADIANS ? m_settings.m_omegaS : degreesToSteradian(m_settings.m_omegaS);
}
}
double RadioAstronomyGUI::beamFillingFactor() const
{
if (m_settings.m_sourceType == RadioAstronomySettings::EXTENDED)
{
return 1.0;
}
else
{
// https://www.cv.nrao.edu/~sransom/web/Ch3.html#E55
return calcOmegaS() / calcOmegaA();
}
}
void RadioAstronomyGUI::powerMeasurementReceived(FFTMeasurement *fft, bool skipCalcs)
{
int row = ui->powerTable->rowCount();
ui->powerTable->setRowCount(row + 1);
QTableWidgetItem* dateItem = new QTableWidgetItem();
QTableWidgetItem* timeItem = new QTableWidgetItem();
QTableWidgetItem* powerItem = new QTableWidgetItem();
QTableWidgetItem* powerDBItem = new QTableWidgetItem();
QTableWidgetItem* powerdBmItem = new QTableWidgetItem();
QTableWidgetItem* tSysItem = new QTableWidgetItem();
QTableWidgetItem* tSys0Item = new QTableWidgetItem();
QTableWidgetItem* tSourceItem = new QTableWidgetItem();
QTableWidgetItem* tBItem = new QTableWidgetItem();
QTableWidgetItem* tSkyItem = new QTableWidgetItem();
QTableWidgetItem* fluxItem = new QTableWidgetItem();
QTableWidgetItem* sigmaTItem = new QTableWidgetItem();
QTableWidgetItem* sigmaSItem = new QTableWidgetItem();
QTableWidgetItem* omegaAItem = new QTableWidgetItem();
QTableWidgetItem* omegaSItem = new QTableWidgetItem();
QTableWidgetItem* raItem = new QTableWidgetItem();
QTableWidgetItem* decItem = new QTableWidgetItem();
QTableWidgetItem* lonItem = new QTableWidgetItem();
QTableWidgetItem* latItem = new QTableWidgetItem();
QTableWidgetItem* azItem = new QTableWidgetItem();
QTableWidgetItem* elItem = new QTableWidgetItem();
QTableWidgetItem* vBCRSItem = new QTableWidgetItem();
QTableWidgetItem* vLSRItem = new QTableWidgetItem();
QTableWidgetItem* solarFluxItem = new QTableWidgetItem();
QTableWidgetItem* airTempItem = new QTableWidgetItem();
QTableWidgetItem* sensor1Item = new QTableWidgetItem();
QTableWidgetItem* sensor2Item = new QTableWidgetItem();
ui->powerTable->setItem(row, POWER_COL_DATE, dateItem);
ui->powerTable->setItem(row, POWER_COL_TIME, timeItem);
ui->powerTable->setItem(row, POWER_COL_POWER, powerItem);
ui->powerTable->setItem(row, POWER_COL_POWER_DB, powerDBItem);
ui->powerTable->setItem(row, POWER_COL_POWER_DBM, powerdBmItem);
ui->powerTable->setItem(row, POWER_COL_TSYS, tSysItem);
ui->powerTable->setItem(row, POWER_COL_TSYS0, tSys0Item);
ui->powerTable->setItem(row, POWER_COL_TSOURCE, tSourceItem);
ui->powerTable->setItem(row, POWER_COL_TB, tBItem);
ui->powerTable->setItem(row, POWER_COL_TSKY, tSkyItem);
ui->powerTable->setItem(row, POWER_COL_FLUX, fluxItem);
ui->powerTable->setItem(row, POWER_COL_SIGMA_T, sigmaTItem);
ui->powerTable->setItem(row, POWER_COL_SIGMA_S, sigmaSItem);
ui->powerTable->setItem(row, POWER_COL_OMEGA_A, omegaAItem);
ui->powerTable->setItem(row, POWER_COL_OMEGA_S, omegaSItem);
ui->powerTable->setItem(row, POWER_COL_RA, raItem);
ui->powerTable->setItem(row, POWER_COL_DEC, decItem);
ui->powerTable->setItem(row, POWER_COL_GAL_LON, lonItem);
ui->powerTable->setItem(row, POWER_COL_GAL_LAT, latItem);
ui->powerTable->setItem(row, POWER_COL_AZ, azItem);
ui->powerTable->setItem(row, POWER_COL_EL, elItem);
ui->powerTable->setItem(row, POWER_COL_VBCRS, vBCRSItem);
ui->powerTable->setItem(row, POWER_COL_VLSR, vLSRItem);
ui->powerTable->setItem(row, POWER_COL_SOLAR_FLUX, solarFluxItem);
ui->powerTable->setItem(row, POWER_COL_AIR_TEMP, airTempItem);
ui->powerTable->setItem(row, POWER_COL_SENSOR_1, sensor1Item);
ui->powerTable->setItem(row, POWER_COL_SENSOR_2, sensor2Item);
QDateTime dateTime = fft->m_dateTime;
dateItem->setData(Qt::DisplayRole, dateTime.date());
timeItem->setData(Qt::DisplayRole, dateTime.time());
powerItem->setData(Qt::DisplayRole, fft->m_totalPower);
powerDBItem->setData(Qt::DisplayRole, fft->m_totalPowerdBFS);
if (fft->m_tSys != 0.0f) {
powerdBmItem->setData(Qt::DisplayRole, fft->m_totalPowerdBm);
}
if (fft->m_temp) {
updatePowerColumns(row, fft);
}
if (fft->m_coordsValid)
{
raItem->setData(Qt::DisplayRole, fft->m_ra);
decItem->setData(Qt::DisplayRole, fft->m_dec);
latItem->setData(Qt::DisplayRole, fft->m_b);
lonItem->setData(Qt::DisplayRole, fft->m_l);
azItem->setData(Qt::DisplayRole, fft->m_azimuth);
elItem->setData(Qt::DisplayRole, fft->m_elevation);
vBCRSItem->setData(Qt::DisplayRole, fft->m_vBCRS);
vLSRItem->setData(Qt::DisplayRole, fft->m_vLSR);
tSkyItem->setData(Qt::DisplayRole, fft->m_skyTemp);
}
solarFluxItem->setData(Qt::DisplayRole, fft->m_solarFlux);
airTempItem->setData(Qt::DisplayRole, fft->m_airTemp);
sensor1Item->setData(Qt::DisplayRole, fft->m_sensor[0]);
sensor2Item->setData(Qt::DisplayRole, fft->m_sensor[1]);
addToPowerSeries(fft, skipCalcs);
}
void RadioAstronomyGUI::powerAutoscale()
{
if (m_settings.m_powerAutoscale)
{
on_powerAutoscaleX_clicked();
on_powerAutoscaleY_clicked();
}
}
// Scale X and Y axis according to min and max values
void RadioAstronomyGUI::on_powerAutoscale_toggled(bool checked)
{
m_settings.m_powerAutoscale = checked;
ui->powerAutoscaleX->setEnabled(!m_settings.m_powerAutoscale);
ui->powerAutoscaleY->setEnabled(!m_settings.m_powerAutoscale);
ui->powerReference->setEnabled(!m_settings.m_powerAutoscale);
ui->powerRange->setEnabled(!m_settings.m_powerAutoscale);
ui->powerStartTime->setEnabled(!m_settings.m_powerAutoscale);
ui->powerEndTime->setEnabled(!m_settings.m_powerAutoscale);
powerAutoscale();
applySettings();
}
void RadioAstronomyGUI::powerAutoscaleY(bool adjustAxis)
{
double min = m_powerMin;
double max = m_powerMax;
double range = max - min;
// Round to 1 or 2 decimal places
if (range > 1.0)
{
min = std::floor(min * 10.0) / 10.0;
max = std::ceil(max * 10.0) / 10.0;
}
else
{
min = std::floor(min * 100.0) / 100.0;
max = std::ceil(max * 100.0) / 100.0;
}
range = max - min;
max += range * 0.2; // Add 20% space for markers
range = max - min;
range = std::max(0.1, range); // Don't be smaller than minimum value we can set in GUI
if (adjustAxis) {
m_powerYAxis->setRange(min, max);
}
ui->powerRange->setValue(range); // Call before setting reference, so number of decimals are adjusted
ui->powerReference->setValue(max);
}
// Scale Y axis according to min and max values
void RadioAstronomyGUI::on_powerAutoscaleY_clicked()
{
if (m_powerYAxis) {
powerAutoscaleY(true);
}
}
// Scale X axis according to min and max values in series
void RadioAstronomyGUI::on_powerAutoscaleX_clicked()
{
if (m_powerSeries && (m_powerSeries->count() > 0))
{
QDateTime start = QDateTime::fromMSecsSinceEpoch(m_powerSeries->at(0).x());
QDateTime end = QDateTime::fromMSecsSinceEpoch(m_powerSeries->at(m_powerSeries->count()-1).x());
ui->powerStartTime->setDateTime(start);
ui->powerEndTime->setDateTime(end);
}
}
void RadioAstronomyGUI::on_powerReference_valueChanged(double value)
{
m_settings.m_powerReference = value;
if (m_powerYAxis) {
m_powerYAxis->setRange(m_settings.m_powerReference - m_settings.m_powerRange, m_settings.m_powerReference);
}
applySettings();
}
void RadioAstronomyGUI::on_powerRange_valueChanged(double value)
{
m_settings.m_powerRange = value;
if (m_settings.m_powerRange <= 1.0)
{
ui->powerRange->setSingleStep(0.1);
ui->powerRange->setDecimals(2);
ui->powerReference->setDecimals(2);
}
else
{
ui->powerRange->setSingleStep(1.0);
ui->powerRange->setDecimals(1);
ui->powerReference->setDecimals(1);
}
if (m_powerYAxis) {
m_powerYAxis->setRange(m_settings.m_powerReference - m_settings.m_powerRange, m_settings.m_powerReference);
}
applySettings();
}
void RadioAstronomyGUI::on_powerStartTime_dateTimeChanged(QDateTime value)
{
if (m_powerXAxis) {
m_powerXAxis->setMin(value);
}
}
void RadioAstronomyGUI::on_powerEndTime_dateTimeChanged(QDateTime value)
{
if (m_powerXAxis) {
m_powerXAxis->setMax(value);
}
}
// Columns in table reordered
void RadioAstronomyGUI::powerTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex)
{
(void) oldVisualIndex;
m_settings.m_powerTableColumnIndexes[logicalIndex] = newVisualIndex;
}
// Column in table resized (when hidden size is 0)
void RadioAstronomyGUI::powerTable_sectionResized(int logicalIndex, int oldSize, int newSize)
{
(void) oldSize;
m_settings.m_powerTableColumnSizes[logicalIndex] = newSize;
}
// Right click in table header - show column select menu
void RadioAstronomyGUI::powerTableColumnSelectMenu(QPoint pos)
{
powerTableMenu->popup(ui->powerTable->horizontalHeader()->viewport()->mapToGlobal(pos));
}
// Hide/show column when menu selected
void RadioAstronomyGUI::powerTableColumnSelectMenuChecked(bool checked)
{
(void) checked;
QAction* action = qobject_cast<QAction*>(sender());
if (action != nullptr)
{
int idx = action->data().toInt(nullptr);
ui->powerTable->setColumnHidden(idx, !action->isChecked());
}
}
// Create column select menu item
QAction *RadioAstronomyGUI::createCheckableItem(QString &text, int idx, bool checked, const char *slot)
{
QAction *action = new QAction(text, this);
action->setCheckable(true);
action->setChecked(checked);
action->setData(QVariant(idx));
connect(action, SIGNAL(triggered()), this, slot);
return action;
}
RadioAstronomyGUI* RadioAstronomyGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel)
{
RadioAstronomyGUI* gui = new RadioAstronomyGUI(pluginAPI, deviceUISet, rxChannel);
return gui;
}
void RadioAstronomyGUI::destroy()
{
delete this;
}
void RadioAstronomyGUI::resetToDefaults()
{
m_settings.resetToDefaults();
displaySettings();
applySettings(true);
}
QByteArray RadioAstronomyGUI::serialize() const
{
return m_settings.serialize();
}
bool RadioAstronomyGUI::deserialize(const QByteArray& data)
{
if(m_settings.deserialize(data)) {
displaySettings();
applySettings(true);
return true;
} else {
resetToDefaults();
return false;
}
}
void RadioAstronomyGUI::updateAvailableFeatures()
{
QString currentText = ui->starTracker->currentText();
ui->starTracker->blockSignals(true);
ui->starTracker->clear();
for (const auto& feature : m_availableFeatures) {
ui->starTracker->addItem(tr("F%1:%2 %3").arg(feature.m_featureSetIndex).arg(feature.m_featureIndex).arg(feature.m_type));
}
if (currentText.isEmpty())
{
if (m_availableFeatures.size() > 0) {
ui->starTracker->setCurrentIndex(0);
}
}
else
{
ui->starTracker->setCurrentIndex(ui->starTracker->findText(currentText));
}
ui->starTracker->blockSignals(false);
QString newText = ui->starTracker->currentText();
if (currentText != newText)
{
m_settings.m_starTracker = newText;
applySettings();
}
}
bool RadioAstronomyGUI::handleMessage(const Message& message)
{
if (DSPSignalNotification::match(message))
{
DSPSignalNotification& notif = (DSPSignalNotification&) message;
m_basebandSampleRate = notif.getSampleRate();
m_centerFrequency = notif.getCenterFrequency();
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();
if (m_settings.m_tempGalLink) {
calcGalacticBackgroundTemp();
}
updateTSys0();
return true;
}
else if (RadioAstronomy::MsgReportAvailableFeatures::match(message))
{
qDebug("RadioAstronomyGUI::handleMessage: MsgReportAvailableFeatures");
RadioAstronomy::MsgReportAvailableFeatures& report = (RadioAstronomy::MsgReportAvailableFeatures&) message;
m_availableFeatures = report.getFeatures();
updateAvailableFeatures();
return true;
}
else if (MainCore::MsgStarTrackerTarget::match(message))
{
MainCore::MsgStarTrackerTarget& msg = (MainCore::MsgStarTrackerTarget&)message;
SWGSDRangel::SWGStarTrackerTarget *target = msg.getSWGStarTrackerTarget();
m_coordsValid = true;
m_ra = target->getRa();
m_dec = target->getDec();
m_azimuth = target->getAzimuth();
m_elevation = target->getElevation();
m_l = target->getL();
m_b = target->getB();
m_vBCRS = target->getEarthRotationVelocity() + target->getEarthOrbitVelocityBcrs();
m_vLSR = target->getSunVelocityLsr() + m_vBCRS;
m_solarFlux = target->getSolarFlux();
double airTemp = target->getAirTemperature();
m_skyTemp = target->getSkyTemperature();
m_beamWidth = target->getHpbw();
if (m_settings.m_elevationLink) {
ui->elevation->setValue(m_elevation);
}
if (m_settings.m_tempAirLink) {
ui->tempAir->setValue(airTemp);
}
SensorMeasurement* sm = new SensorMeasurement(QDateTime::currentDateTime(), airTemp);
m_airTemps.append(sm);
updateTSys0();
updateOmegaA();
return true;
}
else if (RadioAstronomy::MsgConfigureRadioAstronomy::match(message))
{
const RadioAstronomy::MsgConfigureRadioAstronomy& cfg = (RadioAstronomy::MsgConfigureRadioAstronomy&) message;
m_settings = cfg.getSettings();
blockApplySettings(true);
m_channelMarker.updateSettings(static_cast<const ChannelMarker*>(m_settings.m_channelMarker));
displaySettings();
blockApplySettings(false);
return true;
}
else if (RadioAstronomy::MsgMeasurementProgress::match(message))
{
RadioAstronomy::MsgMeasurementProgress& progress = (RadioAstronomy::MsgMeasurementProgress&) message;
ui->measurementProgress->setValue(progress.getPercentComplete());
return true;
}
else if (RadioAstronomy::MsgSweepStatus::match(message))
{
RadioAstronomy::MsgSweepStatus& status = (RadioAstronomy::MsgSweepStatus&) message;
ui->sweepStatus->setText(status.getStatus());
return true;
}
else if (RadioAstronomy::MsgSweepComplete::match(message))
{
ui->startStop->blockSignals(true);
ui->startStop->setChecked(false);
ui->startStop->blockSignals(false);
ui->startStop->setStyleSheet("QToolButton { background-color : blue; }");
return true;
}
else if (RadioAstronomy::MsgCalComplete::match(message))
{
RadioAstronomy::MsgCalComplete& measurement = (RadioAstronomy::MsgCalComplete&) message;
calCompletetReceived(measurement);
return true;
}
else if (RadioAstronomy::MsgFFTMeasurement::match(message))
{
RadioAstronomy::MsgFFTMeasurement& measurement = (RadioAstronomy::MsgFFTMeasurement&) message;
fftMeasurementReceived(measurement);
if (m_settings.m_runMode == RadioAstronomySettings::SINGLE)
{
ui->startStop->blockSignals(true);
ui->startStop->setChecked(false);
ui->startStop->blockSignals(false);
ui->startStop->setStyleSheet("QToolButton { background-color : blue; }");
}
return true;
}
else if (RadioAstronomy::MsgSensorMeasurement::match(message))
{
RadioAstronomy::MsgSensorMeasurement& measurement = (RadioAstronomy::MsgSensorMeasurement&) message;
sensorMeasurementReceived(measurement);
return true;
}
else if (RadioAstronomy::MsgReportAvailableRotators::match(message))
{
RadioAstronomy::MsgReportAvailableRotators& report = (RadioAstronomy::MsgReportAvailableRotators&) message;
updateRotatorList(report.getFeatures());
return true;
}
return false;
}
void RadioAstronomyGUI::handleInputMessages()
{
Message* message;
while ((message = getInputMessageQueue()->pop()) != 0)
{
if (handleMessage(*message))
{
delete message;
}
}
}
void RadioAstronomyGUI::channelMarkerChangedByCursor()
{
ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency());
m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
applySettings();
}
void RadioAstronomyGUI::channelMarkerHighlightedByCursor()
{
setHighlighted(m_channelMarker.getHighlighted());
}
// Calculate Tsys0 - i.e. receiver noise temperature when there's no source signal, just unwanted noise
void RadioAstronomyGUI::updateTSys0()
{
double tSys0 = calcTSys0();
ui->tSys0->setText(QString("%1").arg(round(tSys0)));
double sigmaT = calcSigmaT(tSys0);
double sigmaS = calcSigmaS(tSys0);
ui->sigmaTSys0->setText(QString("%1").arg(sigmaT, 0, 'f', 1));
ui->sigmaSSys0->setText(QString("%1").arg(sigmaS, 0, 'f', 1));
}
// Estimate of system noise temperature due to all sources of unwanted noise, from user settings
double RadioAstronomyGUI::calcTSys0() const
{
return m_settings.m_tempRX + m_settings.m_tempCMB + m_settings.m_tempGal + m_settings.m_tempSP + m_settings.m_tempAtm;
}
// Calculate measurement time
double RadioAstronomyGUI::calcTau() const
{
return m_settings.m_integration / (m_settings.m_sampleRate / (double)m_settings.m_fftSize);
}
double RadioAstronomyGUI::calcTau(const FFTMeasurement* fft) const
{
return fft->m_integration / (fft->m_sampleRate / (double)fft->m_fftSize);
}
// Calculate variation in Tsys due to random noise fluctuations, including receiver gain variations
// Minimum temp we can reliably detect will be ~5x this
// Uses practical total-power radiometer equation: https://www.cv.nrao.edu/~sransom/web/Ch3.html#E158
double RadioAstronomyGUI::calcSigmaT(double tSys) const
{
double tau = calcTau();
return tSys * sqrt(1.0/(m_settings.m_rfBandwidth * tau) + m_settings.m_gainVariation * m_settings.m_gainVariation);
}
double RadioAstronomyGUI::calcSigmaT(const FFTMeasurement* fft) const
{
double tau = calcTau(fft);
return fft->m_tSys * sqrt(1.0/(fft->m_rfBandwidth * tau) + m_settings.m_gainVariation * m_settings.m_gainVariation);
}
// Calculate variations in flux due to random noise fluctuations, including receiver gain variations
// Minimum flux we can reliably detect will be ~5x this
double RadioAstronomyGUI::calcSigmaS(double tSys) const
{
double omegaA = hpbwToSteradians(m_beamWidth);
double lambda = Astronomy::m_speedOfLight / (double)m_centerFrequency;
double flux = 2.0 * Astronomy::m_boltzmann * tSys * omegaA / (lambda * lambda); // Should we use Aeff here instead?
double tau = calcTau();
double sigma = flux * sqrt(1.0/(m_settings.m_rfBandwidth * tau) + m_settings.m_gainVariation * m_settings.m_gainVariation);
return Units::wattsPerMetrePerHertzToJansky(sigma);
}
double RadioAstronomyGUI::calcSigmaS(const FFTMeasurement* fft) const
{
double omegaA = fft->m_omegaA;
double lambda = Astronomy::m_speedOfLight / (double)fft->m_centerFrequency;
double flux = 2.0 * Astronomy::m_boltzmann * fft->m_tSys * omegaA / (lambda * lambda); // Should we use Aeff here instead?
double tau = calcTau(fft);
double sigma = flux * sqrt(1.0/(fft->m_rfBandwidth * tau) + m_settings.m_gainVariation * m_settings.m_gainVariation);
return Units::wattsPerMetrePerHertzToJansky(sigma);
}
// Calculate and display how long a single measurement will take
void RadioAstronomyGUI::updateIntegrationTime()
{
double secs = calcTau();
if (secs >= 60) {
ui->integrationTime->setText(QString("%1m").arg(secs/60, 0, 'f', 1));
} else {
ui->integrationTime->setText(QString("%1s").arg(secs, 0, 'f', 1));
}
updateTSys0();
}
// Limit bandwidth to be less than sample rate
void RadioAstronomyGUI::updateBWLimits()
{
qint64 sr = (qint64) m_settings.m_sampleRate;
int digits = ceil(log10(sr+1));
ui->rfBW->setValueRange(true, digits, 1000, sr);
}
void RadioAstronomyGUI::on_deltaFrequency_changed(qint64 value)
{
m_channelMarker.setCenterFrequency(value);
m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
updateAbsoluteCenterFrequency();
applySettings();
}
void RadioAstronomyGUI::on_sampleRate_changed(qint64 value)
{
float sr = value;
m_settings.m_sampleRate = sr;
updateBWLimits();
updateIntegrationTime();
applySettings();
}
void RadioAstronomyGUI::on_rfBW_changed(qint64 value)
{
float bw = value;
m_channelMarker.setBandwidth(bw);
m_settings.m_rfBandwidth = bw;
applySettings();
}
void RadioAstronomyGUI::on_integration_changed(qint64 value)
{
m_settings.m_integration = value;
updateIntegrationTime();
applySettings();
}
void RadioAstronomyGUI::on_recalibrate_toggled(bool checked)
{
m_settings.m_recalibrate = checked;
applySettings();
if (checked) {
recalibrate();
}
}
void RadioAstronomyGUI::on_showCalSettings_clicked()
{
RadioAstronomyCalibrationDialog dialog(&m_settings);
if (dialog.exec() == QDialog::Accepted) {
applySettings();
}
}
// Start hot calibration
void RadioAstronomyGUI::on_startCalHot_clicked()
{
if (ui->startStop->isChecked()) {
ui->startStop->click();
}
m_radioAstronomy->getInputMessageQueue()->push(RadioAstronomy::MsgStartCal::create(true));
ui->startCalHot->setStyleSheet("QToolButton { background-color : green; }");
}
// Start cold calibration
void RadioAstronomyGUI::on_startCalCold_clicked()
{
if (ui->startStop->isChecked()) {
ui->startStop->click();
}
m_radioAstronomy->getInputMessageQueue()->push(RadioAstronomy::MsgStartCal::create(false));
ui->startCalCold->setStyleSheet("QToolButton { background-color : green; }");
}
// Clear all measurements (but not calibration data)
void RadioAstronomyGUI::clearData()
{
ui->powerTable->setRowCount(0);
m_powerSeries->clear();
m_powerPeakSeries->clear();
m_powerMarkerSeries->clear();
m_powerTsys0Series->clear();
m_powerFilteredSeries->clear();
m_airTemps.clear();
for (int i = 0; i < RADIOASTRONOMY_SENSORS; i++) {
m_sensors[i].clear();
}
for (int row = 0; row < POWER_MARKER_ROWS; row++)
{
for (int col = POWER_MARKER_COL_DATE; col <= POWER_MARKER_COL_DELTA_TO; col++)
{
ui->powerMarkerTable->item(row, col)->setText("");
}
}
m_powerM1Valid = false;
m_powerM2Valid = false;
qDeleteAll(m_fftMeasurements);
m_fftMeasurements.clear();
m_fftSeries->clear();
m_fftPeakSeries->clear();
m_fftMarkerSeries->clear();
for (int row = 0; row < SPECTRUM_MARKER_ROWS; row++)
{
for (int col = SPECTRUM_MARKER_COL_FREQ; col <= SPECTRUM_MARKER_COL_D; col++)
{
ui->spectrumMarkerTable->item(row, col)->setText("");
}
}
m_spectrumM1Valid = false;
m_spectrumM2Valid = false;
clearLoSMarker("Max");
clearLoSMarker("M1");
clearLoSMarker("M2");
ui->spectrumIndex->setRange(0, 0);
ui->spectrumDateTime->setDateTime(QDateTime::currentDateTime());
ui->powerMean->setText("");
ui->powerRMS->setText("");
ui->powerSD->setText("");
plotPowerVsTimeChart(); // To ensure min/max/peaks are reset
create2DImage();
plotPowerChart();
ui->measurementProgress->setValue(0);
ui->sweepStatus->setText("");
}
// Clear calibration data
void RadioAstronomyGUI::clearCalData()
{
delete m_calHot;
delete m_calCold;
delete m_calG;
m_calHot = nullptr;
m_calCold = nullptr;
m_calG = nullptr;
m_calHotSeries->clear();
m_calColdSeries->clear();
ui->calAvgDiff->setText("");
}
// deleteRowsComplete should be called after all rows are deleted
// Returns if the row being deleted is the currently displayed FFT
bool RadioAstronomyGUI::deleteRow(int row)
{
ui->powerTable->removeRow(row);
delete m_fftMeasurements[row];
m_fftMeasurements.removeAt(row);
return row == ui->spectrumIndex->value();
}
// Updates GUI after rows have been deleted
void RadioAstronomyGUI::deleteRowsComplete(bool deletedCurrent, int next)
{
if (m_fftMeasurements.size() == 0)
{
clearData();
}
else
{
if (deletedCurrent) {
ui->spectrumIndex->setValue(next);
}
plotPowerChart();
powerAutoscale();
}
}
// Calculate average difference in hot and cold cal data - so we can easily validate results
void RadioAstronomyGUI::calcCalAvgDiff()
{
if ((m_calHot && m_calCold) && (m_calHot->m_fftSize == m_calCold->m_fftSize))
{
Real sum = 0.0f;
for (int i = 0; i < m_calHot->m_fftSize; i++) {
sum += CalcDb::dbPower(m_calHot->m_fftData[i]) - CalcDb::dbPower(m_calCold->m_fftData[i]);
}
Real avg = sum / m_calHot->m_fftSize;
ui->calAvgDiff->setText(QString::number(avg, 'f', 1));
}
else
{
ui->calAvgDiff->setText("");
}
}
void RadioAstronomyGUI::calcCalibrationScaleFactors()
{
if (m_calHot)
{
delete[] m_calG;
m_calG = new double[m_calHot->m_fftSize];
// Calculate scaling factors from FFT mag to temperature
// FIXME: This assumes cal hot is fixed reference temp - E.g. 50Ohm term
for (int i = 0; i < m_calHot->m_fftSize; i++) {
m_calG[i] = (m_settings.m_tCalHot + m_settings.m_tempRX) / m_calHot->m_fftData[i];
}
}
}
void RadioAstronomyGUI::calibrate()
{
if (m_calHotSeries)
{
calcCalibrationScaleFactors();
calcCalTrx();
calcCalTsp();
if (m_settings.m_recalibrate)
{
// Apply new calibration to existing measurements
recalibrate();
}
}
}
// Apply calibration to all existing measurements
void RadioAstronomyGUI::recalibrate()
{
for (int i = 0; i < m_fftMeasurements.size(); i++)
{
FFTMeasurement* fft = m_fftMeasurements[i];
// Recalibrate
calcFFTTemperatures(fft);
calcFFTTotalTemperature(fft);
// Update table
if (fft->m_tSys != 0.0f) {
ui->powerTable->item(i, POWER_COL_POWER_DBM)->setData(Qt::DisplayRole, fft->m_totalPowerdBm);
}
if (fft->m_temp) {
updatePowerColumns(i, fft);
}
}
// Update charts
plotFFTMeasurement();
plotPowerChart();
}
// Calculate Trx using Y-factor method
void RadioAstronomyGUI::calcCalTrx()
{
if ((m_calHot && m_calCold) && (m_calHot->m_fftSize == m_calCold->m_fftSize))
{
// y=Ph/Pc
double sumH = 0.0;
double sumC = 0.0;
for (int i = 0; i < m_calHot->m_fftSize; i++)
{
sumH += m_calHot->m_fftData[i];
sumC += m_calCold->m_fftData[i];
}
double y = sumH/sumC;
// Use y to calculate Trx, which should be the same for both calibration points
double Trx = (m_settings.m_tCalHot - (m_settings.m_tCalCold * y)) / (y - 1.0);
ui->calYFactor->setText(QString::number(y, 'f', 2));
ui->calTrx->setText(QString::number(Trx, 'f', 1));
}
else
{
ui->calYFactor->setText("");
ui->calTrx->setText("");
}
}
// Estimate spillover temperature (This is typically very Az/El depenedent as ground noise will vary)
void RadioAstronomyGUI::calcCalTsp()
{
if (!ui->calTrx->text().isEmpty() && !ui->calTsky->text().isEmpty() && !ui->calYFactor->text().isEmpty())
{
double Trx = ui->calTrx->text().toDouble();
double Tsky = ui->calTsky->text().toDouble();
double y = ui->calYFactor->text().toDouble();
double atmosphericAbsorbtion = std::exp(-m_settings.m_zenithOpacity/cos(Units::degreesToRadians(90.0f - m_settings.m_elevation)));
double Tsp = (m_settings.m_tCalHot + Trx) / y - (Tsky*atmosphericAbsorbtion) - m_settings.m_tempAtm - Trx;
ui->calTsp->setText(QString::number(Tsp, 'f', 1));
}
else
{
ui->calTsp->setText("");
}
}
void RadioAstronomyGUI::on_clearData_clicked()
{
clearData();
}
void RadioAstronomyGUI::on_clearCal_clicked()
{
clearCalData();
}
// Save power data in table to a CSV file
void RadioAstronomyGUI::on_savePowerData_clicked()
{
// Get filename to save to
QFileDialog fileDialog(nullptr, "Select file to save data to", "", "*.csv");
fileDialog.setAcceptMode(QFileDialog::AcceptSave);
if (fileDialog.exec())
{
QStringList fileNames = fileDialog.selectedFiles();
if (fileNames.size() > 0)
{
QFile file(fileNames[0]);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(this, "Radio Astronomy", QString("Failed to open file %1").arg(fileNames[0]));
return;
}
QTextStream out(&file);
// Create a CSV file from the values in the table
for (int i = 0; i < ui->powerTable->horizontalHeader()->count(); i++)
{
QString text = ui->powerTable->horizontalHeaderItem(i)->text();
out << text << ",";
}
out << "\n";
for (int i = 0; i < ui->powerTable->rowCount(); i++)
{
for (int j = 0; j < ui->powerTable->horizontalHeader()->count(); j++)
{
out << ui->powerTable->item(i,j)->data(Qt::DisplayRole).toString() << ",";
}
out << "\n";
}
}
}
}
// Create a hash mapping from column name to array index
QHash<QString,int> RadioAstronomyGUI::csvHeadersToHash(QStringList cols)
{
QHash<QString,int> hash;
for (int i = 0; i < cols.size(); i++) {
hash.insert(cols[i], i);
}
return hash;
}
// Get data from column with given name, if available
QString RadioAstronomyGUI::csvData(QHash<QString,int> hash, QStringList cols, QString col)
{
QString s;
if (hash.contains(col))
{
int idx = hash[col];
if (idx < cols.size()) {
s = cols[idx];
}
}
return s;
}
bool RadioAstronomyGUI::hasNeededFFTData(QHash<QString,int> hash)
{
return hash.contains("FFT Size") && hash.contains("Data");
}
// Write FFTMeasurement to a stream
void RadioAstronomyGUI::saveFFT(QTextStream& out, const FFTMeasurement* fft)
{
out << fft->m_dateTime.toString();
out << ",";
out << fft->m_centerFrequency;
out << ",";
out << fft->m_sampleRate;
out << ",";
out << fft->m_integration;
out << ",";
out << fft->m_rfBandwidth;
out << ",";
out << fft->m_omegaA;
out << ",";
out << fft->m_omegaS;
out << ",";
out << fft->m_totalPower;
out << ",";
out << fft->m_totalPowerdBFS;
out << ",";
out << fft->m_totalPowerdBm;
out << ",";
out << fft->m_totalPowerWatts;
out << ",";
out << fft->m_tSys;
out << ",";
out << fft->m_tSys0;
out << ",";
out << fft->m_tSource;
out << ",";
out << fft->m_flux;
out << ",";
out << fft->m_sigmaT;
out << ",";
out << fft->m_sigmaS;
out << ",";
out << fft->m_tempMin;
out << ",";
out << fft->m_baseline;
out << ",";
out << fft->m_ra;
out << ",";
out << fft->m_dec;
out << ",";
out << fft->m_azimuth;
out << ",";
out << fft->m_elevation;
out << ",";
out << fft->m_l;
out << ",";
out << fft->m_b;
out << ",";
out << fft->m_vBCRS;
out << ",";
out << fft->m_vLSR;
out << ",";
out << fft->m_solarFlux;
out << ",";
out << fft->m_airTemp;
out << ",";
out << fft->m_skyTemp;
out << ",";
out << fft->m_sensor[0];
out << ",";
out << fft->m_sensor[1];
out << ",";
out << fft->m_fftSize;
out << ",";
for (int j = 0; j < fft->m_fftSize; j++)
{
out << fft->m_fftData[j];
out << ",";
}
if (fft->m_snr)
{
for (int j = 0; j < fft->m_fftSize; j++)
{
out << fft->m_snr[j];
out << ",";
}
}
if (fft->m_temp)
{
for (int j = 0; j < fft->m_fftSize; j++)
{
out << fft->m_temp[j];
out << ",";
}
}
out << "\n";
}
// Create a FFTMeasurement from data read from CSV file
RadioAstronomyGUI::FFTMeasurement* RadioAstronomyGUI::loadFFT(QHash<QString,int> hash, QStringList cols)
{
int fftSize = csvData(hash, cols, "FFT Size").toInt();
int fftDataIdx = hash["Data"];
if ((fftSize > 0) && (cols.size() >= fftDataIdx + fftSize))
{
FFTMeasurement* fft = new FFTMeasurement();
fft->m_dateTime = QDateTime::fromString(csvData(hash, cols, "Date Time"));
fft->m_centerFrequency = csvData(hash, cols, "Centre Freq").toLongLong();
fft->m_sampleRate = csvData(hash, cols, "Sample Rate").toInt();
fft->m_integration = csvData(hash, cols, "Integration").toInt();
fft->m_rfBandwidth = csvData(hash, cols, "Bandwidth").toInt();
fft->m_omegaA = csvData(hash, cols, "OmegaA").toFloat();
fft->m_omegaS = csvData(hash, cols, "OmegaS").toFloat();
fft->m_fftSize = fftSize;
fft->m_fftData = new Real[fftSize];
fft->m_db = new Real[fftSize];
for (int i = 0; i < fftSize; i++)
{
fft->m_fftData[i] = cols[fftDataIdx+i].toFloat();
fft->m_db[i] = (Real)CalcDb::dbPower(fft->m_fftData[i]);
}
if (cols.size() >= fftDataIdx + 2*fftSize)
{
fft->m_snr = new Real[fftSize];
for (int i = 0; i < fftSize; i++) {
fft->m_snr[i] = cols[fftDataIdx+fftSize+i].toFloat();
}
if (cols.size() >= fftDataIdx + 3*fftSize)
{
fft->m_temp = new Real[fftSize];
for (int i = 0; i < fftSize; i++) {
fft->m_temp[i] = cols[fftDataIdx+2*fftSize+i].toFloat();
}
}
}
fft->m_totalPower = csvData(hash, cols, "Power (FFT)").toFloat();
fft->m_totalPowerdBFS = csvData(hash, cols, "Power (dBFS)").toFloat();
fft->m_totalPowerdBm = csvData(hash, cols, "Power (dBm)").toFloat();
fft->m_totalPowerWatts = csvData(hash, cols, "Power (Watts)").toFloat();
fft->m_tSys = csvData(hash, cols, "Tsys").toFloat();
fft->m_tSys0 = csvData(hash, cols, "Tsys0").toFloat();
fft->m_tSource = csvData(hash, cols, "Tsource").toFloat();
fft->m_flux = csvData(hash, cols, "Sv").toFloat();
fft->m_sigmaT = csvData(hash, cols, "SigmaTsys").toFloat();
fft->m_sigmaS = csvData(hash, cols, "SigmaSsys").toFloat();
fft->m_tempMin = csvData(hash, cols, "Min Temp").toFloat();
fft->m_baseline = (RadioAstronomySettings::SpectrumBaseline)csvData(hash, cols, "Baseline").toInt();
fft->m_ra = csvData(hash, cols, "RA").toFloat();
fft->m_dec = csvData(hash, cols, "Dec").toFloat();
fft->m_azimuth = csvData(hash, cols, "Azimuth").toFloat();
fft->m_elevation = csvData(hash, cols, "Elevation").toFloat();
fft->m_l = csvData(hash, cols, "l").toFloat();
fft->m_b = csvData(hash, cols, "b").toFloat();
if ((fft->m_ra != 0.0) || (fft->m_dec != 0.0) || (fft->m_azimuth != 0.0) || (fft->m_elevation != 0.0) || (fft->m_l != 0.0) || (fft->m_b != 0.0)) {
fft->m_coordsValid = true;
}
fft->m_vBCRS = csvData(hash, cols, "vBCRS").toFloat();
fft->m_vLSR = csvData(hash, cols, "vLSR").toFloat();
fft->m_solarFlux = csvData(hash, cols, "Solar Flux").toFloat();
fft->m_airTemp = csvData(hash, cols, "Air Temp").toFloat();
fft->m_skyTemp = csvData(hash, cols, "Sky Temp").toFloat();
fft->m_sensor[0] = csvData(hash, cols, "Sensor 1").toFloat();
fft->m_sensor[1] = csvData(hash, cols, "Sensor 2").toFloat();
if (fft->m_rfBandwidth == 0)
{
fft->m_rfBandwidth = 0.9 * fft->m_sampleRate; // Older files don't have this column and we need a value for min
calcFFTTotalPower(fft);
/*calcFFTMinTemperature(fft);
calcFFTTotalTemperature(fft);*/
}
return fft;
}
else
{
return nullptr;
}
}
void RadioAstronomyGUI::on_saveSpectrumData_clicked()
{
// Get filename to save to
QFileDialog fileDialog(nullptr, "Select file to save data to", "", "*.csv");
fileDialog.setAcceptMode(QFileDialog::AcceptSave);
if (fileDialog.exec())
{
QStringList fileNames = fileDialog.selectedFiles();
if (fileNames.size() > 0)
{
QFile file(fileNames[0]);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(this, "Radio Astronomy", QString("Failed to open file %1").arg(fileNames[0]));
return;
}
QTextStream out(&file);
if (ui->spectrumChartSelect->currentIndex() == 0)
{
// Create a CSV file for all the spectrum data
out << "Date Time,Centre Freq,Sample Rate,Integration,Bandwidth,OmegaA,OmegaS,Power (FFT),Power (dBFS),Power (dBm),Power (Watts),Tsys,Tsys0,Tsource,Sv,SigmaTsys,SigmaSsys,Min Temp,Baseline,RA,Dec,Azimuth,Elevation,l,b,vBCRS,vLSR,Solar Flux,Air Temp,Sky Temp,Sensor 1,Sensor 2,FFT Size,Data\n";
for (int i = 0; i < m_fftMeasurements.size(); i++) {
saveFFT(out, m_fftMeasurements[i]);
}
}
else
{
// Create a CSV file for calibration data
out << "Cal,Cal Temp,Date Time,Centre Freq,Sample Rate,Integration,Bandwidth,OmegaA,OmegaS,Power (FFT),Power (dBFS),Power (dBm),Power (Watts),Tsys,Tsys0,Tsource,Sv,SigmaTsys,SigmaSsys,Min Temp,Baseline,RA,Dec,Azimuth,Elevation,l,b,vBCRS,vLSR,Solar Flux,Air Temp,Sky Temp,Sensor 1,Sensor 2,FFT Size,Data\n";
if (m_calHot)
{
out << "Hot,";
out << m_settings.m_tCalHot;
out << ",";
saveFFT(out, m_calHot);
}
if (m_calCold)
{
out << "Cold,";
out << m_settings.m_tCalCold;
out << ",";
saveFFT(out, m_calCold);
}
}
}
}
}
void RadioAstronomyGUI::on_loadSpectrumData_clicked()
{
// Get filename to load from
QFileDialog fileDialog(nullptr, "Select file to load data from", "", "*.csv");
fileDialog.setAcceptMode(QFileDialog::AcceptOpen);
if (fileDialog.exec())
{
QStringList fileNames = fileDialog.selectedFiles();
if (fileNames.size() > 0)
{
QFile file(fileNames[0]);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QMessageBox::critical(this, "Radio Astronomy", QString("Failed to open file %1").arg(fileNames[0]));
return;
}
// Get column names
QTextStream in(&file);
QString header = in.readLine();
QStringList colNames = header.split(",");
QHash<QString,int> hash = csvHeadersToHash(colNames);
if (ui->spectrumChartSelect->currentIndex() == 0)
{
// Load data from CSV file
if (hasNeededFFTData(hash))
{
// Remove old data - we could support multiple series for comparison
clearData();
// Read in FFT data from file
ui->spectrumIndex->blockSignals(true); // Prevent every spectrum from being displayed
while (!in.atEnd())
{
QString row = in.readLine();
QStringList cols = row.split(",");
FFTMeasurement* fft = loadFFT(hash, cols);
if (fft) {
addFFT(fft, true);
}
}
ui->spectrumIndex->blockSignals(false);
// Add data from FFT to sensor measurements
for (int i = 0; i < m_fftMeasurements.size(); i++)
{
SensorMeasurement* sm;
sm = new SensorMeasurement(m_fftMeasurements[i]->m_dateTime, m_fftMeasurements[i]->m_airTemp);
m_airTemps.append(sm);
for (int j = 0; j < RADIOASTRONOMY_SENSORS; j++)
{
sm = new SensorMeasurement(m_fftMeasurements[i]->m_dateTime, m_fftMeasurements[i]->m_sensor[j]);
m_sensors[j].append(sm);
}
}
// If we're loading data from scratch, autoscale both axis
if ((ui->spectrumCenterFreq->value() == 0.0) || m_settings.m_spectrumAutoscale)
{
on_spectrumAutoscaleY_clicked();
on_spectrumAutoscaleX_clicked();
}
// Ensure both charts are redrawn fully, as we've disabled some updates/calcs during load
on_spectrumIndex_valueChanged(m_fftMeasurements.size() - 1); // Don't call setValue, as it already has this value
plotPowerChart();
// As signals were blocked above, power axis may not match up with GUI. Manually update
// Just calling autoscale will not work, as the GUI values may not change
on_powerStartTime_dateTimeChanged(ui->powerStartTime->dateTime());
on_powerEndTime_dateTimeChanged(ui->powerEndTime->dateTime());
on_powerRange_valueChanged(m_settings.m_powerRange);
on_powerReference_valueChanged(m_settings.m_powerReference);
}
}
else
{
// Load calibration data from CSV file
if (hasNeededFFTData(hash) && hash.contains("Cal"))
{
while (!in.atEnd())
{
QString row = in.readLine();
QStringList cols = row.split(",");
QString calName = csvData(hash, cols, "Cal");
FFTMeasurement** calp = nullptr;
FFTMeasurement* cal = nullptr;
if (calName == "Hot") {
calp = &m_calHot;
} else if (calName == "Cold") {
calp = &m_calCold;
} else {
qDebug() << "RadioAstronomyGUI::on_loadSpectrumData_clicked: Skipping unknown calibration " << calName;
}
if (calp)
{
cal = loadFFT(hash, cols);
if (cal)
{
delete *calp;
*calp = cal;
qDebug() << "RadioAstronomyGUI::on_loadSpectrumData_clicked: Loaded calibration " << calName;
if (calName == "Cold") {
ui->calTsky->setText(QString::number(cal->m_skyTemp, 'f', 1));
}
QString calTempString = csvData(hash, cols, "Cal Temp");
bool ok;
double calTemp = calTempString.toDouble(&ok);
if (ok)
{
if (calName == "Cold")
{
ui->tCalColdSelect->setCurrentIndex(0);
ui->tCalCold->setValue(calTemp);
}
else
{
ui->tCalHotSelect->setCurrentIndex(0);
ui->tCalHot->setValue(calTemp);
}
}
}
}
}
calcCalAvgDiff();
calibrate();
plotCalMeasurements();
}
else
{
QMessageBox::critical(this, "Radio Astronomy", QString("Missing required columns in file %1").arg(fileNames[0]));
return;
}
}
}
}
}
void RadioAstronomyGUI::on_powerTable_cellDoubleClicked(int row, int column)
{
if ((column >= POWER_COL_RA) && (column >= POWER_COL_EL))
{
// Display target in Star Tracker
QList<ObjectPipe*> starTrackerPipes;
MainCore::instance()->getMessagePipes().getMessagePipes(this, "startracker.display", starTrackerPipes);
for (const auto& pipe : starTrackerPipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
SWGSDRangel::SWGStarTrackerDisplaySettings *swgSettings = new SWGSDRangel::SWGStarTrackerDisplaySettings();
QDateTime dt(ui->powerTable->item(row, POWER_COL_DATE)->data(Qt::DisplayRole).toDate(),
ui->powerTable->item(row, POWER_COL_TIME)->data(Qt::DisplayRole).toTime());
swgSettings->setDateTime(new QString(dt.toString(Qt::ISODateWithMs)));
swgSettings->setAzimuth(ui->powerTable->item(row, POWER_COL_AZ)->data(Qt::DisplayRole).toFloat());
swgSettings->setElevation(ui->powerTable->item(row, POWER_COL_EL)->data(Qt::DisplayRole).toFloat());
messageQueue->push(MainCore::MsgStarTrackerDisplaySettings::create(m_radioAstronomy, swgSettings));
}
}
else
{
// Display in Spectrometer
ui->spectrumIndex->setValue(row);
}
}
void RadioAstronomyGUI::onWidgetRolled(QWidget* widget, bool rollDown)
{
(void) widget;
(void) rollDown;
RollupContents *rollupContents = getRollupContents();
if (rollupContents->hasExpandableWidgets()) {
setSizePolicy(sizePolicy().horizontalPolicy(), QSizePolicy::Expanding);
} else {
setSizePolicy(sizePolicy().horizontalPolicy(), QSizePolicy::Fixed);
}
int h = rollupContents->height() + getAdditionalHeight();
resize(width(), h);
rollupContents->saveState(m_rollupState);
applySettings();
}
void RadioAstronomyGUI::onMenuDialogCalled(const QPoint &p)
{
if (m_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_radioAstronomy->getNumberOfDeviceStreams());
dialog.setStreamIndex(m_settings.m_streamIndex);
}
dialog.move(p);
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();
}
RadioAstronomyGUI::RadioAstronomyGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) :
ChannelGUI(parent),
ui(new Ui::RadioAstronomyGUI),
m_pluginAPI(pluginAPI),
m_deviceUISet(deviceUISet),
m_channelMarker(this),
m_deviceCenterFrequency(0),
m_doApplySettings(true),
m_basebandSampleRate(0),
m_centerFrequency(0),
m_tickCount(0),
m_powerChart(nullptr),
m_powerSeries(nullptr),
m_powerXAxis(nullptr),
m_powerYAxis(nullptr),
m_powerPeakSeries(nullptr),
m_powerMarkerSeries(nullptr),
m_powerTsys0Series(nullptr),
m_powerGaussianSeries(nullptr),
m_powerFilteredSeries(nullptr),
m_powerPeakValid(false),
m_2DChart(nullptr),
m_2DXAxis(nullptr),
m_2DYAxis(nullptr),
m_2DMapIntensity(nullptr),
m_sweepIndex(0),
m_calChart(nullptr),
m_calXAxis(nullptr),
m_calYAxis(nullptr),
m_calHotSeries(nullptr),
m_calColdSeries(nullptr),
m_calHot(nullptr),
m_calCold(nullptr),
m_calG(nullptr),
m_fftChart(nullptr),
m_fftSeries(nullptr),
m_fftHlineSeries(nullptr),
m_fftPeakSeries(nullptr),
m_fftMarkerSeries(nullptr),
m_fftGaussianSeries(nullptr),
m_fftLABSeries(nullptr),
m_fftXAxis(nullptr),
m_fftYAxis(nullptr),
m_fftDopplerAxis(nullptr),
m_powerM1Valid(false),
m_powerM2Valid(false),
m_spectrumM1Valid(false),
m_spectrumM2Valid(false),
m_coordsValid(false),
m_ra(0.0f),
m_dec(0.0f),
m_azimuth(0.0f),
m_elevation(0.0f),
m_l(0.0f),
m_b(0.0f),
m_vBCRS(0.0f),
m_vLSR(0.0f),
m_solarFlux(0.0f),
m_beamWidth(5.6f),
m_lLAB(0.0f),
m_bLAB(0.0f),
m_downloadingLAB(false),
m_window(nullptr),
m_windowSorted(nullptr),
m_windowIdx(0),
m_windowCount(0)
{
qDebug("RadioAstronomyGUI::RadioAstronomyGUI");
setAttribute(Qt::WA_DeleteOnClose, true);
m_helpURL = "plugins/channelrx/radioastronomy/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_radioAstronomy = reinterpret_cast<RadioAstronomy*>(rxChannel);
m_radioAstronomy->setMessageQueueToGUI(getInputMessageQueue());
connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms
m_networkManager = new QNetworkAccessManager();
QObject::connect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&RadioAstronomyGUI::networkManagerFinished
);
connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &RadioAstronomyGUI::downloadFinished);
ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03)));
ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold));
ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999);
// Need to setValue before calling setValueRange, otherwise valueChanged is called
// overwriting the default settings (could also blockSignals)
// Also, set bandwidth before sampleRate
ui->rfBW->setColorMapper(ColorMapper(ColorMapper::GrayGold));
ui->rfBW->setValue(m_settings.m_rfBandwidth);
ui->rfBW->setValueRange(true, 8, 1000, 99999999);
ui->sampleRate->setColorMapper(ColorMapper(ColorMapper::GrayGold));
ui->sampleRate->setValue(m_settings.m_sampleRate);
ui->sampleRate->setValueRange(true, 8, 100000, 99999999);
ui->integration->setColorMapper(ColorMapper(ColorMapper::GrayGold));
ui->integration->setValue(m_settings.m_integration);
ui->integration->setValueRange(true, 7, 1, 99999999);
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("Radio Astronomy");
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.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()));
// Resize the table using dummy data
resizePowerTable();
// Allow user to reorder columns
ui->powerTable->horizontalHeader()->setSectionsMovable(true);
// Allow user to sort table by clicking on headers
ui->powerTable->setSortingEnabled(true);
// Add context menu to allow hiding/showing of columns
powerTableMenu = new QMenu(ui->powerTable);
for (int i = 0; i < ui->powerTable->horizontalHeader()->count(); i++)
{
QString text = ui->powerTable->horizontalHeaderItem(i)->text();
powerTableMenu->addAction(createCheckableItem(text, i, true, SLOT(powerTableColumnSelectMenuChecked())));
}
ui->powerTable->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->powerTable->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(powerTableColumnSelectMenu(QPoint)));
// Get signals when columns change
connect(ui->powerTable->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(powerTable_sectionMoved(int, int, int)));
connect(ui->powerTable->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(powerTable_sectionResized(int, int, int)));
ui->powerTable->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->powerTable, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customContextMenuRequested(QPoint)));
ui->powerTable->setItemDelegateForColumn(POWER_COL_TIME, new TimeDelegate());
//ui->powerTable->setItemDelegateForColumn(POWER_COL_POWER, new DecimalDelegate(6));
ui->powerTable->setItemDelegateForColumn(POWER_COL_POWER_DB, new DecimalDelegate(1));
ui->powerTable->setItemDelegateForColumn(POWER_COL_POWER_DBM, new DecimalDelegate(1));
ui->powerTable->setItemDelegateForColumn(POWER_COL_TSYS, new DecimalDelegate(0));
ui->powerTable->setItemDelegateForColumn(POWER_COL_TSYS0, new DecimalDelegate(0));
ui->powerTable->setItemDelegateForColumn(POWER_COL_TSOURCE, new DecimalDelegate(0));
ui->powerTable->setItemDelegateForColumn(POWER_COL_TB, new DecimalDelegate(0));
ui->powerTable->setItemDelegateForColumn(POWER_COL_TSKY, new DecimalDelegate(0));
ui->powerTable->setItemDelegateForColumn(POWER_COL_FLUX, new DecimalDelegate(2));
ui->powerTable->setItemDelegateForColumn(POWER_COL_SIGMA_T, new DecimalDelegate(2));
ui->powerTable->setItemDelegateForColumn(POWER_COL_SIGMA_S, new DecimalDelegate(1));
ui->powerTable->setItemDelegateForColumn(POWER_COL_RA, new HMSDelegate());
ui->powerTable->setItemDelegateForColumn(POWER_COL_DEC, new DMSDelegate());
ui->powerTable->setItemDelegateForColumn(POWER_COL_GAL_LAT, new DecimalDelegate(0));
ui->powerTable->setItemDelegateForColumn(POWER_COL_GAL_LON, new DecimalDelegate(0));
ui->powerTable->setItemDelegateForColumn(POWER_COL_AZ, new DecimalDelegate(0));
ui->powerTable->setItemDelegateForColumn(POWER_COL_EL, new DecimalDelegate(0));
ui->powerTable->setItemDelegateForColumn(POWER_COL_VBCRS, new DecimalDelegate(1));
ui->powerTable->setItemDelegateForColumn(POWER_COL_VLSR, new DecimalDelegate(1));
ui->powerTable->setItemDelegateForColumn(POWER_COL_AIR_TEMP, new DecimalDelegate(1));
resizeSpectrumMarkerTable();
ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_FREQ, new DecimalDelegate(6));
ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_VALUE, new DecimalDelegate(1));
ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_DELTA_X, new DecimalDelegate(6));
ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_DELTA_Y, new DecimalDelegate(1));
ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_VR, new DecimalDelegate(2));
ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_R, new DecimalDelegate(1));
ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_D, new DecimalDelegate(1));
ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_R_MIN, new DecimalDelegate(1));
ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_V, new DecimalDelegate(1));
// Create blank marker table
ui->spectrumMarkerTable->setRowCount(SPECTRUM_MARKER_ROWS); // 1 peak and two markers
for (int row = 0; row < SPECTRUM_MARKER_ROWS; row++)
{
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_NAME, new QTableWidgetItem());
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_FREQ, new QTableWidgetItem());
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_VALUE, new QTableWidgetItem());
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_DELTA_X, new QTableWidgetItem());
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_DELTA_Y, new QTableWidgetItem());
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_DELTA_TO, new QTableWidgetItem());
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_VR, new QTableWidgetItem());
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_R, new QTableWidgetItem());
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_D, new QTableWidgetItem());
// It seems clearing Qt::ItemIsUserCheckable doesn't remove the checkbox, so once set, we always have it
QTableWidgetItem* item = new QTableWidgetItem();
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(Qt::Unchecked);
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_PLOT_MAX, item);
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_R_MIN, new QTableWidgetItem());
ui->spectrumMarkerTable->setItem(row, SPECTRUM_MARKER_COL_V, new QTableWidgetItem());
}
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_PEAK, SPECTRUM_MARKER_COL_NAME)->setText("Max");
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_M1, SPECTRUM_MARKER_COL_NAME)->setText("M1");
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_M2, SPECTRUM_MARKER_COL_NAME)->setText("M2");
connect(ui->spectrumMarkerTable, &QTableWidget::itemChanged, this, &RadioAstronomyGUI::spectrumMarkerTableItemChanged);
resizePowerMarkerTable();
ui->powerMarkerTable->setItemDelegateForColumn(POWER_MARKER_COL_TIME, new TimeDelegate());
ui->powerMarkerTable->setItemDelegateForColumn(POWER_MARKER_COL_VALUE, new DecimalDelegate(1));
ui->powerMarkerTable->setItemDelegateForColumn(POWER_MARKER_COL_DELTA_X, new TimeDeltaDelegate());
ui->powerMarkerTable->setItemDelegateForColumn(POWER_MARKER_COL_DELTA_Y, new DecimalDelegate(1));
// Create blank marker table
ui->powerMarkerTable->setRowCount(POWER_MARKER_ROWS); // 1 peak and two markers
for (int row = 0; row < POWER_MARKER_ROWS; row++)
{
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_NAME, new QTableWidgetItem());
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_DATE, new QTableWidgetItem());
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_TIME, new QTableWidgetItem());
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_VALUE, new QTableWidgetItem());
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_DELTA_X, new QTableWidgetItem());
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_DELTA_Y, new QTableWidgetItem());
ui->powerMarkerTable->setItem(row, POWER_MARKER_COL_DELTA_TO, new QTableWidgetItem());
}
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MAX, POWER_MARKER_COL_NAME)->setText("Max");
ui->powerMarkerTable->item(POWER_MARKER_ROW_PEAK_MIN, POWER_MARKER_COL_NAME)->setText("Min");
ui->powerMarkerTable->item(POWER_MARKER_ROW_M1, POWER_MARKER_COL_NAME)->setText("M1");
ui->powerMarkerTable->item(POWER_MARKER_ROW_M2, POWER_MARKER_COL_NAME)->setText("M2");
ui->sweepStartDateTime->setMinimumDateTime(QDateTime::currentDateTime());
ui->spectrumDateTime->setDateTime(QDateTime::currentDateTime());
ui->startStop->setStyleSheet("QToolButton { background-color : blue; }");
displaySettings();
makeUIConnections();
applySettings(true);
create2DImage();
plotCalSpectrum();
plotSpectrum();
plotPowerChart();
}
void RadioAstronomyGUI::customContextMenuRequested(QPoint pos)
{
QTableWidgetItem *item = ui->powerTable->itemAt(pos);
if (item)
{
QMenu* tableContextMenu = new QMenu(ui->powerTable);
connect(tableContextMenu, &QMenu::aboutToHide, tableContextMenu, &QMenu::deleteLater);
// Copy cell contents to clipboard
QAction* copyAction = new QAction("Copy cell", tableContextMenu);
const QString text = item->text();
connect(copyAction, &QAction::triggered, this, [text]()->void {
QClipboard *clipboard = QGuiApplication::clipboard();
clipboard->setText(text);
});
tableContextMenu->addAction(copyAction);
// Delete selected rows
QAction* delAction = new QAction("Delete rows", tableContextMenu);
connect(delAction, &QAction::triggered, this, [this]()->void {
QModelIndexList rowIndexes = ui->powerTable->selectionModel()->selectedRows();
if (rowIndexes.size() > 0)
{
// Delete in reverse row order
std::vector<int> rows;
foreach (auto rowIndex, rowIndexes) {
rows.push_back(rowIndex.row());
}
std::sort(rows.begin(), rows.end(), std::greater<int>());
bool deletedCurrent = false;
int next;
foreach (auto row, rows) {
next = row - 1;
if (deleteRow(row)) {
deletedCurrent = true;
}
}
deleteRowsComplete(deletedCurrent, next);
}
});
tableContextMenu->addAction(delAction);
// Update rows with new Tsys0 and baseline
QAction* updateTSysAction = new QAction(QString("Update Tsys0 / baseline / %1").arg(QChar(937)), tableContextMenu);
connect(updateTSysAction, &QAction::triggered, this, [this]()->void {
QModelIndexList rowIndexes = ui->powerTable->selectionModel()->selectedRows();
if (rowIndexes.size() > 0)
{
foreach (auto rowIndex, rowIndexes)
{
int row = rowIndex.row();
m_fftMeasurements[row]->m_tSys0 = calcTSys0();
m_fftMeasurements[row]->m_baseline = m_settings.m_spectrumBaseline;
m_fftMeasurements[row]->m_omegaA = calcOmegaA();
m_fftMeasurements[row]->m_omegaS = calcOmegaS();
calcFFTTotalTemperature(m_fftMeasurements[row]);
updatePowerColumns(row, m_fftMeasurements[row]);
}
plotFFTMeasurement();
}
});
tableContextMenu->addAction(updateTSysAction);
tableContextMenu->popup(ui->powerTable->viewport()->mapToGlobal(pos));
}
}
RadioAstronomyGUI::~RadioAstronomyGUI()
{
delete ui;
delete m_calHot;
delete m_calCold;
qDeleteAll(m_dataLAB);
m_dataLAB.clear();
delete[] m_2DMapIntensity;
delete[] m_window;
delete[] m_windowSorted;
}
void RadioAstronomyGUI::blockApplySettings(bool block)
{
m_doApplySettings = !block;
}
void RadioAstronomyGUI::applySettings(bool force)
{
if (m_doApplySettings)
{
RadioAstronomy::MsgConfigureRadioAstronomy* message = RadioAstronomy::MsgConfigureRadioAstronomy::create( m_settings, force);
m_radioAstronomy->getInputMessageQueue()->push(message);
}
}
int RadioAstronomyGUI::fftSizeToIndex(int size)
{
switch (size)
{
case 16:
return 0;
case 32:
return 1;
case 64:
return 2;
case 128:
return 3;
case 256:
return 4;
case 512:
return 5;
case 1024:
return 6;
case 2048:
return 7;
case 4096:
return 8;
}
return 0;
}
void RadioAstronomyGUI::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());
float rfBW = m_settings.m_rfBandwidth; // Save value, as it may be corrupted when setting sampleRate
ui->sampleRate->setValue(m_settings.m_sampleRate);
ui->rfBW->setValue(rfBW);
updateBWLimits();
ui->integration->setValue(m_settings.m_integration);
ui->fftSize->setCurrentIndex(fftSizeToIndex(m_settings.m_fftSize));
ui->fftWindow->setCurrentIndex((int)m_settings.m_fftWindow);
ui->filterFreqs->setText(m_settings.m_filterFreqs);
int idx = ui->starTracker->findText(m_settings.m_starTracker);
if (idx != -1) {
ui->starTracker->setCurrentIndex(idx);
}
idx = ui->rotator->findText(m_settings.m_rotator);
if (idx != -1) {
ui->rotator->setCurrentIndex(idx);
}
ui->tempRXSelect->setCurrentIndex(0);
ui->tempRX->setValue(m_settings.m_tempRX);
ui->tempRXUnitsLabel->setText("K");
ui->tempCMB->setValue(m_settings.m_tempCMB);
ui->tempGal->setValue(m_settings.m_tempGal);
ui->tempGal->setEnabled(!m_settings.m_tempGalLink);
ui->tempGalLink->setChecked(m_settings.m_tempGalLink);
ui->tempSP->setValue(m_settings.m_tempSP);
ui->tempAtm->setValue(m_settings.m_tempAtm);
ui->tempAtm->setEnabled(!m_settings.m_tempAtmLink);
ui->tempAtmLink->setChecked(m_settings.m_tempAtmLink);
ui->tempAir->setValue(m_settings.m_tempAir);
ui->tempAir->setEnabled(!m_settings.m_tempAirLink);
ui->tempAirLink->setChecked(m_settings.m_tempAirLink);
ui->zenithOpacity->setValue(m_settings.m_zenithOpacity);
ui->elevation->setValue(m_settings.m_elevation);
ui->elevation->setEnabled(!m_settings.m_elevationLink);
ui->elevationLink->setChecked(m_settings.m_elevationLink);
ui->gainVariation->setValue(m_settings.m_gainVariation);
ui->sourceType->setCurrentIndex((int)m_settings.m_sourceType);
ui->omegaS->setValue(m_settings.m_omegaS);
ui->omegaSUnits->setCurrentIndex((int)m_settings.m_omegaSUnits);
ui->omegaAUnits->setCurrentIndex((int)m_settings.m_omegaAUnits);
ui->recalibrate->setChecked(m_settings.m_recalibrate);
ui->tCalHot->setValue(m_settings.m_tCalHot);
ui->tCalCold->setValue(m_settings.m_tCalCold);
ui->spectrumAutoscale->setChecked(m_settings.m_spectrumAutoscale);
ui->spectrumReference->setValue(m_settings.m_spectrumReference);
ui->spectrumRange->setValue(m_settings.m_spectrumRange);
FFTMeasurement* fft = currentFFT();
if (fft) {
ui->spectrumCenterFreq->setValue(fft->m_centerFrequency/1e6 + m_settings.m_spectrumCenterFreqOffset);
} else {
ui->spectrumCenterFreq->setValue(m_centerFrequency/1e6 + m_settings.m_spectrumCenterFreqOffset);
}
ui->spectrumSpan->setValue(m_settings.m_spectrumSpan);
ui->spectrumYUnits->setCurrentIndex((int)m_settings.m_spectrumYScale);
ui->spectrumBaseline->setCurrentIndex((int)m_settings.m_spectrumBaseline);
ui->spectrumAutoscaleX->setEnabled(!m_settings.m_spectrumAutoscale);
ui->spectrumAutoscaleY->setEnabled(!m_settings.m_spectrumAutoscale);
ui->spectrumReference->setEnabled(!m_settings.m_spectrumAutoscale);
ui->spectrumRange->setEnabled(!m_settings.m_spectrumAutoscale);
ui->spectrumCenterFreq->setEnabled(!m_settings.m_spectrumAutoscale);
ui->spectrumSpan->setEnabled(!m_settings.m_spectrumAutoscale);
ui->powerAutoscale->setChecked(m_settings.m_powerAutoscale);
ui->powerReference->setValue(m_settings.m_powerReference);
ui->powerRange->setValue(m_settings.m_powerRange);
ui->powerShowPeak->setChecked(m_settings.m_powerPeaks);
if (m_powerPeakSeries)
{
m_powerPeakSeries->setVisible(m_settings.m_powerPeaks);
m_powerChart->legend()->markers(m_powerPeakSeries)[0]->setVisible(false);
}
ui->powerShowMarker->setChecked(m_settings.m_powerMarkers);
if (m_powerMarkerSeries)
{
m_powerMarkerSeries->setVisible(m_settings.m_powerMarkers);
m_powerChart->legend()->markers(m_powerMarkerSeries)[0]->setVisible(false);
}
ui->powerShowAvg->setChecked(m_settings.m_powerAvg);
ui->powerChartAvgWidgets->setVisible(m_settings.m_powerAvg);
ui->powerShowGaussian->setChecked(m_settings.m_powerShowGaussian);
ui->powerGaussianWidgets->setVisible(m_settings.m_powerShowGaussian);
if (m_powerGaussianSeries) {
m_powerGaussianSeries->setVisible(m_settings.m_powerShowGaussian);
}
ui->powerShowLegend->setChecked(m_settings.m_powerLegend);
if (m_powerChart) {
m_powerChart->legend()->setVisible(m_settings.m_powerLegend);
}
ui->powerChartSelect->setCurrentIndex((int)m_settings.m_powerYData);
ui->powerYUnits->setCurrentIndex(powerYUnitsToIndex(m_settings.m_powerYUnits));
ui->powerShowTsys0->setChecked(m_settings.m_powerShowTsys0);
ui->powerShowAirTemp->setChecked(m_settings.m_powerShowAirTemp);
m_airTemps.clicked(m_settings.m_powerShowAirTemp);
ui->powerShowSensor1->setChecked(m_settings.m_sensorVisible[0]);
m_sensors[0].setName(m_settings.m_sensorName[0]);
m_sensors[0].clicked(m_settings.m_sensorVisible[0]);
ui->powerShowSensor2->setChecked(m_settings.m_sensorVisible[1]);
m_sensors[1].setName(m_settings.m_sensorName[1]);
m_sensors[1].clicked(m_settings.m_sensorVisible[1]);
ui->powerShowFiltered->setChecked(m_settings.m_powerShowFiltered);
if (m_powerFilteredSeries) {
m_powerFilteredSeries->setVisible(m_settings.m_powerShowFiltered);
}
ui->powerFilterWidgets->setVisible(m_settings.m_powerShowFiltered);
ui->powerFilter->setCurrentIndex((int)m_settings.m_powerFilter);
ui->powerFilterN->setValue(m_settings.m_powerFilterN);
ui->powerShowMeasurement->setChecked(m_settings.m_powerShowMeasurement);
if (m_powerSeries) {
m_powerSeries->setVisible(m_settings.m_powerShowMeasurement);
}
ui->power2DLinkSweep->setChecked(m_settings.m_power2DLinkSweep);
ui->power2DSweepType->setCurrentIndex((int)m_settings.m_power2DSweepType);
ui->power2DWidth->setValue(m_settings.m_power2DWidth);
ui->power2DHeight->setValue(m_settings.m_power2DHeight);
ui->power2DXMin->setValue(m_settings.m_power2DXMin);
ui->power2DXMax->setValue(m_settings.m_power2DXMax);
ui->power2DYMin->setValue(m_settings.m_power2DYMin);
ui->power2DYMax->setValue(m_settings.m_power2DYMax);
ui->powerColourAutoscale->setChecked(m_settings.m_powerColourAutoscale);
ui->powerColourScaleMin->setValue(m_settings.m_powerColourScaleMin);
ui->powerColourScaleMin->setEnabled(!m_settings.m_powerColourAutoscale);
ui->powerColourScaleMax->setValue(m_settings.m_powerColourScaleMax);
ui->powerColourScaleMax->setEnabled(!m_settings.m_powerColourAutoscale);
ui->powerColourPalette->setCurrentIndex(ui->powerColourPalette->findText(m_settings.m_powerColourPalette));
ui->spectrumReverseXAxis->setChecked(m_settings.m_spectrumReverseXAxis);
ui->spectrumPeak->setChecked(m_settings.m_spectrumPeaks);
ui->spectrumMarker->setChecked(m_settings.m_spectrumMarkers);
ui->spectrumTemp->setChecked(m_settings.m_spectrumTemp);
if (m_fftGaussianSeries) {
m_fftGaussianSeries->setVisible(m_settings.m_spectrumTemp);
}
ui->spectrumShowRefLine->setChecked(m_settings.m_spectrumRefLine);
if (m_fftHlineSeries)
{
m_fftHlineSeries->setVisible(m_settings.m_spectrumRefLine);
m_fftDopplerAxis->setVisible(m_settings.m_spectrumRefLine);
}
ui->spectrumShowLAB->setChecked(m_settings.m_spectrumLAB);
if (m_fftLABSeries) {
m_fftLABSeries->setVisible(m_settings.m_spectrumLAB);
}
ui->spectrumShowDistance->setChecked(m_settings.m_spectrumDistance);
updateDistanceColumns();
ui->spectrumShowLegend->setChecked(m_settings.m_spectrumLegend);
if (m_fftChart) {
m_fftChart->legend()->setVisible(m_settings.m_spectrumLegend);
}
if (m_calChart) {
m_calChart->legend()->setVisible(m_settings.m_spectrumLegend);
}
ui->refFrame->setCurrentIndex((int)m_settings.m_refFrame);
ui->spectrumLine->setCurrentIndex((int)m_settings.m_line);
ui->sunDistanceToGC->setValue(m_settings.m_sunDistanceToGC);
ui->sunOrbitalVelocity->setValue(m_settings.m_sunOrbitalVelocity);
displaySpectrumLineFrequency();
updateSpectrumSelect();
updatePowerSelect();
// Updates visibility of widgets
updateSpectrumMarkerTableVisibility();
updatePowerMarkerTableVisibility();
updatePowerChartWidgetsVisibility();
updateSpectrumChartWidgetsVisibility();
updateIntegrationTime();
ui->runMode->setCurrentIndex((int)m_settings.m_runMode);
ui->sweepStartAtTime->setCurrentIndex(m_settings.m_sweepStartAtTime ? 1 : 0);
ui->sweepStartDateTime->setDateTime(m_settings.m_sweepStartDateTime);
ui->sweepStartDateTime->setVisible(m_settings.m_sweepStartAtTime);
ui->sweepType->setCurrentIndex((int)m_settings.m_sweepType);
ui->sweep1Start->setValue(m_settings.m_sweep1Start);
ui->sweep1Stop->setValue(m_settings.m_sweep1Stop);
ui->sweep1Step->setValue(m_settings.m_sweep1Step);
ui->sweep1Delay->setValue(m_settings.m_sweep1Delay);
ui->sweep2Start->setValue(m_settings.m_sweep2Start);
ui->sweep2Stop->setValue(m_settings.m_sweep2Stop);
ui->sweep2Step->setValue(m_settings.m_sweep2Step);
ui->sweep2Delay->setValue(m_settings.m_sweep2Delay);
displayRunModeSettings();
updateIndexLabel();
// Order and size columns
QHeaderView *header = ui->powerTable->horizontalHeader();
for (int i = 0; i < RADIOASTRONOMY_POWERTABLE_COLUMNS; i++)
{
bool hidden = m_settings.m_powerTableColumnSizes[i] == 0;
header->setSectionHidden(i, hidden);
powerTableMenu->actions().at(i)->setChecked(!hidden);
if (m_settings.m_powerTableColumnSizes[i] > 0)
ui->powerTable->setColumnWidth(i, m_settings.m_powerTableColumnSizes[i]);
header->moveSection(header->visualIndex(i), m_settings.m_powerTableColumnIndexes[i]);
}
getRollupContents()->restoreState(m_rollupState);
updateAbsoluteCenterFrequency();
blockApplySettings(false);
getRollupContents()->arrangeRollups();
}
void RadioAstronomyGUI::leaveEvent(QEvent* event)
{
m_channelMarker.setHighlighted(false);
ChannelGUI::leaveEvent(event);
}
void RadioAstronomyGUI::enterEvent(QEvent* event)
{
m_channelMarker.setHighlighted(true);
ChannelGUI::enterEvent(event);
}
void RadioAstronomyGUI::tick()
{
double magsqAvg, magsqPeak;
int nbMagsqSamples;
m_radioAstronomy->getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples);
double powDbAvg = CalcDb::dbPower(magsqAvg);
if (m_tickCount % 4 == 0) {
ui->channelPower->setText(QString::number(powDbAvg, 'f', 1));
}
m_tickCount++;
}
void RadioAstronomyGUI::updateRotatorList(const QList<RadioAstronomySettings::AvailableFeature>& rotators)
{
// Update list of rotators
ui->rotator->blockSignals(true);
ui->rotator->clear();
ui->rotator->addItem("None");
for (const auto& rotator : rotators)
{
QString name = QString("F%1:%2 %3").arg(rotator.m_featureSetIndex).arg(rotator.m_featureIndex).arg(rotator.m_type);
ui->rotator->addItem(name);
}
// Rotator feature can be created after this plugin, so select it
// if the chosen rotator appears
int rotatorIndex = ui->rotator->findText(m_settings.m_rotator);
if (rotatorIndex >= 0) {
ui->rotator->setCurrentIndex(rotatorIndex);
} else {
ui->rotator->setCurrentIndex(0); // return to None
}
ui->rotator->blockSignals(false);
}
void RadioAstronomyGUI::on_fftSize_currentIndexChanged(int index)
{
m_settings.m_fftSize = 1 << (4+index);
applySettings();
updateIntegrationTime();
}
void RadioAstronomyGUI::on_fftWindow_currentIndexChanged(int index)
{
m_settings.m_fftWindow = (RadioAstronomySettings::FFTWindow)index;
applySettings();
}
void RadioAstronomyGUI::on_filterFreqs_editingFinished()
{
m_settings.m_filterFreqs = ui->filterFreqs->text();
applySettings();
}
void RadioAstronomyGUI::on_gainVariation_valueChanged(double value)
{
m_settings.m_gainVariation = value;
applySettings();
updateTSys0();
}
// Can we allow user to enter text that is automatically looked up on SIMBAD?
void RadioAstronomyGUI::on_sourceType_currentIndexChanged(int index)
{
m_settings.m_sourceType = (RadioAstronomySettings::SourceType)index;
applySettings();
if (m_settings.m_sourceType == RadioAstronomySettings::SUN)
{
// Mean diameter of Sun in degrees from Earth
ui->omegaS->setValue(0.53);
ui->omegaSUnits->setCurrentIndex(0);
}
else if (m_settings.m_sourceType == RadioAstronomySettings::CAS_A)
{
// Diameter of Cas A in degrees http://simbad.u-strasbg.fr/simbad/sim-id?Ident=Cassiopeia+A
ui->omegaS->setValue(0.08333);
ui->omegaSUnits->setCurrentIndex(0);
}
bool visible = index == 1 || index >= 3;
ui->omegaS->setVisible(visible);
ui->omegaSUnits->setVisible(visible);
}
void RadioAstronomyGUI::on_omegaS_valueChanged(double value)
{
m_settings.m_omegaS = value;
if ((m_settings.m_sourceType == RadioAstronomySettings::SUN) && (value != 0.53)) {
ui->sourceType->setCurrentIndex((int)RadioAstronomySettings::COMPACT);
} else if ((m_settings.m_sourceType == RadioAstronomySettings::CAS_A) && (value != 0.08333)) {
ui->sourceType->setCurrentIndex((int)RadioAstronomySettings::COMPACT);
}
applySettings();
}
void RadioAstronomyGUI::updateOmegaA()
{
if (m_settings.m_omegaAUnits == RadioAstronomySettings::DEGREES) {
ui->omegaA->setText(QString("%1").arg(m_beamWidth, 0, 'f', 1));
} else {
ui->omegaA->setText(QString("%1").arg(hpbwToSteradians(m_beamWidth), 0, 'f', 4));
}
}
void RadioAstronomyGUI::on_omegaAUnits_currentIndexChanged(int index)
{
m_settings.m_omegaAUnits = (RadioAstronomySettings::AngleUnits)index;
updateOmegaA();
if (m_settings.m_omegaAUnits == RadioAstronomySettings::DEGREES) {
ui->omegaALabel->setText("HPBW");
} else {
ui->omegaALabel->setText(QString("%1<sub>A</sub>").arg(QChar(937)));
}
applySettings();
}
void RadioAstronomyGUI::on_omegaSUnits_currentIndexChanged(int index)
{
m_settings.m_omegaSUnits = (RadioAstronomySettings::AngleUnits)index;
if ( ( (m_settings.m_sourceType == RadioAstronomySettings::SUN)
|| (m_settings.m_sourceType == RadioAstronomySettings::CAS_A)
)
&& (m_settings.m_omegaSUnits != RadioAstronomySettings::DEGREES)
)
{
ui->sourceType->setCurrentIndex((int)RadioAstronomySettings::COMPACT);
}
applySettings();
}
void RadioAstronomyGUI::on_starTracker_currentTextChanged(const QString& text)
{
m_settings.m_starTracker = text;
applySettings();
}
void RadioAstronomyGUI::on_rotator_currentTextChanged(const QString& text)
{
m_settings.m_rotator = text;
applySettings();
}
void RadioAstronomyGUI::on_showSensors_clicked()
{
RadioAstronomySensorDialog dialog(&m_settings);
if (dialog.exec() == QDialog::Accepted)
{
m_sensors[0].setName(m_settings.m_sensorName[0]);
m_sensors[1].setName(m_settings.m_sensorName[1]);
applySettings();
}
}
void RadioAstronomyGUI::sensorMeasurementReceived(const RadioAstronomy::MsgSensorMeasurement& measurement)
{
int sensor = measurement.getSensor();
double value = measurement.getValue();
QDateTime dateTime = measurement.getDateTime();
SensorMeasurement* sm = new SensorMeasurement(dateTime, value);
m_sensors[sensor].append(sm);
}
void RadioAstronomyGUI::on_powerChartSelect_currentIndexChanged(int index)
{
m_settings.m_powerYData = (RadioAstronomySettings::PowerYData)index;
ui->powerYUnits->clear();
switch (m_settings.m_powerYData)
{
case RadioAstronomySettings::PY_POWER:
ui->powerYUnits->addItem("dBFS");
ui->powerYUnits->addItem("dBm");
ui->powerYUnits->addItem("Watts");
break;
case RadioAstronomySettings::PY_TSYS:
case RadioAstronomySettings::PY_TSOURCE:
ui->powerYUnits->addItem("K");
break;
case RadioAstronomySettings::PY_FLUX:
ui->powerYUnits->addItem("SFU");
ui->powerYUnits->addItem("Jy");
break;
case RadioAstronomySettings::PY_2D_MAP:
ui->powerYUnits->addItem("dBFS");
ui->powerYUnits->addItem("dBm");
//ui->powerYUnits->addItem("Watts"); // No watts for now, as range spin boxes can't handle scientific notation
ui->powerYUnits->addItem("K");
break;
default:
break;
}
updatePowerMarkerTableVisibility();
updatePowerChartWidgetsVisibility();
plotPowerChart();
applySettings();
}
void RadioAstronomyGUI::updatePowerChartWidgetsVisibility()
{
bool powerChart;
if (m_settings.m_powerYData != RadioAstronomySettings::PY_2D_MAP) {
powerChart = true;
} else {
powerChart = false;
}
ui->powerShowLegend->setVisible(powerChart);
ui->powerShowSensor2->setVisible(powerChart);
ui->powerShowSensor1->setVisible(powerChart);
ui->powerShowAirTemp->setVisible(powerChart);
ui->powerShowTsys0->setVisible(powerChart);
ui->powerShowAvg->setVisible(powerChart);
ui->powerShowGaussian->setVisible(powerChart);
ui->powerShowMarker->setVisible(powerChart);
ui->powerShowPeak->setVisible(powerChart);
ui->powerScaleWidgets->setVisible(powerChart);
ui->powerGaussianWidgets->setVisible(powerChart && m_settings.m_powerShowGaussian);
ui->powerMarkerTableWidgets->setVisible(powerChart && (m_settings.m_powerPeaks || m_settings.m_powerMarkers));
ui->power2DScaleWidgets->setVisible(!powerChart);
ui->power2DColourScaleWidgets->setVisible(!powerChart);
getRollupContents()->arrangeRollups();
}
int RadioAstronomyGUI::powerYUnitsToIndex(RadioAstronomySettings::PowerYUnits units)
{
switch (units)
{
case RadioAstronomySettings::PY_DBFS:
return 0;
case RadioAstronomySettings::PY_DBM:
return 1;
case RadioAstronomySettings::PY_WATTS:
return 2;
case RadioAstronomySettings::PY_KELVIN:
return 0;
case RadioAstronomySettings::PY_SFU:
return 0;
case RadioAstronomySettings::PY_JANSKY:
return 1;
}
return -1;
}
void RadioAstronomyGUI::on_powerYUnits_currentIndexChanged(int index)
{
(void) index;
QString text = ui->powerYUnits->currentText();
if (text == "dBFS")
{
m_settings.m_powerYUnits = RadioAstronomySettings::PY_DBFS;
ui->powerMarkerTable->horizontalHeaderItem(POWER_MARKER_COL_VALUE)->setText("Power (dBFS)");
ui->powerColourScaleMin->setDecimals(2);
ui->powerColourScaleMax->setDecimals(2);
}
else if (text == "dBm")
{
m_settings.m_powerYUnits = RadioAstronomySettings::PY_DBM;
ui->powerMarkerTable->horizontalHeaderItem(POWER_MARKER_COL_VALUE)->setText("Power (dBm)");
ui->powerColourScaleMin->setDecimals(2);
ui->powerColourScaleMax->setDecimals(2);
}
else if (text == "Watts")
{
m_settings.m_powerYUnits = RadioAstronomySettings::PY_WATTS;
ui->powerMarkerTable->horizontalHeaderItem(POWER_MARKER_COL_VALUE)->setText("Power (W)");
}
else if (text == "K")
{
m_settings.m_powerYUnits = RadioAstronomySettings::PY_KELVIN;
ui->powerMarkerTable->horizontalHeaderItem(POWER_MARKER_COL_VALUE)->setText("Temp (K)");
ui->powerColourScaleMin->setDecimals(0);
ui->powerColourScaleMax->setDecimals(0);
}
else if (text == "SFU")
{
m_settings.m_powerYUnits = RadioAstronomySettings::PY_SFU;
ui->powerMarkerTable->horizontalHeaderItem(POWER_MARKER_COL_VALUE)->setText("Flux (SFU)");
}
else if (text == "Jy")
{
m_settings.m_powerYUnits = RadioAstronomySettings::PY_JANSKY;
ui->powerMarkerTable->horizontalHeaderItem(POWER_MARKER_COL_VALUE)->setText("Flux (Jy)");
}
if (text == "dBFS")
{
ui->powerColourScaleMinUnits->setText("dB");
ui->powerColourScaleMaxUnits->setText("dB");
}
else
{
ui->powerColourScaleMinUnits->setText(text);
ui->powerColourScaleMaxUnits->setText(text);
}
applySettings();
plotPowerChart();
}
void RadioAstronomyGUI::on_spectrumChartSelect_currentIndexChanged(int index)
{
updateSpectrumMarkerTableVisibility(); // If this follows updateSpectrumChartWidgetsVisibility, widgets are not redrawn properly.
updateSpectrumChartWidgetsVisibility(); // when switching from cal to spectrum if table is visible
if (index == 0)
{
if (m_fftChart) {
ui->spectrumChart->setChart(m_fftChart);
}
}
else
{
if (m_calChart) {
ui->spectrumChart->setChart(m_calChart);
}
}
}
void RadioAstronomyGUI::updateSpectrumChartWidgetsVisibility()
{
bool fft = ui->spectrumChartSelect->currentIndex() == 0;
ui->spectrumYUnits->setVisible(fft);
ui->spectrumScaleWidgets->setVisible(fft);
ui->spectrumSelectWidgets->setVisible(fft);
ui->spectrumRefLineWidgets->setVisible(fft && m_settings.m_spectrumRefLine);
ui->spectrumGaussianWidgets->setVisible(fft && m_settings.m_spectrumTemp);
ui->calWidgets->setVisible(!fft);
ui->recalibrate->setVisible(!fft);
ui->startCalHot->setVisible(!fft);
ui->startCalCold->setVisible(!fft);
ui->clearCal->setVisible(!fft);
ui->showCalSettings->setVisible(!fft);
ui->spectrumShowRefLine->setVisible(fft);
ui->spectrumShowDistance->setVisible(fft);
ui->spectrumShowLAB->setVisible(fft);
ui->spectrumTemp->setVisible(fft);
ui->spectrumMarker->setVisible(fft);
ui->spectrumPeak->setVisible(fft);
ui->saveSpectrumChartImages->setVisible(fft);
getRollupContents()->arrangeRollups();
}
// Calulate mean, RMS and standard deviation
// Currently this is for all data - but could make it only for visible data
void RadioAstronomyGUI::calcAverages()
{
qreal sum = 0.0;
qreal sumSq = 0.0;
QVector<QPointF> points = m_powerSeries->pointsVector();
for (int i = 0; i < points.size(); i++)
{
QPointF point = points.at(i);
qreal y = point.y();
sum += y;
sumSq += y * y;
}
qreal mean = sum / points.size();
qreal rms = std::sqrt(sumSq / points.size());
qreal sumSqDiff = 0.0;
for (int i = 0; i < points.size(); i++)
{
QPointF point = points.at(i);
qreal y = point.y();
qreal diff = y - mean;
sumSqDiff += diff * diff;
}
qreal sigma = std::sqrt(sumSqDiff / points.size());
ui->powerMean->setText(QString::number(mean));
ui->powerRMS->setText(QString::number(rms));
ui->powerSD->setText(QString::number(sigma));
}
QRgb RadioAstronomyGUI::intensityToColor(float intensity)
{
QRgb c1, c2;
float scale;
if (std::isnan(intensity)) {
return qRgb(0, 0, 0);
}
// Get in range 0-1
intensity = (intensity - m_settings.m_powerColourScaleMin) / (m_settings.m_powerColourScaleMax - m_settings.m_powerColourScaleMin);
intensity = std::min(intensity, 1.0f);
intensity = std::max(intensity, 0.0f);
if (m_settings.m_powerColourPalette[0] == 'C')
{
// Colour heat map gradient
if (intensity <= 0.25f)
{
c1 = qRgb(0, 0, 0x80); // Navy
c2 = qRgb(0, 0, 0xff); // Blue
scale = intensity * 4.0f;
}
else if (intensity <= 0.5f)
{
c1 = qRgb(0, 0, 0xff); // Blue
c2 = qRgb(0, 0xff, 0); // Green
scale = (intensity - 0.25f) * 4.0f;
}
else if (intensity <= 0.75f)
{
c1 = qRgb(0, 0xff, 0); // Green
c2 = qRgb(0xff, 0xff, 0); // Yellow
scale = (intensity - 0.5f) * 4.0f;
}
else
{
c1 = qRgb(0xff, 0xff, 0); // Yellow
c2 = qRgb(0xff, 0, 0); // Red
scale = (intensity - 0.75f) * 4.0f;
}
int r, g, b;
r = (qRed(c2)-qRed(c1))*scale+qRed(c1);
g = (qGreen(c2)-qGreen(c1))*scale+qGreen(c1);
b = (qBlue(c2)-qBlue(c1))*scale+qBlue(c1);
return qRgb(r, g, b);
}
else
{
// Greyscale
int g = 255 * intensity;
return qRgb(g, g, g);
}
}
void RadioAstronomyGUI::plot2DChart()
{
// Only plot if visible
if (ui->powerChartSelect->currentIndex() == 4)
{
QChart *oldChart = m_2DChart;
m_2DChart = new QChart();
m_2DChart->layout()->setContentsMargins(0, 0, 0, 0);
m_2DChart->setMargins(QMargins(1, 1, 1, 1));
m_2DChart->setTheme(QChart::ChartThemeDark);
m_2DChart->setTitle("");
m_2DXAxis = new QValueAxis();
m_2DYAxis = new QValueAxis();
m_2DXAxis->setGridLineVisible(false);
m_2DYAxis->setGridLineVisible(false);
set2DAxisTitles();
m_2DXAxis->setRange(m_settings.m_power2DXMin, m_settings.m_power2DXMax);
m_2DYAxis->setRange(m_settings.m_power2DYMin, m_settings.m_power2DYMax);
m_2DChart->addAxis(m_2DXAxis, Qt::AlignBottom);
m_2DChart->addAxis(m_2DYAxis, Qt::AlignLeft);
m_2DMap.fill(qRgb(0, 0, 0));
for (int i = 0; i < m_fftMeasurements.size(); i++) {
update2DImage(m_fftMeasurements[i], i < m_fftMeasurements.size() - 1);
}
if (m_settings.m_powerColourAutoscale) {
powerColourAutoscale();
}
connect(m_2DChart, SIGNAL(plotAreaChanged(QRectF)), this, SLOT(plotAreaChanged(QRectF)));
ui->powerChart->setChart(m_2DChart);
delete oldChart;
}
}
void RadioAstronomyGUI::set2DAxisTitles()
{
if (m_settings.m_power2DSweepType == RadioAstronomySettings::SWP_LB)
{
m_2DXAxis->setTitleText(QString("Galactic longitude (%1)").arg(QChar(0xb0)));
m_2DYAxis->setTitleText(QString("Galactic latitude (%1)").arg(QChar(0xb0)));
}
else
{
m_2DXAxis->setTitleText(QString("Azimuth (%1)").arg(QChar(0xb0)));
m_2DYAxis->setTitleText(QString("Elevation (%1)").arg(QChar(0xb0)));
}
}
void RadioAstronomyGUI::update2DSettingsFromSweep()
{
if (m_settings.m_runMode == RadioAstronomySettings::SWEEP)
{
ui->power2DSweepType->setCurrentIndex((int)m_settings.m_sweepType);
// Calculate width and height of image - 1 pixel per sweep measurement
float sweep1Start, sweep1Stop;
sweep1Start = m_settings.m_sweep1Start;
sweep1Stop = m_settings.m_sweep1Stop;
// Handle azimuth/l sweep through 0. E.g. 340deg -> 20deg with +vs step, or 20deg -> 340deg with -ve step
if ((m_settings.m_sweep1Stop < m_settings.m_sweep1Start) && (m_settings.m_sweep1Step > 0)) {
sweep1Stop = m_settings.m_sweep1Stop + 360.0;
} else if ((m_settings.m_sweep1Stop > m_settings.m_sweep1Start) && (m_settings.m_sweep1Step < 0)) {
sweep1Start += 360.0;
}
int width = abs((sweep1Stop - sweep1Start) / m_settings.m_sweep1Step) + 1;
int height = abs((m_settings.m_sweep2Stop - m_settings.m_sweep2Start) / m_settings.m_sweep2Step) + 1;
ui->power2DWidth->setValue(width);
ui->power2DHeight->setValue(height);
// Subtract/add half a step so that pixels are centred on coordinates
int start1 = m_settings.m_sweep1Start - m_settings.m_sweep1Step / 2;
int stop1 = m_settings.m_sweep1Stop + m_settings.m_sweep1Step / 2;
if (start1 < stop1)
{
ui->power2DXMin->setValue(start1);
ui->power2DXMax->setValue(stop1);
}
else
{
ui->power2DXMin->setValue(stop1);
ui->power2DXMax->setValue(start1);
}
int start2 = m_settings.m_sweep2Start - m_settings.m_sweep2Step / 2;
int stop2 = m_settings.m_sweep2Stop + m_settings.m_sweep2Step / 2;
if (start2 < stop2)
{
ui->power2DYMin->setValue(start2);
ui->power2DYMax->setValue(stop2);
}
else
{
ui->power2DYMin->setValue(stop2);
ui->power2DYMax->setValue(start2);
}
m_sweepIndex = 0;
}
}
void RadioAstronomyGUI::create2DImage()
{
// Intensity array holds power/temperature values which are then colourised in to a QImage
delete m_2DMapIntensity;
int size = m_settings.m_power2DWidth * m_settings.m_power2DHeight;
if (size > 0)
{
m_2DMapIntensity = new float[size];
for (int i = 0; i < size; i++) {
m_2DMapIntensity[i] = NAN;
}
m_2DMapMin = std::numeric_limits<float>::max();
m_2DMapMax = -std::numeric_limits<float>::max();
QImage map(m_settings.m_power2DWidth, m_settings.m_power2DHeight, QImage::Format_ARGB32);
map.fill(qRgb(0, 0, 0));
m_2DMap = map;
}
else
{
m_2DMapIntensity = nullptr;
m_2DMap = QImage();
}
}
void RadioAstronomyGUI::update2DImage(FFTMeasurement* fft, bool skipCalcs)
{
if (m_2DMap.width() > 0)
{
int x, y;
if (m_settings.m_power2DSweepType == RadioAstronomySettings::SWP_OFFSET)
{
y = fft->m_sweepIndex / m_2DMap.width();
x = fft->m_sweepIndex % m_2DMap.width();
if (m_settings.m_sweep2Step >= 0) {
y = m_2DMap.height() - 1 - y;
}
if (m_settings.m_sweep1Step < 0) {
x = m_2DMap.width() - 1 - x;
}
}
else
{
if (m_settings.m_power2DSweepType == RadioAstronomySettings::SWP_LB)
{
x = (int)round(fft->m_l);
y = (int)round(fft->m_b);
}
else
{
x = (int)round(fft->m_azimuth);
y = (int)round(fft->m_elevation);
}
// Map coordinates to pixels
float xRange = m_settings.m_power2DXMax - m_settings.m_power2DXMin;
float yRange = m_settings.m_power2DYMax - m_settings.m_power2DYMin;
x = (x - m_settings.m_power2DXMin) * m_settings.m_power2DWidth / xRange;
y = (y - m_settings.m_power2DYMin) * m_settings.m_power2DHeight / yRange;
if (yRange >= 0) {
y = m_2DMap.height() - 1 - y;
}
if (xRange < 0) {
x = m_2DMap.width() - 1 - x;
}
}
if ((x >= 0) && (x < m_2DMap.width()) && (y >= 0) && (y < m_2DMap.height()))
{
float intensity;
switch (m_settings.m_powerYUnits)
{
case RadioAstronomySettings::PY_DBFS:
intensity = fft->m_totalPowerdBFS;
break;
case RadioAstronomySettings::PY_DBM:
intensity = fft->m_totalPowerdBm;
break;
case RadioAstronomySettings::PY_WATTS:
intensity = fft->m_totalPowerWatts;
break;
case RadioAstronomySettings::PY_KELVIN:
intensity = fft->m_tSys;
break;
default:
break;
}
float newMin = std::min(m_2DMapMin, intensity);
float newMax = std::max(m_2DMapMax, intensity);
bool rescale = false;
if ((newMin != m_2DMapMin) || (newMax != m_2DMapMax)) {
rescale = m_settings.m_powerColourAutoscale;
}
m_2DMapMin = newMin;
m_2DMapMax = newMax;
m_2DMapIntensity[y*m_2DMap.width()+x] = intensity;
m_2DMap.setPixel(x, y, intensityToColor(intensity));
if (rescale && !skipCalcs) {
powerColourAutoscale();
}
if (m_2DChart && !skipCalcs) {
plotAreaChanged(m_2DChart->plotArea());
}
}
}
}
void RadioAstronomyGUI::recolour2DImage()
{
for (int y = 0; y < m_2DMap.height(); y++)
{
for (int x = 0; x < m_2DMap.width(); x++) {
m_2DMap.setPixel(x, y, intensityToColor(m_2DMapIntensity[y*m_2DMap.width()+x]));
}
}
if (m_2DChart) {
plotAreaChanged(m_2DChart->plotArea());
}
}
void RadioAstronomyGUI::plotAreaChanged(const QRectF& plotArea)
{
if (ui->powerChartSelect->currentIndex() == 4)
{
int width = static_cast<int>(plotArea.width());
int height = static_cast<int>(plotArea.height());
int viewW = static_cast<int>(ui->powerChart->width());
int viewH = static_cast<int>(ui->powerChart->height());
// Scale the image to fit plot area
QImage image = m_2DMap.scaled(QSize(width, height), Qt::IgnoreAspectRatio);
QImage translated(viewW, viewH, QImage::Format_ARGB32);
translated.fill(Qt::black);
QPainter painter(&translated);
painter.drawImage(plotArea.topLeft(), image);
m_2DChart->setPlotAreaBackgroundBrush(translated);
m_2DChart->setPlotAreaBackgroundVisible(true);
}
}
// Find min and max coordinates from existing data
void RadioAstronomyGUI::power2DAutoscale()
{
if (m_fftMeasurements.size() > 0)
{
float minX = std::numeric_limits<float>::max();
float maxX = -std::numeric_limits<float>::max();
float minY = std::numeric_limits<float>::max();
float maxY = -std::numeric_limits<float>::max();
for (int i = 0; i < m_fftMeasurements.size(); i++)
{
FFTMeasurement* fft = m_fftMeasurements[i];
float x, y;
if (m_settings.m_power2DSweepType == RadioAstronomySettings::SWP_LB)
{
x = fft->m_l;
y = fft->m_b;
}
else
{
x = fft->m_azimuth;
y = fft->m_elevation;
}
minX = std::min(minX, x);
maxX = std::max(maxX, x);
minY = std::min(minY, y);
maxY = std::max(maxY, y);
}
// Adjust so pixels are centered
float xAdjust = (maxX - minX) / m_2DMap.width() / 2;
float yAdjust = (maxY - minY) / m_2DMap.height() / 2;
ui->power2DXMin->setValue(minX - xAdjust);
ui->power2DXMax->setValue(maxX + xAdjust);
ui->power2DYMin->setValue(minY - yAdjust);
ui->power2DYMax->setValue(maxY + xAdjust);
}
}
void RadioAstronomyGUI::on_power2DAutoscale_clicked()
{
power2DAutoscale();
plot2DChart();
}
void RadioAstronomyGUI::on_power2DLinkSweep_toggled(bool checked)
{
m_settings.m_power2DLinkSweep = checked;
applySettings();
}
void RadioAstronomyGUI::on_power2DSweepType_currentIndexChanged(int index)
{
m_settings.m_power2DSweepType = (RadioAstronomySettings::SweepType)index;
applySettings();
plot2DChart();
}
void RadioAstronomyGUI::on_power2DWidth_valueChanged(int value)
{
m_settings.m_power2DWidth = value;
applySettings();
create2DImage();
plot2DChart();
}
void RadioAstronomyGUI::on_power2DHeight_valueChanged(int value)
{
m_settings.m_power2DHeight = value;
applySettings();
create2DImage();
plot2DChart();
}
void RadioAstronomyGUI::on_power2DXMin_valueChanged(double value)
{
m_settings.m_power2DXMin = value;
applySettings();
if (m_2DXAxis)
{
m_2DXAxis->setMin(m_settings.m_power2DXMin);
plot2DChart();
}
}
void RadioAstronomyGUI::on_power2DXMax_valueChanged(double value)
{
m_settings.m_power2DXMax = value;
applySettings();
if (m_2DXAxis)
{
m_2DXAxis->setMax(m_settings.m_power2DXMax);
plot2DChart();
}
}
void RadioAstronomyGUI::on_power2DYMin_valueChanged(double value)
{
m_settings.m_power2DYMin = value;
applySettings();
if (m_2DYAxis)
{
m_2DYAxis->setMin(m_settings.m_power2DYMin);
plot2DChart();
}
}
void RadioAstronomyGUI::on_power2DYMax_valueChanged(double value)
{
m_settings.m_power2DYMax = value;
applySettings();
if (m_2DYAxis)
{
m_2DYAxis->setMax(m_settings.m_power2DYMax);
plot2DChart();
}
}
void RadioAstronomyGUI::powerColourAutoscale()
{
int width = m_2DMap.width();
int height = m_2DMap.height();
float newMin = std::numeric_limits<float>::max();
float newMax = -std::numeric_limits<float>::max();
for (int i = 0; i < width * height; i++)
{
if (!std::isnan(m_2DMapIntensity[i]))
{
newMin = std::min(newMin, m_2DMapIntensity[i]);
newMax = std::max(newMax, m_2DMapIntensity[i]);
}
}
if ((newMin != ui->powerColourScaleMin->value()) || (newMax != ui->powerColourScaleMax->value()))
{
ui->powerColourScaleMin->setValue(std::floor(newMin * 10.0) / 10.0);
ui->powerColourScaleMax->setValue(std::ceil(newMax * 10.0) / 10.0);
}
}
void RadioAstronomyGUI::on_powerColourAutoscale_toggled(bool checked)
{
m_settings.m_powerColourAutoscale = checked;
applySettings();
if (m_settings.m_powerColourAutoscale) {
powerColourAutoscale();
}
ui->powerColourScaleMin->setEnabled(!m_settings.m_powerColourAutoscale);
ui->powerColourScaleMax->setEnabled(!m_settings.m_powerColourAutoscale);
}
void RadioAstronomyGUI::updatePowerColourScaleStep()
{
float diff = abs(m_settings.m_powerColourScaleMax - m_settings.m_powerColourScaleMin);
double step = (diff <= 1.0f) ? 0.1 : 1.0f;
ui->powerColourScaleMin->setSingleStep(step);
ui->powerColourScaleMax->setSingleStep(step);
}
void RadioAstronomyGUI::on_powerColourScaleMin_valueChanged(double value)
{
m_settings.m_powerColourScaleMin = value;
updatePowerColourScaleStep();
applySettings();
recolour2DImage();
}
void RadioAstronomyGUI::on_powerColourScaleMax_valueChanged(double value)
{
m_settings.m_powerColourScaleMax = value;
updatePowerColourScaleStep();
applySettings();
recolour2DImage();
}
void RadioAstronomyGUI::on_powerColourPalette_currentIndexChanged(int index)
{
(void) index;
m_settings.m_powerColourPalette = ui->powerColourPalette->currentText();
applySettings();
recolour2DImage();
}
void RadioAstronomyGUI::plotPowerChart()
{
if (ui->powerChartSelect->currentIndex() == 4) {
plot2DChart();
} else {
plotPowerVsTimeChart();
}
}
void RadioAstronomyGUI::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::AlignRight);
m_powerChart->legend()->setVisible(m_settings.m_powerLegend);
// Create measurement data series
m_powerSeries = new QLineSeries();
m_powerSeries->setVisible(m_settings.m_powerShowMeasurement);
connect(m_powerSeries, &QXYSeries::clicked, this, &RadioAstronomyGUI::powerSeries_clicked);
// Plot peak info
m_powerPeakSeries = new QScatterSeries();
m_powerPeakSeries->setName("Peak");
m_powerPeakSeries->setPointLabelsVisible(true);
m_powerPeakSeries->setPointLabelsFormat("@yPoint"); // Qt can't display dates, so omit @xPoint
m_powerPeakSeries->setMarkerSize(5);
m_powerPeakSeries->setVisible(m_settings.m_powerPeaks);
// Markers
m_powerMarkerSeries = new QScatterSeries();
m_powerMarkerSeries->setName("Marker");
m_powerMarkerSeries->setPointLabelsVisible(true);
m_powerMarkerSeries->setPointLabelsFormat("@yPoint");
m_powerMarkerSeries->setMarkerSize(5);
m_powerMarkerSeries->setVisible(m_settings.m_powerMarkers);
// Noise
m_powerTsys0Series = new QLineSeries();
m_powerTsys0Series->setName("Tsys0");
m_powerTsys0Series->setVisible(m_settings.m_powerShowTsys0);
// Air temperature
m_airTemps.init("Air temp", m_settings.m_powerShowAirTemp);
// Gaussian
m_powerGaussianSeries = new QLineSeries();
m_powerGaussianSeries->setName("Gaussian fit");
m_powerGaussianSeries->setVisible(m_settings.m_powerShowGaussian);
// Filtered measurement
m_powerFilteredSeries = new QLineSeries();
m_powerFilteredSeries->setName("Filtered");
m_powerFilteredSeries->setVisible(m_settings.m_powerShowFiltered);
plotPowerFiltered();
// Sensors
for (int i = 0; i < RADIOASTRONOMY_SENSORS; i++) {
m_sensors[i].init(m_settings.m_sensorName[i], m_settings.m_sensorVisible[i]);
}
// Reset min/max and peaks
m_powerMin = std::numeric_limits<double>::max();
m_powerMax = -std::numeric_limits<double>::max();
m_powerPeakValid = false;
// Create X axis
m_powerXAxis = new QDateTimeAxis();
int rows = ui->powerTable->rowCount();
QString dateTimeFormat = "hh:mm:ss";
m_powerXAxisSameDay = true;
if (rows > 1)
{
QDate start = ui->powerTable->item(0, POWER_COL_DATE)->data(Qt::DisplayRole).toDate();
QDate end = ui->powerTable->item(rows-1, POWER_COL_DATE)->data(Qt::DisplayRole).toDate();
if (start != end)
{
dateTimeFormat = QString("%1 hh:mm").arg(QLocale::system().dateFormat(QLocale::ShortFormat));
m_powerXAxisSameDay = false;
}
}
m_powerXAxis->setFormat(dateTimeFormat);
m_powerXAxis->setRange(ui->powerStartTime->dateTime(), ui->powerEndTime->dateTime());
ui->powerStartTime->setDisplayFormat(dateTimeFormat);
ui->powerEndTime->setDisplayFormat(dateTimeFormat);
// Create Y axis
m_powerYAxis = new QValueAxis();
m_powerXAxis->setTitleText("Time");
calcPowerChartTickCount(size().width());
switch (m_settings.m_powerYData)
{
case RadioAstronomySettings::PY_POWER:
m_powerSeries->setName("Measurement");
switch (m_settings.m_powerYUnits)
{
case RadioAstronomySettings::PY_DBFS:
m_powerYAxis->setTitleText("Power (dBFS)");
break;
case RadioAstronomySettings::PY_DBM:
m_powerYAxis->setTitleText("Power (dBm)");
break;
case RadioAstronomySettings::PY_WATTS:
m_powerYAxis->setTitleText("Power (Watts)");
break;
default:
break;
}
break;
case RadioAstronomySettings::PY_TSYS:
m_powerSeries->setName("Tsys");
m_powerYAxis->setTitleText("Tsys (K)");
break;
case RadioAstronomySettings::PY_TSOURCE:
m_powerSeries->setName("Tsource");
m_powerYAxis->setTitleText("Tsource (K)");
break;
case RadioAstronomySettings::PY_FLUX:
m_powerSeries->setName("Flux density");
switch (m_settings.m_powerYUnits)
{
case RadioAstronomySettings::PY_SFU:
m_powerYAxis->setTitleText("Flux density (SFU)");
break;
case RadioAstronomySettings::PY_JANSKY:
m_powerYAxis->setTitleText("Flux density (Jy)");
break;
default:
break;
}
break;
default:
break;
}
m_powerChart->addAxis(m_powerXAxis, Qt::AlignBottom);
m_powerChart->addAxis(m_powerYAxis, Qt::AlignLeft);
m_powerChart->addAxis(m_airTemps.yAxis(), Qt::AlignRight);
for (int i = 0; i < RADIOASTRONOMY_SENSORS; i++) {
m_powerChart->addAxis(m_sensors[i].yAxis(), Qt::AlignRight);
}
// Add data to series and calculate peaks
for (int i = 0; i < m_fftMeasurements.size(); i++) {
addToPowerSeries(m_fftMeasurements[i], i < (m_fftMeasurements.size() - 1));
}
m_airTemps.addAllToSeries();
for (int i = 0; i < RADIOASTRONOMY_SENSORS; i++) {
m_sensors[i].addAllToSeries();
}
m_powerChart->addSeries(m_powerSeries);
m_powerSeries->attachAxis(m_powerXAxis);
m_powerSeries->attachAxis(m_powerYAxis);
m_powerChart->addSeries(m_powerTsys0Series);
m_powerTsys0Series->attachAxis(m_powerXAxis);
m_powerTsys0Series->attachAxis(m_powerYAxis);
m_powerChart->addSeries(m_powerGaussianSeries);
m_powerGaussianSeries->attachAxis(m_powerXAxis);
m_powerGaussianSeries->attachAxis(m_powerYAxis);
m_airTemps.addToChart(m_powerChart, m_powerXAxis);
for (int i = 0; i < RADIOASTRONOMY_SENSORS; i++) {
m_sensors[i].addToChart(m_powerChart, m_powerXAxis);
}
m_powerChart->addSeries(m_powerFilteredSeries);
m_powerFilteredSeries->attachAxis(m_powerXAxis);
m_powerFilteredSeries->attachAxis(m_powerYAxis);
m_powerChart->addSeries(m_powerPeakSeries);
m_powerPeakSeries->attachAxis(m_powerXAxis);
m_powerPeakSeries->attachAxis(m_powerYAxis);
m_powerChart->addSeries(m_powerMarkerSeries);
m_powerMarkerSeries->attachAxis(m_powerXAxis);
m_powerMarkerSeries->attachAxis(m_powerYAxis);
// Dark theme only has 5 colours for series, so use an extra unique colour (purple)
QPen pen(QColor(qRgb(146, 65, 146)), 2, Qt::SolidLine);
m_sensors[1].setPen(pen);
// Don't have peaks and markers in legend
m_powerChart->legend()->markers(m_powerPeakSeries)[0]->setVisible(false);
m_powerChart->legend()->markers(m_powerMarkerSeries)[0]->setVisible(false);
ui->powerChart->setChart(m_powerChart);
delete oldChart;
}
void RadioAstronomyGUI::calCompletetReceived(const RadioAstronomy::MsgCalComplete& measurement)
{
bool hot = measurement.getHot();
int size = measurement.getSize();
Real* data = measurement.getCal();
FFTMeasurement* fft = new FFTMeasurement();
if (hot)
{
delete m_calHot;
m_calHot = fft;
ui->startCalHot->setStyleSheet("QToolButton { background: none; }");
}
else
{
delete m_calCold;
m_calCold = fft;
ui->startCalCold->setStyleSheet("QToolButton { background: none; }");
}
fft->m_fftData = data;
fft->m_fftSize = size;
fft->m_dateTime = measurement.getDateTime();
fft->m_centerFrequency = m_centerFrequency;
fft->m_sampleRate = m_settings.m_sampleRate;
fft->m_integration = m_settings.m_integration;
fft->m_rfBandwidth = m_settings.m_rfBandwidth;
fft->m_omegaA = calcOmegaA();
fft->m_omegaS = calcOmegaS();
fft->m_coordsValid = m_coordsValid;
fft->m_ra = m_ra;
fft->m_dec = m_dec;
fft->m_azimuth = m_azimuth;
fft->m_elevation = m_elevation;
fft->m_l = m_l;
fft->m_b = m_b;
fft->m_vBCRS = m_vBCRS;
fft->m_vLSR = m_vLSR;
fft->m_solarFlux = m_solarFlux;
fft->m_airTemp = m_airTemps.lastValue();
fft->m_skyTemp = m_skyTemp;
for (int i = 0; i < RADIOASTRONOMY_SENSORS; i++) {
fft->m_sensor[i] = m_sensors[i].lastValue();
}
fft->m_tSys0 = calcTSys0();
fft->m_baseline = m_settings.m_spectrumBaseline;
if (!hot) {
ui->calTsky->setText(QString::number(m_skyTemp, 'f', 1));
}
calcFFTTotalPower(fft);
calcCalAvgDiff();
calibrate();
calcFFTTemperatures(fft);
calcFFTTotalTemperature(fft);
plotCalMeasurements();
}
void RadioAstronomyGUI::plotCalMeasurements()
{
m_calHotSeries->clear();
m_calColdSeries->clear();
if (m_calHot || m_calCold)
{
float minVal = std::numeric_limits<float>::max();
float maxVal = -std::numeric_limits<float>::max();
double size;
double sampleRate;
double centerFrequency;
if (m_calCold && m_calHot)
{
size = (double)std::min(m_calCold->m_fftSize, m_calHot->m_fftSize);
sampleRate = (double)m_calCold->m_sampleRate;
centerFrequency = (double)m_calCold->m_centerFrequency;
}
else if (m_calCold)
{
size = (double)m_calCold->m_fftSize;
sampleRate = (double)m_calCold->m_sampleRate;
centerFrequency = (double)m_calCold->m_centerFrequency;
}
else
{
size = (double)m_calHot->m_fftSize;
sampleRate = (double)m_calHot->m_sampleRate;
centerFrequency = (double)m_calHot->m_centerFrequency;
}
double binRes = sampleRate / size;
double startFreq = centerFrequency - sampleRate / 2.0;
double freq = startFreq;
for (int i = 0; i < size; i++)
{
float value;
bool hotValid = m_calHot && (i < m_calHot->m_fftSize);
bool coldValid = m_calCold && (i < m_calCold->m_fftSize);
if (hotValid)
{
value = CalcDb::dbPower(m_calHot->m_fftData[i]);
m_calHotSeries->append(freq / 1e6, value);
minVal = std::min(minVal, value);
maxVal = std::max(maxVal, value);
}
if (coldValid)
{
value = CalcDb::dbPower(m_calCold->m_fftData[i]);
m_calColdSeries->append(freq / 1e6, value);
minVal = std::min(minVal, value);
maxVal = std::max(maxVal, value);
}
freq += binRes;
}
m_calYAxis->setRange(minVal, maxVal);
double startFreqMHz = centerFrequency/1e6 - sampleRate/ 1e6 / 2.0;
double endFreqMHz = centerFrequency/1e6 + sampleRate/ 1e6 / 2.0;
m_calXAxis->setRange(startFreqMHz, endFreqMHz);
m_calXAxis->setReverse(m_settings.m_spectrumReverseXAxis);
}
}
void RadioAstronomyGUI::plotCalSpectrum()
{
QChart *oldChart = m_calChart;
m_calChart = new QChart();
m_calChart->layout()->setContentsMargins(0, 0, 0, 0);
m_calChart->setMargins(QMargins(1, 1, 1, 1));
m_calChart->setTheme(QChart::ChartThemeDark);
m_calChart->legend()->setAlignment(Qt::AlignRight);
m_calChart->legend()->setVisible(m_settings.m_spectrumLegend);
m_calHotSeries = new QLineSeries();
m_calHotSeries->setName("Hot");
m_calColdSeries = new QLineSeries();
m_calColdSeries->setName("Cold");
m_calXAxis = new QValueAxis();
m_calYAxis = new QValueAxis();
m_calChart->addAxis(m_calXAxis, Qt::AlignBottom);
m_calChart->addAxis(m_calYAxis, Qt::AlignLeft);
m_calXAxis->setTitleText("Frequency (MHz)");
calcSpectrumChartTickCount(m_calXAxis, size().width());
m_calYAxis->setTitleText("Power (dBFS)");
m_calChart->addSeries(m_calHotSeries);
m_calHotSeries->attachAxis(m_calXAxis);
m_calHotSeries->attachAxis(m_calYAxis);
m_calChart->addSeries(m_calColdSeries);
m_calColdSeries->attachAxis(m_calXAxis);
m_calColdSeries->attachAxis(m_calYAxis);
plotCalMeasurements();
ui->spectrumChart->setChart(m_calChart);
delete oldChart;
}
void RadioAstronomyGUI::on_spectrumReference_valueChanged(double value)
{
m_settings.m_spectrumReference = value;
spectrumUpdateYRange();
if (!m_settings.m_spectrumAutoscale) {
applySettings();
}
}
void RadioAstronomyGUI::on_spectrumRange_valueChanged(double value)
{
m_settings.m_spectrumRange = value;
if (m_settings.m_spectrumRange <= 1.0)
{
ui->spectrumRange->setSingleStep(0.1);
ui->spectrumRange->setDecimals(2);
ui->spectrumReference->setDecimals(2);
}
else
{
ui->spectrumRange->setSingleStep(1.0);
ui->spectrumRange->setDecimals(1);
ui->spectrumReference->setDecimals(1);
}
spectrumUpdateYRange();
if (!m_settings.m_spectrumAutoscale) {
applySettings();
}
}
void RadioAstronomyGUI::on_spectrumSpan_valueChanged(double value)
{
m_settings.m_spectrumSpan = value;
spectrumUpdateXRange();
applySettings();
}
void RadioAstronomyGUI::on_spectrumCenterFreq_valueChanged(double value)
{
double offset;
FFTMeasurement* fft = currentFFT();
if (fft) {
offset = value - fft->m_centerFrequency/1e6;
} else {
offset = value - m_centerFrequency/1e6;
}
m_settings.m_spectrumCenterFreqOffset = offset;
spectrumUpdateXRange();
applySettings();
}
void RadioAstronomyGUI::spectrumUpdateXRange(FFTMeasurement* fft)
{
if (!fft) {
fft = currentFFT();
}
if (m_fftXAxis && fft)
{
double startFreqMHz = fft->m_centerFrequency/1e6 - m_settings.m_spectrumSpan / 2.0;
double endFreqMHz = fft->m_centerFrequency/1e6 + m_settings.m_spectrumSpan / 2.0;
m_fftXAxis->setRange(startFreqMHz + m_settings.m_spectrumCenterFreqOffset, endFreqMHz + m_settings.m_spectrumCenterFreqOffset);
double lineFreqMHz = ui->spectrumLineFrequency->value();
double lineFreq = lineFreqMHz * 1e6;
double startFreq = fft->m_centerFrequency + m_settings.m_spectrumSpan*1e6 / 2.0 + m_settings.m_spectrumCenterFreqOffset*1e6;
double endFreq = fft->m_centerFrequency - m_settings.m_spectrumSpan*1e6 / 2.0 + m_settings.m_spectrumCenterFreqOffset*1e6;
m_fftDopplerAxis->setRange(dopplerToVelocity(lineFreq, startFreq, fft),
dopplerToVelocity(lineFreq, endFreq, fft));
}
}
void RadioAstronomyGUI::spectrumUpdateYRange(FFTMeasurement* fft)
{
if (!fft) {
fft = currentFFT();
}
if (m_fftYAxis && fft) {
m_fftYAxis->setRange(m_settings.m_spectrumReference - m_settings.m_spectrumRange, m_settings.m_spectrumReference);
}
}
void RadioAstronomyGUI::spectrumAutoscale()
{
if (m_settings.m_spectrumAutoscale)
{
on_spectrumAutoscaleX_clicked();
on_spectrumAutoscaleY_clicked();
}
}
// Scale Y axis according to min and max values
void RadioAstronomyGUI::on_spectrumAutoscale_toggled(bool checked)
{
m_settings.m_spectrumAutoscale = checked;
ui->spectrumAutoscaleX->setEnabled(!m_settings.m_spectrumAutoscale);
ui->spectrumAutoscaleY->setEnabled(!m_settings.m_spectrumAutoscale);
ui->spectrumReference->setEnabled(!m_settings.m_spectrumAutoscale);
ui->spectrumRange->setEnabled(!m_settings.m_spectrumAutoscale);
ui->spectrumCenterFreq->setEnabled(!m_settings.m_spectrumAutoscale);
ui->spectrumSpan->setEnabled(!m_settings.m_spectrumAutoscale);
spectrumAutoscale();
applySettings();
}
// Get minimum and maximum values in a series
// Assumes minVal and maxVal are initialised
static bool seriesMinAndMax(QLineSeries* series, qreal& minVal, qreal& maxVal)
{
QVector<QPointF> points = series->pointsVector();
for (int i = 0; i < points.size(); i++)
{
qreal power = points[i].y();
minVal = std::min(minVal, power);
maxVal = std::max(maxVal, power);
}
return points.size() > 0;
}
// Scale Y axis according to min and max values
void RadioAstronomyGUI::on_spectrumAutoscaleY_clicked()
{
double minVal = std::numeric_limits<double>::max();
double maxVal = -std::numeric_limits<double>::max();
bool v0 = false, v1 = false;
if (m_fftSeries) {
v0 = seriesMinAndMax(m_fftSeries, minVal, maxVal);
}
if (m_fftLABSeries && m_settings.m_spectrumLAB) {
v1 = seriesMinAndMax(m_fftLABSeries, minVal, maxVal);
}
if (v0 || v1)
{
double range = (maxVal - minVal) * 1.2; // 20% wider than signal range, for space for markers
range = std::max(0.1, range); // Don't be smaller than minimum value we can set in GUI
ui->spectrumRange->setValue(range); // Call before setting reference, so number of decimals are adjusted
ui->spectrumReference->setValue(maxVal + range * 0.15);
}
}
// Scale X axis according to min and max values
void RadioAstronomyGUI::on_spectrumAutoscaleX_clicked()
{
FFTMeasurement* fft = currentFFT();
if (fft)
{
ui->spectrumSpan->setValue(fft->m_sampleRate/1e6);
ui->spectrumCenterFreq->setValue(fft->m_centerFrequency/1e6);
}
else
{
ui->spectrumSpan->setValue(m_basebandSampleRate/1e6);
ui->spectrumCenterFreq->setValue(m_centerFrequency/1e6);
}
}
RadioAstronomyGUI::FFTMeasurement* RadioAstronomyGUI::currentFFT()
{
int index = ui->spectrumIndex->value();
if ((index >= 0) && (index < m_fftMeasurements.size())) {
return m_fftMeasurements[index];
} else {
return nullptr;
}
}
void RadioAstronomyGUI::on_spectrumYUnits_currentIndexChanged(int index)
{
(void) index;
QString text = ui->spectrumYUnits->currentText();
if (text == "dBFS")
{
m_settings.m_spectrumYScale = RadioAstronomySettings::SY_DBFS;
ui->spectrumMarkerTable->horizontalHeaderItem(SPECTRUM_MARKER_COL_VALUE)->setText("Power (dBFS)");
}
else if (text == "SNR")
{
m_settings.m_spectrumYScale = RadioAstronomySettings::SY_SNR;
ui->spectrumMarkerTable->horizontalHeaderItem(SPECTRUM_MARKER_COL_VALUE)->setText("SNR");
}
else if (text == "dBm")
{
m_settings.m_spectrumYScale = RadioAstronomySettings::SY_DBM;
ui->spectrumMarkerTable->horizontalHeaderItem(SPECTRUM_MARKER_COL_VALUE)->setText("Power (dBm)");
}
else if (text == "Tsys K")
{
m_settings.m_spectrumYScale = RadioAstronomySettings::SY_TSYS;
ui->spectrumMarkerTable->horizontalHeaderItem(SPECTRUM_MARKER_COL_VALUE)->setText("Tsys (K)");
}
else
{
m_settings.m_spectrumYScale = RadioAstronomySettings::SY_TSOURCE;
ui->spectrumMarkerTable->horizontalHeaderItem(SPECTRUM_MARKER_COL_VALUE)->setText("Tsource (K)");
}
plotFFTMeasurement();
applySettings();
}
void RadioAstronomyGUI::on_spectrumBaseline_currentIndexChanged(int index)
{
m_settings.m_spectrumBaseline = (RadioAstronomySettings::SpectrumBaseline)index;
plotFFTMeasurement();
if ((m_settings.m_powerYData == RadioAstronomySettings::PY_TSOURCE) || (m_settings.m_powerYData == RadioAstronomySettings::PY_FLUX)) {
plotPowerChart();
}
applySettings();
}
// Convert frequency shift to velocity in km/s (+ve approaching)
static double lineDopplerVelocity(double centre, double f)
{
return Astronomy::dopplerToVelocity(f, centre) / 1000.0f;
}
// Convert frequency shift to velocity (+ve receeding - which seems to be the astronomical convention)
double RadioAstronomyGUI::dopplerToVelocity(double centre, double f, FFTMeasurement *fft)
{
double v = lineDopplerVelocity(centre, f);
// Adjust in to selected reference frame
switch (m_settings.m_refFrame)
{
case RadioAstronomySettings::BCRS:
v -= fft->m_vBCRS;
break;
case RadioAstronomySettings::LSR:
v -= fft->m_vLSR;
break;
default:
break;
}
// Make +ve receeding
return -v;
}
// Replot current FFT
void RadioAstronomyGUI::plotFFTMeasurement()
{
plotFFTMeasurement(ui->spectrumIndex->value());
}
// Plot FFT with specified index
void RadioAstronomyGUI::plotFFTMeasurement(int index)
{
if (index < m_fftMeasurements.size())
{
FFTMeasurement *fft = m_fftMeasurements[index];
m_fftSeries->clear();
m_fftHlineSeries->clear();
m_fftGaussianSeries->clear();
m_fftLABSeries->clear();
m_fftPeakSeries->clear();
double binRes = fft->m_sampleRate / (double)fft->m_fftSize;
double startFreq = fft->m_centerFrequency - fft->m_sampleRate / 2.0;
// Plot reference spectral line and Doppler axis
plotRefLine(fft);
// Plot Gaussian for temp estimation
plotTempGaussian(startFreq, binRes, fft->m_fftSize);
// Plot LAB reference data
if ( fft->m_coordsValid && m_settings.m_spectrumLAB
&& ( (m_settings.m_spectrumYScale == RadioAstronomySettings::SY_TSYS)
|| (m_settings.m_spectrumYScale == RadioAstronomySettings::SY_TSOURCE)
)
)
{
plotLAB(fft->m_l, fft->m_b, m_beamWidth);
}
if ( ((m_settings.m_spectrumYScale == RadioAstronomySettings::SY_SNR) && !fft->m_snr)
|| ((m_settings.m_spectrumYScale == RadioAstronomySettings::SY_DBM) && !fft->m_temp)
|| ((m_settings.m_spectrumYScale == RadioAstronomySettings::SY_TSYS) && !fft->m_temp)
|| ((m_settings.m_spectrumYScale == RadioAstronomySettings::SY_TSOURCE) && !fft->m_temp)
)
{
m_fftChart->setTitle("No cal data: Run calibration or set units to dBFS.");
}
else
{
m_fftChart->setTitle("");
if (fft->m_coordsValid)
{
m_fftChart->setTitle(QString("RA: %1 Dec: %2 l: %3%7 b: %4%7 Az: %5%7 El: %6%7")
.arg(Units::decimalHoursToHoursMinutesAndSeconds(fft->m_ra))
.arg(Units::decimalDegreesToDegreeMinutesAndSeconds(fft->m_dec))
.arg(QString::number(fft->m_l, 'f', 1))
.arg(QString::number(fft->m_b, 'f', 1))
.arg(QString::number(fft->m_azimuth, 'f', 0))
.arg(QString::number(fft->m_elevation, 'f', 0))
.arg(QChar(0xb0)));
}
int peakIdx = 0;
double peakValue = -std::numeric_limits<double>::max();
double freq = startFreq; // Main spectrum seems to use bin midpoint - this uses lowest frequency, so we're tone at centre freq appears in centre of plot
// Plot power/temp
for (int i = 0; i < fft->m_fftSize; i++)
{
qreal value;
switch (m_settings.m_spectrumYScale)
{
case RadioAstronomySettings::SY_DBFS:
value = fft->m_db[i];
break;
case RadioAstronomySettings::SY_SNR:
value = fft->m_snr[i];
break;
case RadioAstronomySettings::SY_DBM:
value = Astronomy::noisePowerdBm(fft->m_temp[i], fft->m_sampleRate/(double)fft->m_fftSize);
break;
case RadioAstronomySettings::SY_TSYS:
value = fft->m_temp[i];
break;
case RadioAstronomySettings::SY_TSOURCE:
switch (m_settings.m_spectrumBaseline)
{
case RadioAstronomySettings::SBL_TSYS0:
value = fft->m_temp[i] - fft->m_tSys0;
break;
case RadioAstronomySettings::SBL_TMIN:
value = fft->m_temp[i] - fft->m_tempMin;
break;
case RadioAstronomySettings::SBL_CAL_COLD:
if (m_calCold) {
value = m_calG[i] * (fft->m_fftData[i] - m_calCold->m_fftData[i]);
} else {
value = 0.0;
}
break;
}
break;
}
if (value > peakValue)
{
peakValue = value;
peakIdx = i;
}
m_fftSeries->append(freq / 1e6, value);
freq += binRes;
}
double startFreqMHz = fft->m_centerFrequency/1e6 - m_settings.m_spectrumSpan / 2.0;
spectrumUpdateXRange(fft);
spectrumUpdateYRange(fft);
m_fftXAxis->setReverse(m_settings.m_spectrumReverseXAxis);
switch (m_settings.m_spectrumYScale)
{
case RadioAstronomySettings::SY_DBFS:
m_fftYAxis->setTitleText("Power (dBFS)");
break;
case RadioAstronomySettings::SY_SNR:
m_fftYAxis->setTitleText("SNR");
break;
case RadioAstronomySettings::SY_DBM:
m_fftYAxis->setTitleText("Power (dBm)");
break;
case RadioAstronomySettings::SY_TSYS:
m_fftYAxis->setTitleText("Tsys (K)");
break;
case RadioAstronomySettings::SY_TSOURCE:
m_fftYAxis->setTitleText("Tsource (K)");
break;
}
// Plot peaks
if (m_settings.m_spectrumPeaks)
{
double peakFreqMHz = (startFreq + peakIdx * binRes) / 1e6;
double peakFreq = peakFreqMHz * 1e6;
m_fftPeakSeries->append(peakFreqMHz, peakValue);
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_PEAK, SPECTRUM_MARKER_COL_FREQ)->setData(Qt::DisplayRole, peakFreqMHz);
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_PEAK, SPECTRUM_MARKER_COL_VALUE)->setData(Qt::DisplayRole, peakValue);
calcVrAndDistanceToPeak(peakFreq, fft, SPECTRUM_MARKER_ROW_PEAK);
}
// Update markers to track current data
if (m_spectrumM1Valid)
{
m_fftMarkerSeries->clear();
int idx;
idx = (m_spectrumM1X - startFreqMHz) / (binRes/1e6);
if ((idx >= 0) && (idx < m_fftSeries->count()))
{
m_spectrumM1Y = m_fftSeries->at(idx).y();
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_M1, SPECTRUM_MARKER_COL_VALUE)->setData(Qt::DisplayRole, m_spectrumM1Y);
m_fftMarkerSeries->append(m_spectrumM1X, m_spectrumM1Y);
calcVrAndDistanceToPeak(m_spectrumM1X*1e6, fft, SPECTRUM_MARKER_ROW_M1);
}
if (m_spectrumM2Valid)
{
idx = (m_spectrumM2X - startFreqMHz) / (binRes/1e6);
if (idx < m_fftSeries->count())
{
m_spectrumM2Y = m_fftSeries->at(idx).y();
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_M2, SPECTRUM_MARKER_COL_VALUE)->setData(Qt::DisplayRole, m_spectrumM2Y);
m_fftMarkerSeries->append(m_spectrumM2X, m_spectrumM2Y);
calcVrAndDistanceToPeak(m_spectrumM2X*1e6, fft, SPECTRUM_MARKER_ROW_M2);
calcSpectrumMarkerDelta();
}
}
}
spectrumAutoscale();
}
}
}
bool RadioAstronomyGUI::losMarkerEnabled(const QString& name)
{
if (m_settings.m_spectrumDistance && m_settings.m_spectrumRefLine)
{
if (name == "Max") {
return m_settings.m_spectrumPeaks;
} else if (name == "M1") {
return m_settings.m_spectrumMarkers;
} else {
return m_settings.m_spectrumMarkers;
}
}
return false;
}
void RadioAstronomyGUI::showLoSMarker(const QString& name)
{
if (losMarkerEnabled(name))
{
if (name == "Max") {
showLoSMarker(SPECTRUM_MARKER_ROW_PEAK);
} else if (name == "M1") {
showLoSMarker(SPECTRUM_MARKER_ROW_M1);
} else {
showLoSMarker(SPECTRUM_MARKER_ROW_M2);
}
}
}
void RadioAstronomyGUI::showLoSMarker(int row)
{
bool ok;
float d = ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_D)->data(Qt::DisplayRole).toFloat(&ok);
if (ok)
{
FFTMeasurement *fft = currentFFT();
if (fft)
{
QString name = ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_NAME)->text();
updateLoSMarker(name, fft->m_l, fft->m_b, d);
}
}
}
void RadioAstronomyGUI::updateLoSMarker(const QString& name, float l, float b, float d)
{
// Send to Star Tracker
QList<ObjectPipe*> starTrackerPipes;
MainCore::instance()->getMessagePipes().getMessagePipes(this, "startracker.display", starTrackerPipes);
for (const auto& pipe : starTrackerPipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
SWGSDRangel::SWGStarTrackerDisplayLoSSettings *swgSettings = new SWGSDRangel::SWGStarTrackerDisplayLoSSettings();
swgSettings->setName(new QString(name));
swgSettings->setL(l);
swgSettings->setB(b);
swgSettings->setD(d);
messageQueue->push(MainCore::MsgStarTrackerDisplayLoSSettings::create(m_radioAstronomy, swgSettings));
}
}
void RadioAstronomyGUI::clearLoSMarker(const QString& name)
{
// Set d to 0 to clear
updateLoSMarker(name, 0.0f, 0.0f, 0.0f);
}
void RadioAstronomyGUI::calcVrAndDistanceToPeak(double freq, FFTMeasurement *fft, int row)
{
double lineFreq = ui->spectrumLineFrequency->value() * 1e6;
// Calculate radial velocity (along line-of-sight) from Doppler shift
double vR = dopplerToVelocity(lineFreq, freq, fft);
ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_VR)->setData(Qt::DisplayRole, vR);
// Tangent point method only valid for Galactic quadrants I (0-90) and IV (270-360)
if ((fft->m_l < 90.0) || (fft->m_l > 270.0))
{
// Calculate minimum distance to Galactic centre along line of sight (tangential radius)
double rMin = m_settings.m_sunDistanceToGC * sin(Units::degreesToRadians(fft->m_l));
ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_R_MIN)->setData(Qt::DisplayRole, rMin);
// Calculate orbital velocity at tangent/minimum point
// This is the velocity to plot for the rotation curve
double w0 = m_settings.m_sunOrbitalVelocity / m_settings.m_sunDistanceToGC;
double vOrb = vR + rMin * w0;
ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_V)->setData(Qt::DisplayRole, vOrb);
}
else
{
ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_R_MIN)->setText("");
ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_V)->setText("");
}
// Calculate distance of HI cloud (as indicated by a peak) to Sun and Galactic center
double r, d1, d2;
int solutions = calcDistanceToPeak(vR, fft->m_l, fft->m_b, r, d1, d2);
if (solutions == 0)
{
ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_R)->setText("");
ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_D)->setText("");
}
else if (solutions == 1)
{
ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_R)->setData(Qt::DisplayRole, r);
ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_D)->setData(Qt::DisplayRole, d1);
}
else
{
ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_R)->setData(Qt::DisplayRole, r);
ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_D)->setText(QString("%1/%2").arg(QString::number(d1, 'f', 1)).arg(QString::number(d2, 'f', 1)));
}
// Send to Star Tracker
QString name = ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_NAME)->text();
if (losMarkerEnabled(name))
{
if ((solutions == 0) || std::isnan(d1))
{
updateLoSMarker(name, fft->m_l, fft->m_b, 0.0f);
}
else if (solutions == 1)
{
updateLoSMarker(name, fft->m_l, fft->m_b, d1);
}
else
{
bool plotMax = ui->spectrumMarkerTable->item(row, SPECTRUM_MARKER_COL_PLOT_MAX)->checkState() == Qt::Checked;
if ((plotMax && (d1 > d2)) || (!plotMax && (d1 < d2))) {
updateLoSMarker(name, fft->m_l, fft->m_b, d1);
} else {
updateLoSMarker(name, fft->m_l, fft->m_b, d2);
}
}
}
}
int RadioAstronomyGUI::calcDistanceToPeak(double vr, float l, float b, double& r, double &d1, double &d2)
{
// Radio Astronomy 4th edition - Burke - p343
double r0 = m_settings.m_sunDistanceToGC; // Distance of Sun to Galactic centre in kpc
double v0 = m_settings.m_sunOrbitalVelocity; // Orbital velocity of the Sun around Galactic centre in km/s
double w0 = v0/r0; // Angular velocity of the Sun
double gl = Units::degreesToRadians(l);
double gb = Units::degreesToRadians(b);
double w = vr/(r0*sin(gl)*cos(gb))+w0;
r = v0/w; // Assume constant v, regardless of distance from GC - Dark matter magic - Not valid <1kpc from GC
if (r < 0) {
return 0;
}
// https://en.wikipedia.org/wiki/Solution_of_triangles#Two_sides_and_non-included_angle_given_(SSA)
double beta = gl;
double d = sin(beta)*r0/r;
if (d > 1.0) {
// No solutions
return 0;
}
if ((r <= r0) && (beta >= M_PI/2.0)) {
return 0;
}
double gamma = asin(d);
double alpha1 = M_PI - beta - gamma;
d1 = r * sin(alpha1)/sin(beta); // Distance from Sun to peak
if (r < r0)
{
double alpha2 = M_PI - beta - (M_PI-gamma);
d2 = r * sin(alpha2)/sin(beta); // Distance from Sun to peak
return 2;
}
else
{
return 1;
}
}
void RadioAstronomyGUI::spectrumMarkerTableItemChanged(QTableWidgetItem *item)
{
if (item->column() == SPECTRUM_MARKER_COL_PLOT_MAX) {
// Plot max checkbox clicked
calcDistances();
}
}
void RadioAstronomyGUI::calcDistances()
{
double freq;
bool ok;
FFTMeasurement* fft = currentFFT();
if (fft)
{
freq = ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_PEAK, SPECTRUM_MARKER_COL_FREQ)->data(Qt::DisplayRole).toDouble(&ok);
if (ok) {
calcVrAndDistanceToPeak(freq*1e6, fft, SPECTRUM_MARKER_ROW_PEAK);
}
freq = ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_M1, SPECTRUM_MARKER_COL_FREQ)->data(Qt::DisplayRole).toDouble(&ok);
if (ok) {
calcVrAndDistanceToPeak(freq*1e6, fft, SPECTRUM_MARKER_ROW_M1);
}
freq = ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_M2, SPECTRUM_MARKER_COL_FREQ)->data(Qt::DisplayRole).toDouble(&ok);
if (ok) {
calcVrAndDistanceToPeak(freq*1e6, fft, SPECTRUM_MARKER_ROW_M2);
}
}
}
void RadioAstronomyGUI::plotRefLine(FFTMeasurement *fft)
{
double lineFreqMHz = ui->spectrumLineFrequency->value();
double lineFreq = lineFreqMHz * 1e6;
QString refFrame[] = {"Topocentric", "Barycentric", "LSR"};
m_fftDopplerAxis->setTitleText(QString("%1 radial velocity (km/s - +ve receeding)").arg(refFrame[m_settings.m_refFrame]));
m_fftHlineSeries->setName(QString("%1 line").arg(ui->spectrumLine->currentText()));
m_fftHlineSeries->append(0.0f, -200.0f); // For dB
m_fftHlineSeries->append(0.0f, 10000.0f); // For temp can be >1e6?
double startFreq = fft->m_centerFrequency + m_settings.m_spectrumSpan*1e6 / 2.0 + m_settings.m_spectrumCenterFreqOffset*1e6;
double endFreq = fft->m_centerFrequency - m_settings.m_spectrumSpan*1e6 / 2.0 + m_settings.m_spectrumCenterFreqOffset*1e6;
m_fftDopplerAxis->setRange(dopplerToVelocity(lineFreq, startFreq, fft),
dopplerToVelocity(lineFreq, endFreq, fft));
m_fftDopplerAxis->setReverse(!m_settings.m_spectrumReverseXAxis);
m_fftDopplerAxis->setVisible(m_settings.m_spectrumRefLine);
}
void RadioAstronomyGUI::plotTempGaussian(double startFreq, double freqStep, int steps)
{
m_fftGaussianSeries->clear();
double f0 = ui->spectrumGaussianFreq->value() * 1e6;
double a = ui->spectrumGaussianAmp->value();
double floor = ui->spectrumGaussianFloor->value();
double fwhm = ui->spectrumGaussianFWHM->value();
double fwhm_sq = fwhm*fwhm;
double freq = startFreq;
for (int i = 0; i < steps; i++)
{
double fd = freq - f0;
double g = a * std::exp(-4.0*M_LN2*fd*fd/fwhm_sq) + floor;
m_fftGaussianSeries->append(freq/1e6, g);
freq += freqStep;
}
}
void RadioAstronomyGUI::calcFFTPower(FFTMeasurement* fft)
{
// Convert linear to dB
for (int i = 0; i < fft->m_fftSize; i++) {
fft->m_db[i] = CalcDb::dbPower(fft->m_fftData[i]);
}
}
void RadioAstronomyGUI::calcFFTTotalPower(FFTMeasurement* fft)
{
double total = 0.0;
for (int i = 0; i < fft->m_fftSize; i++) {
total += fft->m_fftData[i];
}
fft->m_totalPower = total;
fft->m_totalPowerdBFS = CalcDb::dbPower(total);
}
void RadioAstronomyGUI::calcFFTTemperatures(FFTMeasurement* fft)
{
if (m_calCold && !fft->m_snr) {
fft->m_snr = new Real[fft->m_fftSize];
}
if (m_calG && !fft->m_temp) {
fft->m_temp = new Real[fft->m_fftSize];
}
for (int i = 0; i < fft->m_fftSize; i++)
{
if (fft->m_snr && m_calCold)
{
// Calculate SNR (relative to cold cal)
fft->m_snr[i] = fft->m_fftData[i] / m_calCold->m_fftData[i];
}
if (m_calG)
{
// Calculate temperature using scaling from hot cal
fft->m_temp[i] = m_calG[i] * fft->m_fftData[i];
// Calculate temperature using linear interpolation from hot/cold cal
//fft->m_temp[i] = m_calG[i] * (fft->m_fftData[i] - m_calCold->m_fftData[i]) + m_settings.m_tCalCold;
//fft->m_temp[i] = std::max(fft->m_temp[i], 0.0f); // Can't have negative temperatures
}
}
calcFFTMinTemperature(fft);
}
void RadioAstronomyGUI::calcFFTMinTemperature(FFTMeasurement* fft)
{
fft->m_tempMin = 0;
if (fft->m_temp)
{
// Select minimum from within band. 95% of that to account for a little bit of inband rolloff
float tempMin = std::numeric_limits<float>::max();
double pc = 0.95 * fft->m_rfBandwidth / (double)fft->m_sampleRate;
int count = (int)(pc * fft->m_fftSize);
int start = (fft->m_fftSize - count) / 2;
for (int i = 0; i < count; i++)
{
int idx = i + start;
tempMin = std::min(tempMin, fft->m_temp[idx]);
}
if (tempMin != std::numeric_limits<float>::max()) {
fft->m_tempMin = tempMin;
}
}
}
// Estimate Tsource by subtracting selected baseline
double RadioAstronomyGUI::calcTSource(FFTMeasurement *fft) const
{
switch (fft->m_baseline)
{
case RadioAstronomySettings::SBL_TSYS0:
return fft->m_tSys - fft->m_tSys0;
case RadioAstronomySettings::SBL_TMIN:
return fft->m_tSys - fft->m_tempMin;
case RadioAstronomySettings::SBL_CAL_COLD:
if (m_calCold) {
return fft->m_tSys - m_calCold->m_tSys;
}
break;
}
return fft->m_tSys;
}
// Calculate spectral flux density of source
double RadioAstronomyGUI::calcFlux(double Ta, const FFTMeasurement *fft) const
{
// Factor of 2 here assumes single polarization
// See equation 5.13 and 5.20 in Radio Astronomy 4th edition - Burke
double lambda = Astronomy::m_speedOfLight / (double)fft->m_centerFrequency;
return 2.0 * Astronomy::m_boltzmann * Ta * fft->m_omegaA / (lambda * lambda);
}
void RadioAstronomyGUI::calcFFTTotalTemperature(FFTMeasurement* fft)
{
if (fft->m_temp)
{
double tempSum = 0.0;
for (int i = 0; i < fft->m_fftSize; i++) {
tempSum += fft->m_temp[i];
}
// Convert from temperature to power in Watts and dBm
Real bw = fft->m_sampleRate/(Real)fft->m_fftSize;
fft->m_totalPowerWatts = Astronomy::m_boltzmann * tempSum * bw;
fft->m_totalPowerdBm = Astronomy::noisePowerdBm(tempSum, bw);
fft->m_tSys = tempSum/fft->m_fftSize;
// Esimate source temperature
fft->m_tSource = calcTSource(fft);
// Calculate error due to thermal noise and gain variation
fft->m_sigmaT = calcSigmaT(fft);
fft->m_sigmaS = calcSigmaS(fft);
// Calculate spectral flux density of source
fft->m_flux = calcFlux(fft->m_tSource, fft);
}
}
void RadioAstronomyGUI::updatePowerColumns(int row, FFTMeasurement* fft)
{
ui->powerTable->item(row, POWER_COL_TSYS)->setData(Qt::DisplayRole, fft->m_tSys);
ui->powerTable->item(row, POWER_COL_TSYS0)->setData(Qt::DisplayRole, fft->m_tSys0);
ui->powerTable->item(row, POWER_COL_TSOURCE)->setData(Qt::DisplayRole, fft->m_tSource);
if (m_settings.m_sourceType != RadioAstronomySettings::UNKNOWN) {
ui->powerTable->item(row, POWER_COL_TB)->setData(Qt::DisplayRole, fft->m_tSource/beamFillingFactor());
} else {
ui->powerTable->item(row, POWER_COL_TB)->setText("");
}
ui->powerTable->item(row, POWER_COL_FLUX)->setData(Qt::DisplayRole, Units::wattsPerMetrePerHertzToJansky(fft->m_flux));
ui->powerTable->item(row, POWER_COL_SIGMA_T)->setData(Qt::DisplayRole, fft->m_sigmaT);
ui->powerTable->item(row, POWER_COL_SIGMA_S)->setData(Qt::DisplayRole, fft->m_sigmaS);
ui->powerTable->item(row, POWER_COL_OMEGA_A)->setData(Qt::DisplayRole, fft->m_omegaA);
ui->powerTable->item(row, POWER_COL_OMEGA_S)->setData(Qt::DisplayRole, fft->m_omegaS);
}
void RadioAstronomyGUI::addFFT(FFTMeasurement *fft, bool skipCalcs)
{
m_fftMeasurements.append(fft);
powerMeasurementReceived(fft, skipCalcs); // Call before ui->spectrumIndex->setValue, so table row is valid
update2DImage(fft, skipCalcs);
ui->spectrumIndex->setRange(0, m_fftMeasurements.size() - 1);
if ((ui->spectrumIndex->value() == m_fftMeasurements.size() - 2) || (m_fftMeasurements.size() == 1)) {
ui->spectrumIndex->setValue(m_fftMeasurements.size() - 1);
}
if ( m_fftMeasurements.size() == 1)
{
// Force drawing for first measurement
on_spectrumIndex_valueChanged(0);
}
}
void RadioAstronomyGUI::fftMeasurementReceived(const RadioAstronomy::MsgFFTMeasurement& measurement)
{
FFTMeasurement *fft = new FFTMeasurement();
fft->m_fftData = measurement.getFFT();
fft->m_fftSize = measurement.getSize();
fft->m_dateTime = measurement.getDateTime();
fft->m_centerFrequency = m_centerFrequency;
fft->m_sampleRate = m_settings.m_sampleRate;
fft->m_integration = m_settings.m_integration;
fft->m_rfBandwidth = m_settings.m_rfBandwidth;
fft->m_omegaA = calcOmegaA();
fft->m_omegaS = calcOmegaS();
fft->m_coordsValid = m_coordsValid;
fft->m_ra = m_ra;
fft->m_dec = m_dec;
fft->m_azimuth = m_azimuth;
fft->m_elevation = m_elevation;
fft->m_l = m_l;
fft->m_b = m_b;
fft->m_vBCRS = m_vBCRS;
fft->m_vLSR = m_vLSR;
fft->m_solarFlux = m_solarFlux;
fft->m_airTemp = m_airTemps.lastValue();
fft->m_skyTemp = m_skyTemp;
for (int i = 0; i < RADIOASTRONOMY_SENSORS; i++) {
fft->m_sensor[i] = m_sensors[i].lastValue();
}
fft->m_db = new Real[fft->m_fftSize];
fft->m_sweepIndex = m_sweepIndex++;
fft->m_tSys0 = calcTSys0();
fft->m_baseline = m_settings.m_spectrumBaseline;
calcFFTPower(fft);
calcFFTTotalPower(fft);
calcFFTTemperatures(fft);
calcFFTTotalTemperature(fft);
addFFT(fft);
}
void RadioAstronomyGUI::on_spectrumIndex_valueChanged(int value)
{
if (value < m_fftMeasurements.size())
{
plotFFTMeasurement(value);
// Highlight in table
ui->powerTable->selectRow(value);
ui->powerTable->scrollTo(ui->powerTable->model()->index(value, 0));
ui->spectrumDateTime->setDateTime(m_fftMeasurements[value]->m_dateTime);
// Display target in Star Tracker
QList<ObjectPipe*> starTrackerPipes;
MainCore::instance()->getMessagePipes().getMessagePipes(this, "startracker.display", starTrackerPipes);
for (const auto& pipe : starTrackerPipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
SWGSDRangel::SWGStarTrackerDisplaySettings *swgSettings = new SWGSDRangel::SWGStarTrackerDisplaySettings();
swgSettings->setDateTime(new QString(m_fftMeasurements[value]->m_dateTime.toString(Qt::ISODateWithMs)));
swgSettings->setAzimuth(m_fftMeasurements[value]->m_azimuth);
swgSettings->setElevation(m_fftMeasurements[value]->m_elevation);
messageQueue->push(MainCore::MsgStarTrackerDisplaySettings::create(m_radioAstronomy, swgSettings));
}
}
}
void RadioAstronomyGUI::plotSpectrum()
{
QChart *oldChart = m_fftChart;
m_fftChart = new QChart();
m_fftChart->layout()->setContentsMargins(0, 0, 0, 0);
m_fftChart->setMargins(QMargins(1, 1, 1, 1));
m_fftChart->setTheme(QChart::ChartThemeDark);
m_fftChart->legend()->setAlignment(Qt::AlignRight);
m_fftChart->legend()->setVisible(m_settings.m_spectrumLegend);
m_fftSeries = new QLineSeries();
m_fftSeries->setName("Measurement");
connect(m_fftSeries, &QXYSeries::clicked, this, &RadioAstronomyGUI::spectrumSeries_clicked);
// Plot vertical reference spectral line
m_fftHlineSeries = new QLineSeries();
m_fftHlineSeries->setName(QString("%1 line").arg(ui->spectrumLine->currentText()));
m_fftHlineSeries->setVisible(m_settings.m_spectrumRefLine);
// Plot peak info
m_fftPeakSeries = new QScatterSeries();
m_fftPeakSeries->setPointLabelsVisible(true);
m_fftPeakSeries->setMarkerSize(5);
m_fftPeakSeries->setName("Max");
// Markers
m_fftMarkerSeries = new QScatterSeries();
m_fftMarkerSeries->setPointLabelsVisible(true);
m_fftMarkerSeries->setMarkerSize(5);
m_fftMarkerSeries->setName("Markers");
// Gaussian
m_fftGaussianSeries = new QLineSeries();
m_fftGaussianSeries->setName("Gaussian fit");
m_fftGaussianSeries->setVisible(m_settings.m_spectrumTemp);
m_fftLABSeries = new QLineSeries();
m_fftLABSeries->setName("LAB reference");
m_fftLABSeries->setVisible(m_settings.m_spectrumLAB);
m_fftXAxis = new QValueAxis();
m_fftYAxis = new QValueAxis();
m_fftDopplerAxis = new QValueAxis();
m_fftChart->addAxis(m_fftXAxis, Qt::AlignBottom);
m_fftChart->addAxis(m_fftYAxis, Qt::AlignLeft);
m_fftChart->addAxis(m_fftDopplerAxis, Qt::AlignTop);
m_fftXAxis->setTitleText("Frequency (MHz)");
calcSpectrumChartTickCount(m_fftXAxis, size().width());
calcSpectrumChartTickCount(m_fftDopplerAxis, size().width());
m_fftYAxis->setTitleText("Power");
m_fftChart->addSeries(m_fftSeries);
m_fftSeries->attachAxis(m_fftXAxis);
m_fftSeries->attachAxis(m_fftYAxis);
m_fftChart->addSeries(m_fftHlineSeries);
//m_fftHlineSeries->attachAxis(m_fftXAxis);
m_fftHlineSeries->attachAxis(m_fftDopplerAxis);
m_fftHlineSeries->attachAxis(m_fftYAxis);
m_fftChart->addSeries(m_fftGaussianSeries);
m_fftGaussianSeries->attachAxis(m_fftXAxis);
m_fftGaussianSeries->attachAxis(m_fftYAxis);
m_fftChart->addSeries(m_fftLABSeries);
//m_fftLABSeries->attachAxis(m_fftXAxis);
m_fftLABSeries->attachAxis(m_fftDopplerAxis);
m_fftLABSeries->attachAxis(m_fftYAxis);
m_fftChart->addSeries(m_fftPeakSeries);
m_fftPeakSeries->attachAxis(m_fftXAxis);
m_fftPeakSeries->attachAxis(m_fftYAxis);
m_fftChart->addSeries(m_fftMarkerSeries);
m_fftMarkerSeries->attachAxis(m_fftXAxis);
m_fftMarkerSeries->attachAxis(m_fftYAxis);
// Don't have peaks and markers in legend
m_fftChart->legend()->markers(m_fftPeakSeries)[0]->setVisible(false);
m_fftChart->legend()->markers(m_fftMarkerSeries)[0]->setVisible(false);
ui->spectrumChart->setChart(m_fftChart);
delete oldChart;
}
// Calculate galactic background temperature based on center frequency
void RadioAstronomyGUI::calcGalacticBackgroundTemp()
{
// https://arxiv.org/ftp/arxiv/papers/1912/1912.12699.pdf - page 6
// 17.1, 25.2 and 54.8 K for the 10th, 50th and 90th percentile of the all-sky distribution
// See also ITU-R P.372-7 section 6
// If this is used for cold calibration, we don't want to use the higher value
double temp = 25.2 * std::pow(m_centerFrequency/408000000.0, -2.75);
ui->tempGal->setValue(temp);
}
// Calculate athmospheric noise temperature based on air temperature, zenith opacity and elevation
void RadioAstronomyGUI::calcAtmosphericTemp()
{
float el = m_settings.m_elevation;
if (m_settings.m_elevation < 1.0f) {
el = 1.0f; // Avoid divide by 0 and limit max value to match ITU-R P.372-7 figure 5
}
double temp = Units::celsiusToKelvin(m_settings.m_tempAir) * (1.0 - std::exp(-m_settings.m_zenithOpacity/cos(Units::degreesToRadians(90.0f - el))));
ui->tempAtm->setValue(temp);
}
void RadioAstronomyGUI::on_tempRXSelect_currentIndexChanged(int value)
{
if (value == 0)
{
// T_RX
ui->tempRX->setValue(m_settings.m_tempRX);
ui->tempRXUnitsLabel->setText("K");
}
else
{
// NF
ui->tempRX->setValue(Units::noiseTempToNoiseFigureTo(m_settings.m_tempRX));
ui->tempRXUnitsLabel->setText("dB");
}
}
void RadioAstronomyGUI::on_tempRX_valueChanged(double value)
{
if (ui->tempRXSelect->currentIndex() == 0) {
m_settings.m_tempRX = value;
} else {
m_settings.m_tempRX = Units::noiseFigureToNoiseTemp(value);
}
updateTSys0();
applySettings();
}
void RadioAstronomyGUI::on_tempCMB_valueChanged(double value)
{
m_settings.m_tempCMB = value;
updateTSys0();
applySettings();
}
void RadioAstronomyGUI::on_tempGal_valueChanged(double value)
{
m_settings.m_tempGal = value;
updateTSys0();
applySettings();
}
void RadioAstronomyGUI::on_tempSP_valueChanged(double value)
{
m_settings.m_tempSP = value;
updateTSys0();
applySettings();
}
void RadioAstronomyGUI::on_tempAtm_valueChanged(double value)
{
m_settings.m_tempAtm = value;
updateTSys0();
applySettings();
}
void RadioAstronomyGUI::on_tempAir_valueChanged(double value)
{
m_settings.m_tempAir = value;
if (m_settings.m_tempAtmLink) {
calcAtmosphericTemp();
}
applySettings();
}
void RadioAstronomyGUI::on_zenithOpacity_valueChanged(double value)
{
m_settings.m_zenithOpacity = value;
if (m_settings.m_tempAtmLink) {
calcAtmosphericTemp();
}
applySettings();
}
void RadioAstronomyGUI::on_elevation_valueChanged(double value)
{
m_settings.m_elevation = value;
if (m_settings.m_tempAtmLink) {
calcAtmosphericTemp();
}
applySettings();
}
void RadioAstronomyGUI::on_elevationLink_toggled(bool checked)
{
m_settings.m_elevationLink = checked;
ui->elevation->setValue(m_elevation);
ui->elevation->setEnabled(!m_settings.m_elevationLink);
applySettings();
}
void RadioAstronomyGUI::on_tempAtmLink_toggled(bool checked)
{
m_settings.m_tempAtmLink = checked;
ui->tempAtm->setEnabled(!m_settings.m_tempAtmLink);
if (checked) {
calcAtmosphericTemp();
}
applySettings();
}
void RadioAstronomyGUI::on_tempAirLink_toggled(bool checked)
{
m_settings.m_tempAirLink = checked;
ui->tempAir->setEnabled(!m_settings.m_tempAirLink);
if (checked)
{
ui->tempAir->setValue(m_airTemps.lastValue());
calcAtmosphericTemp();
}
applySettings();
}
void RadioAstronomyGUI::on_tempGalLink_toggled(bool checked)
{
m_settings.m_tempGalLink = checked;
if (checked) {
calcGalacticBackgroundTemp();
}
ui->tempGal->setEnabled(!m_settings.m_tempGalLink);
applySettings();
}
void RadioAstronomyGUI::on_tCalHotSelect_currentIndexChanged(int value)
{
if (value == 0)
{
// Thot
ui->tCalHot->setValue(m_settings.m_tCalHot);
ui->tCalHotUnitsLabel->setText("K");
}
else
{
// Phot
double power = Astronomy::noisePowerdBm(m_settings.m_tCalHot, m_settings.m_sampleRate);
ui->tCalHot->setValue(power);
ui->tCalHotUnitsLabel->setText("dBm");
}
}
void RadioAstronomyGUI::on_tCalHot_valueChanged(double value)
{
double temp;
if (ui->tCalHotSelect->currentIndex() == 0) {
temp = value;
} else {
temp = Astronomy::noiseTemp(value, m_settings.m_sampleRate);
}
m_settings.m_tCalHot = (float)temp;
calibrate();
applySettings();
}
void RadioAstronomyGUI::on_tCalColdSelect_currentIndexChanged(int value)
{
if (value == 0)
{
// Tcold
ui->tCalCold->setValue(m_settings.m_tCalCold);
ui->tCalColdUnitsLabel->setText("K");
}
else
{
// Pcold
double power = Astronomy::noisePowerdBm(m_settings.m_tCalCold, m_settings.m_sampleRate);
ui->tCalCold->setValue(power);
ui->tCalColdUnitsLabel->setText("dBm");
}
}
void RadioAstronomyGUI::on_tCalCold_valueChanged(double value)
{
double temp;
if (ui->tCalColdSelect->currentIndex() == 0) {
temp = value;
} else {
temp = Astronomy::noiseTemp(value, m_settings.m_sampleRate);
}
m_settings.m_tCalCold = (float)temp;
calibrate();
applySettings();
}
void RadioAstronomyGUI::on_spectrumLine_currentIndexChanged(int value)
{
m_settings.m_line = (RadioAstronomySettings::Line)value;
displaySpectrumLineFrequency();
plotFFTMeasurement();
applySettings();
}
void RadioAstronomyGUI::displaySpectrumLineFrequency()
{
switch (m_settings.m_line)
{
case RadioAstronomySettings::HI:
ui->spectrumLineFrequency->setValue(Astronomy::m_hydrogenLineFrequency / 1e6);
ui->spectrumLineFrequency->setEnabled(false);
break;
case RadioAstronomySettings::OH:
ui->spectrumLineFrequency->setValue(Astronomy::m_hydroxylLineFrequency / 1e6);
ui->spectrumLineFrequency->setEnabled(false);
break;
case RadioAstronomySettings::DI:
ui->spectrumLineFrequency->setValue(Astronomy::m_deuteriumLineFrequency / 1e6);
ui->spectrumLineFrequency->setEnabled(false);
break;
case RadioAstronomySettings::CUSTOM_LINE:
ui->spectrumLineFrequency->setValue(m_settings.m_lineCustomFrequency / 1e6);
ui->spectrumLineFrequency->setEnabled(true);
break;
}
}
void RadioAstronomyGUI::on_spectrumLineFrequency_valueChanged(double value)
{
m_settings.m_lineCustomFrequency = value * 1e6;
plotFFTMeasurement();
applySettings();
}
void RadioAstronomyGUI::on_refFrame_currentIndexChanged(int value)
{
m_settings.m_refFrame = (RadioAstronomySettings::RefFrame)value;
plotFFTMeasurement();
applySettings();
}
void RadioAstronomyGUI::on_sunDistanceToGC_valueChanged(double value)
{
m_settings.m_sunDistanceToGC = value;
applySettings();
calcDistances();
}
void RadioAstronomyGUI::on_sunOrbitalVelocity_valueChanged(double value)
{
m_settings.m_sunOrbitalVelocity = value;
applySettings();
calcDistances();
}
void RadioAstronomyGUI::on_savePowerChartImage_clicked()
{
QFileDialog fileDialog(nullptr, "Select file to save image to", "", "*.png;*.jpg;*.jpeg;*.bmp;*.ppm;*.xbm;*.xpm");
fileDialog.setAcceptMode(QFileDialog::AcceptSave);
if (fileDialog.exec())
{
QStringList fileNames = fileDialog.selectedFiles();
if (fileNames.size() > 0)
{
QImage image(ui->powerChart->size(), QImage::Format_ARGB32);
image.fill(Qt::transparent);
QPainter painter(&image);
ui->powerChart->render(&painter);
if (!image.save(fileNames[0])) {
QMessageBox::critical(this, "Radio Astronomy", QString("Failed to save image to %1").arg(fileNames[0]));
}
}
}
}
void RadioAstronomyGUI::on_saveSpectrumChartImage_clicked()
{
QFileDialog fileDialog(nullptr, "Select file to save image to", "", "*.png;*.jpg;*.jpeg;*.bmp;*.ppm;*.xbm;*.xpm");
fileDialog.setAcceptMode(QFileDialog::AcceptSave);
if (fileDialog.exec())
{
QStringList fileNames = fileDialog.selectedFiles();
if (fileNames.size() > 0)
{
QImage image(ui->spectrumChart->size(), QImage::Format_ARGB32);
image.fill(Qt::transparent);
QPainter painter(&image);
ui->spectrumChart->render(&painter);
if (!image.save(fileNames[0])) {
QMessageBox::critical(this, "Radio Astronomy", QString("Failed to save image to %1").arg(fileNames[0]));
}
}
}
}
void RadioAstronomyGUI::on_saveSpectrumChartImages_clicked()
{
if (m_fftMeasurements.size() > 1)
{
// Get filename of animation file
QFileDialog fileDialog(nullptr, "Select file to save animation to", "", "*.png");
fileDialog.setAcceptMode(QFileDialog::AcceptSave);
if (fileDialog.exec())
{
QStringList fileNames = fileDialog.selectedFiles();
if (fileNames.size() > 0)
{
// Create animation file
APNG apng(m_fftMeasurements.size());
// Plot each FFT to a temp .png file (in memory) then append to animation file
for (int i = 0; i < m_fftMeasurements.size(); i++)
{
plotFFTMeasurement(i);
QApplication::processEvents(); // To get chart title to be updated
QImage image(ui->spectrumChart->size(), QImage::Format_ARGB32);
image.fill(Qt::transparent);
QPainter painter(&image);
ui->spectrumChart->render(&painter);
apng.addImage(image);
}
if (!apng.save(fileNames[0])) {
QMessageBox::critical(this, "Radio Astronomy", QString("Failed to write to file %1").arg(fileNames[0]));
}
}
}
}
}
void RadioAstronomyGUI::on_spectrumReverseXAxis_toggled(bool checked)
{
m_settings.m_spectrumReverseXAxis = checked;
applySettings();
if (ui->spectrumChartSelect->currentIndex() == 0) {
plotFFTMeasurement();
} else {
m_calXAxis->setReverse(m_settings.m_spectrumReverseXAxis);
}
}
void RadioAstronomyGUI::on_powerShowPeak_toggled(bool checked)
{
m_settings.m_powerPeaks = checked;
updatePowerMarkerTableVisibility();
applySettings();
if (m_powerPeakSeries)
{
m_powerPeakSeries->setVisible(checked);
if (checked) {
m_powerChart->legend()->markers(m_powerPeakSeries)[0]->setVisible(false);
}
}
getRollupContents()->arrangeRollups();
}
void RadioAstronomyGUI::on_spectrumPeak_toggled(bool checked)
{
m_settings.m_spectrumPeaks = checked;
updateSpectrumMarkerTableVisibility();
plotFFTMeasurement();
applySettings();
if (m_fftChart)
{
if (checked) {
m_fftChart->legend()->markers(m_fftPeakSeries)[0]->setVisible(false);
showLoSMarker("Max");
} else {
clearLoSMarker("Max");
}
}
getRollupContents()->arrangeRollups();
}
void RadioAstronomyGUI::on_powerShowMarker_toggled(bool checked)
{
m_settings.m_powerMarkers = checked;
updatePowerMarkerTableVisibility();
applySettings();
if (m_powerMarkerSeries)
{
m_powerMarkerSeries->setVisible(checked);
if (checked) {
m_powerChart->legend()->markers(m_powerMarkerSeries)[0]->setVisible(false);
}
}
updatePowerSelect();
getRollupContents()->arrangeRollups();
}
void RadioAstronomyGUI::on_powerShowAvg_toggled(bool checked)
{
m_settings.m_powerAvg = checked;
applySettings();
ui->powerChartAvgWidgets->setVisible(checked);
getRollupContents()->arrangeRollups();
if (checked) {
calcAverages();
}
}
void RadioAstronomyGUI::on_powerShowLegend_toggled(bool checked)
{
m_settings.m_powerLegend = checked;
applySettings();
if (m_powerChart)
{
if (checked) {
m_powerChart->legend()->show();
} else {
m_powerChart->legend()->hide();
}
}
}
void RadioAstronomyGUI::on_powerShowTsys0_toggled(bool checked)
{
m_settings.m_powerShowTsys0 = checked;
applySettings();
if (m_powerTsys0Series) {
m_powerTsys0Series->setVisible(checked);
}
}
void RadioAstronomyGUI::on_powerShowAirTemp_toggled(bool checked)
{
m_settings.m_powerShowAirTemp = checked;
applySettings();
m_airTemps.clicked(checked);
}
void RadioAstronomyGUI::on_powerShowSensor1_toggled(bool checked)
{
m_settings.m_sensorVisible[0] = checked;
applySettings();
m_sensors[0].clicked(checked);
}
void RadioAstronomyGUI::on_powerShowSensor2_toggled(bool checked)
{
m_settings.m_sensorVisible[1] = checked;
applySettings();
m_sensors[1].clicked(checked);
}
void RadioAstronomyGUI::updatePowerMarkerTableVisibility()
{
ui->powerMarkerTableWidgets->setVisible(m_settings.m_powerPeaks || m_settings.m_powerMarkers);
if (m_settings.m_powerPeaks)
{
ui->powerMarkerTable->showRow(POWER_MARKER_ROW_PEAK_MAX);
ui->powerMarkerTable->showRow(POWER_MARKER_ROW_PEAK_MIN);
}
else
{
ui->powerMarkerTable->hideRow(POWER_MARKER_ROW_PEAK_MAX);
ui->powerMarkerTable->hideRow(POWER_MARKER_ROW_PEAK_MIN);
}
if (m_settings.m_powerMarkers)
{
ui->powerMarkerTable->showRow(POWER_MARKER_ROW_M1);
ui->powerMarkerTable->showRow(POWER_MARKER_ROW_M2);
}
else
{
ui->powerMarkerTable->hideRow(POWER_MARKER_ROW_M1);
ui->powerMarkerTable->hideRow(POWER_MARKER_ROW_M2);
}
ui->powerMarkerTableWidgets->updateGeometry(); // Without this, widgets aren't resized properly
}
void RadioAstronomyGUI::updateSpectrumMarkerTableVisibility()
{
bool fft = ui->spectrumChartSelect->currentIndex() == 0;
ui->spectrumMarkerTableWidgets->setVisible(fft && (m_settings.m_spectrumPeaks || m_settings.m_spectrumMarkers));
if (m_settings.m_spectrumPeaks)
{
ui->spectrumMarkerTable->showRow(SPECTRUM_MARKER_ROW_PEAK);
}
else
{
ui->spectrumMarkerTable->hideRow(SPECTRUM_MARKER_ROW_PEAK);
}
if (m_settings.m_spectrumMarkers)
{
ui->spectrumMarkerTable->showRow(SPECTRUM_MARKER_ROW_M1);
ui->spectrumMarkerTable->showRow(SPECTRUM_MARKER_ROW_M2);
}
else
{
ui->spectrumMarkerTable->hideRow(SPECTRUM_MARKER_ROW_M1);
ui->spectrumMarkerTable->hideRow(SPECTRUM_MARKER_ROW_M2);
}
ui->spectrumMarkerTableWidgets->updateGeometry(); // Without this, widgets aren't resized properly
}
void RadioAstronomyGUI::on_spectrumMarker_toggled(bool checked)
{
m_settings.m_spectrumMarkers = checked;
applySettings();
updateSpectrumMarkerTableVisibility();
m_fftMarkerSeries->setVisible(checked);
if (checked)
{
m_fftChart->legend()->markers(m_fftMarkerSeries)[0]->setVisible(false);
showLoSMarker("M1");
showLoSMarker("M2");
}
else
{
clearLoSMarker("M1");
clearLoSMarker("M2");
}
updateSpectrumSelect();
getRollupContents()->arrangeRollups();
}
void RadioAstronomyGUI::on_spectrumTemp_toggled(bool checked)
{
m_settings.m_spectrumTemp = checked;
applySettings();
ui->spectrumGaussianWidgets->setVisible(checked);
m_fftGaussianSeries->setVisible(checked);
updateSpectrumSelect();
getRollupContents()->arrangeRollups();
}
void RadioAstronomyGUI::on_spectrumShowLegend_toggled(bool checked)
{
m_settings.m_spectrumLegend = checked;
applySettings();
if (m_fftChart)
{
m_fftChart->legend()->setVisible(checked);
m_calChart->legend()->setVisible(checked);
}
}
void RadioAstronomyGUI::on_spectrumShowRefLine_toggled(bool checked)
{
m_settings.m_spectrumRefLine = checked;
applySettings();
ui->spectrumRefLineWidgets->setVisible(checked);
if (m_fftHlineSeries)
{
m_fftHlineSeries->setVisible(m_settings.m_spectrumRefLine);
m_fftDopplerAxis->setVisible(m_settings.m_spectrumRefLine);
}
updateDistanceColumns();
getRollupContents()->arrangeRollups();
}
void RadioAstronomyGUI::on_spectrumShowLAB_toggled(bool checked)
{
m_settings.m_spectrumLAB = checked;
applySettings();
m_fftLABSeries->setVisible(m_settings.m_spectrumLAB);
if (m_settings.m_spectrumLAB) {
plotLAB(); // Replot incase data needs to be downloaded
}
spectrumAutoscale();
}
void RadioAstronomyGUI::updateDistanceColumns()
{
if (m_settings.m_spectrumDistance && m_settings.m_spectrumRefLine)
{
ui->spectrumMarkerTable->showColumn(SPECTRUM_MARKER_COL_R);
ui->spectrumMarkerTable->showColumn(SPECTRUM_MARKER_COL_D);
ui->spectrumMarkerTable->showColumn(SPECTRUM_MARKER_COL_PLOT_MAX);
ui->spectrumMarkerTable->showColumn(SPECTRUM_MARKER_COL_R_MIN);
ui->spectrumMarkerTable->showColumn(SPECTRUM_MARKER_COL_V);
showLoSMarker("Max");
showLoSMarker("M1");
showLoSMarker("M2");
ui->sunDistanceToGCLine->setVisible(true);
ui->sunDistanceToGCLabel->setVisible(true);
ui->sunDistanceToGC->setVisible(true);
ui->sunDistanceToGCUnits->setVisible(true);
ui->sunOrbitalVelocityLine->setVisible(true);
ui->sunOrbitalVelocityLabel->setVisible(true);
ui->sunOrbitalVelocity->setVisible(true);
ui->sunOrbitalVelocityUnits->setVisible(true);
}
else
{
ui->spectrumMarkerTable->hideColumn(SPECTRUM_MARKER_COL_R);
ui->spectrumMarkerTable->hideColumn(SPECTRUM_MARKER_COL_D);
ui->spectrumMarkerTable->hideColumn(SPECTRUM_MARKER_COL_PLOT_MAX);
ui->spectrumMarkerTable->hideColumn(SPECTRUM_MARKER_COL_R_MIN);
ui->spectrumMarkerTable->hideColumn(SPECTRUM_MARKER_COL_V);
clearLoSMarker("Max");
clearLoSMarker("M1");
clearLoSMarker("M2");
ui->sunDistanceToGCLine->setVisible(false);
ui->sunDistanceToGCLabel->setVisible(false);
ui->sunDistanceToGC->setVisible(false);
ui->sunDistanceToGCUnits->setVisible(false);
ui->sunOrbitalVelocityLine->setVisible(false);
ui->sunOrbitalVelocityLabel->setVisible(false);
ui->sunOrbitalVelocity->setVisible(false);
ui->sunOrbitalVelocityUnits->setVisible(false);
}
}
void RadioAstronomyGUI::on_spectrumShowDistance_toggled(bool checked)
{
m_settings.m_spectrumDistance = checked;
applySettings();
if (m_settings.m_spectrumDistance && !m_settings.m_spectrumRefLine) {
ui->spectrumShowRefLine->setChecked(true);
}
updateDistanceColumns();
}
// point isn't necessarily a point in the series - may be interpolated
void RadioAstronomyGUI::powerSeries_clicked(const QPointF &point)
{
QString selection = ui->powerSelect->currentText();
if (selection.startsWith("M"))
{
if (selection == "M1")
{
// Place marker 1
m_powerM1X = point.x();
m_powerM1Y = point.y();
if (m_powerM1Valid) {
m_powerMarkerSeries->replace(0, m_powerM1X, m_powerM1Y);
} else {
m_powerMarkerSeries->insert(0, QPointF(m_powerM1X, m_powerM1Y));
}
m_powerM1Valid = true;
QDateTime dt = QDateTime::fromMSecsSinceEpoch(m_powerM1X);
ui->powerMarkerTable->item(POWER_MARKER_ROW_M1, POWER_MARKER_COL_DATE)->setData(Qt::DisplayRole, dt.date());
ui->powerMarkerTable->item(POWER_MARKER_ROW_M1, POWER_MARKER_COL_TIME)->setData(Qt::DisplayRole, dt.time());
ui->powerMarkerTable->item(POWER_MARKER_ROW_M1, POWER_MARKER_COL_VALUE)->setData(Qt::DisplayRole, m_powerM1Y);
calcPowerMarkerDelta();
}
else if (selection == "M2")
{
// Place marker 2
m_powerM2X = point.x();
m_powerM2Y = point.y();
if (m_powerM2Valid) {
m_powerMarkerSeries->replace(1, m_powerM2X, m_powerM2Y);
} else {
m_powerMarkerSeries->insert(1, QPointF(m_powerM2X, m_powerM2Y));
}
m_powerM2Valid = true;
QDateTime dt = QDateTime::fromMSecsSinceEpoch(m_powerM2X);
ui->powerMarkerTable->item(POWER_MARKER_ROW_M2, POWER_MARKER_COL_DATE)->setData(Qt::DisplayRole, dt.date());
ui->powerMarkerTable->item(POWER_MARKER_ROW_M2, POWER_MARKER_COL_TIME)->setData(Qt::DisplayRole, dt.time());
ui->powerMarkerTable->item(POWER_MARKER_ROW_M2, POWER_MARKER_COL_VALUE)->setData(Qt::DisplayRole, m_powerM2Y);
calcPowerMarkerDelta();
}
}
else if (selection == "Gaussian")
{
// Fit a Gaussian assuming point clicked is the peak
ui->powerGaussianCenter->setDateTime(QDateTime::fromMSecsSinceEpoch(point.x()));
// Calculate noise floor - take average of lowest 10%
qreal floor = calcSeriesFloor(m_powerSeries);
ui->powerGaussianFloor->setValue(floor);
// Set amplitude to achieve selected point
ui->powerGaussianAmp->setValue(point.y() - floor);
}
else
{
if (m_fftMeasurements.size() > 1)
{
// Select row closest to clicked data
QDateTime dt = QDateTime::fromMSecsSinceEpoch(point.x());
int i = 0;
while ((i < m_fftMeasurements.size()) && (dt > m_fftMeasurements[i]->m_dateTime)) {
i++;
}
if (i < m_fftMeasurements.size()) {
ui->spectrumIndex->setValue(i);
}
}
}
}
qreal RadioAstronomyGUI::calcSeriesFloor(QXYSeries *series, int percent)
{
QList<qreal> minValues;
double count = series->count() * percent / 100.0;
for (int i = 0; i < series->count(); i++)
{
qreal y = series->at(i).y();
if (minValues.size() < count)
{
minValues.append(y);
std::sort(minValues.begin(), minValues.end());
}
else if (y < minValues.last())
{
minValues.append(y);
std::sort(minValues.begin(), minValues.end());
}
}
qreal sum = std::accumulate(minValues.begin(), minValues.end(), 0.0);
return sum / minValues.size();
}
void RadioAstronomyGUI::updateSpectrumSelect()
{
ui->spectrumSelect->clear();
if (m_settings.m_spectrumMarkers)
{
ui->spectrumSelect->addItem("M1");
ui->spectrumSelect->addItem("M2");
}
if (m_settings.m_spectrumTemp)
{
ui->spectrumSelect->addItem("Gaussian");
}
bool visible = ui->spectrumSelect->count() != 0;
ui->spectrumSelectLabel->setVisible(visible);
ui->spectrumSelect->setVisible(visible);
}
void RadioAstronomyGUI::updatePowerSelect()
{
ui->powerSelect->clear();
if (m_settings.m_powerMarkers || m_settings.m_powerShowGaussian) {
ui->powerSelect->addItem("Row");
}
if (m_settings.m_powerMarkers)
{
ui->powerSelect->addItem("M1");
ui->powerSelect->addItem("M2");
}
if (m_settings.m_powerShowGaussian) {
ui->powerSelect->addItem("Gaussian");
}
bool visible = ui->powerSelect->count() != 0;
ui->powerSelectLabel->setVisible(visible);
ui->powerSelect->setVisible(visible);
}
void RadioAstronomyGUI::spectrumSeries_clicked(const QPointF &point)
{
QString selection = ui->spectrumSelect->currentText();
if (selection.startsWith("M"))
{
FFTMeasurement *fft = currentFFT();
if (selection == "M1")
{
m_spectrumM1X = point.x();
m_spectrumM1Y = point.y();
m_spectrumM1Valid = true;
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_M1, SPECTRUM_MARKER_COL_FREQ)->setData(Qt::DisplayRole, m_spectrumM1X);
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_M1, SPECTRUM_MARKER_COL_VALUE)->setData(Qt::DisplayRole, m_spectrumM1Y);
calcVrAndDistanceToPeak(m_spectrumM1X*1e6, fft, SPECTRUM_MARKER_ROW_M1);
}
else if (selection == "M2")
{
m_spectrumM2X = point.x();
m_spectrumM2Y = point.y();
m_spectrumM2Valid = true;
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_M2, SPECTRUM_MARKER_COL_FREQ)->setData(Qt::DisplayRole, m_spectrumM2X);
ui->spectrumMarkerTable->item(SPECTRUM_MARKER_ROW_M2, SPECTRUM_MARKER_COL_VALUE)->setData(Qt::DisplayRole, m_spectrumM2Y);
calcVrAndDistanceToPeak(m_spectrumM2X*1e6, fft, SPECTRUM_MARKER_ROW_M2);
}
calcSpectrumMarkerDelta();
m_fftMarkerSeries->clear();
if (m_spectrumM1Valid) {
m_fftMarkerSeries->append(m_spectrumM1X, m_spectrumM1Y);
}
if (m_spectrumM2Valid) {
m_fftMarkerSeries->append(m_spectrumM2X, m_spectrumM2Y);
}
}
else if (selection == "Gaussian")
{
ui->spectrumGaussianFreq->setValue(point.x());
// Calculate noise floor - take average of lowest 10%
qreal floor = calcSeriesFloor(m_fftSeries);
ui->spectrumGaussianFloor->setValue(floor);
// Set amplitude to achieve selected point
ui->spectrumGaussianAmp->setValue(point.y() - floor);
plotFFTMeasurement();
}
}
void RadioAstronomyGUI::on_spectrumGaussianFreq_valueChanged(double value)
{
(void) value;
calcFWHM();
plotFFTMeasurement();
}
void RadioAstronomyGUI::on_spectrumGaussianAmp_valueChanged(double value)
{
(void) value;
plotFFTMeasurement();
calcColumnDensity();
}
void RadioAstronomyGUI::on_spectrumGaussianFloor_valueChanged(double value)
{
(void) value;
plotFFTMeasurement();
}
void RadioAstronomyGUI::on_spectrumGaussianFWHM_valueChanged(double fFWHM)
{
double c = Astronomy::m_speedOfLight;
double k = Astronomy::m_boltzmann;
double m = Astronomy::m_hydrogenMass;
double f0 = ui->spectrumGaussianFreq->value() * 1e6;
double vTurb = ui->spectrumGaussianTurb->value() * 1e3; // RSM turbulent velocties - Convert to m/s
const double bf = 2.0*sqrt(M_LN2); // Convert from Doppler parameter to FWHM
double fr = fFWHM * c / (bf*f0);
double frs = fr*fr;
double T = m * (frs-vTurb*vTurb) / (2.0*k);
ui->spectrumTemperature->blockSignals(true);
ui->spectrumTemperature->setValue(T);
ui->spectrumTemperature->blockSignals(false);
plotFFTMeasurement();
calcColumnDensity();
}
void RadioAstronomyGUI::on_spectrumGaussianTurb_valueChanged(double value)
{
(void) value;
calcFWHM();
plotFFTMeasurement();
}
void RadioAstronomyGUI::on_spectrumTemperature_valueChanged(double value)
{
(void) value;
calcFWHM();
plotFFTMeasurement();
}
void RadioAstronomyGUI::calcFWHM()
{
double c = Astronomy::m_speedOfLight;
double k = Astronomy::m_boltzmann;
double m = Astronomy::m_hydrogenMass;
double f0 = ui->spectrumGaussianFreq->value() * 1e6;
double vTurb = ui->spectrumGaussianTurb->value() * 1e3; // RSM turbulent velocties - Convert to m/s
double T = ui->spectrumTemperature->value();
const double bf = 2.0*sqrt(M_LN2); // Convert from Doppler parameter to FWHM
double fFWHM = bf * f0/c * sqrt((2*k*T)/m + vTurb * vTurb);
ui->spectrumGaussianFWHM->blockSignals(true);
ui->spectrumGaussianFWHM->setValue(fFWHM);
ui->spectrumGaussianFWHM->blockSignals(false);
calcColumnDensity();
}
// Assumes optically thin
void RadioAstronomyGUI::calcColumnDensity()
{
double f0 = ui->spectrumLineFrequency->value() * 1e6;
double f = f0 + ui->spectrumGaussianFWHM->value() / 2.0;
double v = lineDopplerVelocity(f0, f) * 2.0;
double a = ui->spectrumGaussianAmp->value();
double integratedIntensity = v * a;
double columnDensity = 1.81e18 * integratedIntensity;
ui->columnDensity->setText(QString::number(columnDensity, 'g', 2));
}
void RadioAstronomyGUI::on_powerShowGaussian_clicked(bool checked)
{
m_settings.m_powerShowGaussian = checked;
applySettings();
ui->powerGaussianWidgets->setVisible(checked);
m_powerGaussianSeries->setVisible(checked);
updatePowerSelect();
getRollupContents()->arrangeRollups();
update();
}
void RadioAstronomyGUI::plotPowerGaussian()
{
m_powerGaussianSeries->clear();
double dt0 = ui->powerGaussianCenter->dateTime().toMSecsSinceEpoch();
double a = ui->powerGaussianAmp->value();
double floor = ui->powerGaussianFloor->value();
double fwhm = ui->powerGaussianFWHM->value() * 1000; // Convert from s to ms
double fwhm_sq = fwhm*fwhm;
qint64 dt = m_powerXAxis->min().toMSecsSinceEpoch();
qint64 end = m_powerXAxis->max().toMSecsSinceEpoch();
int steps = 256;
qint64 step = (end - dt) / steps;
for (int i = 0; i < steps; i++)
{
double fd = dt - dt0;
double g = a * std::exp(-4.0*M_LN2*fd*fd/fwhm_sq) + floor;
m_powerGaussianSeries->append(dt, g);
dt += step;
}
}
// Calculate antenna HPBW from Sun's FWHM time
void RadioAstronomyGUI::calcHPBWFromFWHM()
{
double fwhmSeconds = ui->powerGaussianFWHM->value();
double sunDegPerSecond = 360.0/(24.0*60.0*60.0);
double hpbwDeg = fwhmSeconds * sunDegPerSecond;
ui->powerGaussianHPBW->setValue(hpbwDeg);
}
// Calculate Sun's FWHM time for anntena HPBW
void RadioAstronomyGUI::calcFHWMFromHPBW()
{
double hpwmDeg = ui->powerGaussianHPBW->value();
double sunDegPerSecond = 360.0/(24.0*60.0*60.0);
double fwhmSeconds = hpwmDeg / sunDegPerSecond;
ui->powerGaussianFWHM->setValue(fwhmSeconds);
}
void RadioAstronomyGUI::on_powerGaussianCenter_dateTimeChanged(QDateTime dateTime)
{
(void) dateTime;
plotPowerGaussian();
}
void RadioAstronomyGUI::on_powerGaussianAmp_valueChanged(double value)
{
(void) value;
plotPowerGaussian();
}
void RadioAstronomyGUI::on_powerGaussianFloor_valueChanged(double value)
{
(void) value;
plotPowerGaussian();
}
void RadioAstronomyGUI::on_powerGaussianFWHM_valueChanged(double value)
{
(void) value;
plotPowerGaussian();
ui->powerGaussianHPBW->blockSignals(true);
calcHPBWFromFWHM();
ui->powerGaussianHPBW->blockSignals(false);
}
void RadioAstronomyGUI::on_powerGaussianHPBW_valueChanged(double value)
{
(void) value;
calcFHWMFromHPBW();
ui->powerGaussianFWHM->blockSignals(true);
plotPowerGaussian();
ui->powerGaussianFWHM->blockSignals(false);
}
void RadioAstronomyGUI::addToPowerFilter(qreal x, qreal y)
{
// Add data to circular buffer
m_window[m_windowIdx] = y;
m_windowIdx = (m_windowIdx + 1) % m_settings.m_powerFilterN;
if (m_windowCount < m_settings.m_powerFilterN) {
m_windowCount++;
}
// Filter
if (m_settings.m_powerFilter == RadioAstronomySettings::FILT_MOVING_AVERAGE)
{
// Moving average
qreal sum = 0.0;
for (int i = 0; i < m_windowCount; i++) {
sum += m_window[i];
}
qreal mean = sum / m_windowCount;
y = mean;
}
else
{
// Median
std::partial_sort_copy(m_window, m_window + m_windowCount, m_windowSorted, m_windowSorted + m_windowCount);
qreal median;
if ((m_windowCount & 1) == 1) {
median = m_windowSorted[m_windowCount / 2];
} else {
median = (m_windowSorted[m_windowCount / 2 - 1] + m_windowSorted[m_windowCount / 2]) / 2.0;
}
y = median;
}
// Add to series for chart
m_powerFilteredSeries->append(x, y);
}
void RadioAstronomyGUI::plotPowerFiltered()
{
delete[] m_window;
delete[] m_windowSorted;
m_window = new qreal[m_settings.m_powerFilterN];
m_windowSorted = new qreal[m_settings.m_powerFilterN];
m_windowIdx = 0;
m_windowCount = 0;
m_powerFilteredSeries->clear();
QVector<QPointF> powerSeries = m_powerSeries->pointsVector();
for (int i = 0; i < powerSeries.size(); i++)
{
QPointF point = powerSeries.at(i);
addToPowerFilter(point.x(), point.y());
}
}
void RadioAstronomyGUI::on_powerShowFiltered_clicked(bool checked)
{
m_settings.m_powerShowFiltered = checked;
applySettings();
ui->powerFilterWidgets->setVisible(checked);
m_powerFilteredSeries->setVisible(checked);
getRollupContents()->arrangeRollups();
update();
}
void RadioAstronomyGUI::on_powerFilter_currentIndexChanged(int index)
{
m_settings.m_powerFilter = (RadioAstronomySettings::PowerFilter)index;
applySettings();
plotPowerFiltered();
}
void RadioAstronomyGUI::on_powerFilterN_valueChanged(int value)
{
m_settings.m_powerFilterN = value;
applySettings();
plotPowerFiltered();
}
void RadioAstronomyGUI::on_powerShowMeasurement_clicked(bool checked)
{
m_settings.m_powerShowMeasurement = checked;
applySettings();
m_powerSeries->setVisible(checked);
}
RadioAstronomyGUI::LABData* RadioAstronomyGUI::parseLAB(QFile* file, float l, float b)
{
LABData *data = new LABData();
data->read(file, l, b);
m_dataLAB.append(data);
return data;
}
void RadioAstronomyGUI::plotLAB()
{
int index = ui->spectrumIndex->value();
if (index < m_fftMeasurements.size())
{
FFTMeasurement *fft = m_fftMeasurements[index];
plotLAB(fft->m_l, fft->m_b, m_beamWidth);
}
}
void RadioAstronomyGUI::plotLAB(float l, float b, float beamWidth)
{
// Assume a beamwidth >1deg
l = round(l);
b = round(b);
// Check if we already have the data in memory
LABData* data = nullptr;
for (int i = 0; i < m_dataLAB.size(); i++)
{
if ((m_dataLAB[i]->m_l == l) && (m_dataLAB[i]->m_b == b))
{
data = m_dataLAB[i];
break;
}
}
if (!data)
{
// Try to open previously downloaded data
QString filenameLAB = HttpDownloadManager::downloadDir() + "/" + QString("lab_l_%1_b_%2.txt").arg(l).arg(b);
QFile file(filenameLAB);
if (file.open(QIODevice::ReadOnly))
{
qDebug() << "RadioAstronomyGUI::plotLAB: Using cached file: " << filenameLAB;
data = parseLAB(&file, l, b);
}
else
{
// Only download one file at a time, so we don't overload the server
if (!m_downloadingLAB)
{
m_downloadingLAB = true;
m_lLAB = l;
m_bLAB = b;
m_filenameLAB = filenameLAB;
// Request data be generated via web server
QNetworkRequest request(QUrl("https://www.astro.uni-bonn.de/hisurvey/euhou/LABprofile/index.php"));
request.setRawHeader("Content-Type", "application/x-www-form-urlencoded");
QUrlQuery params;
params.addQueryItem("coordinates", "lb");
params.addQueryItem("ral", QString::number(l));
params.addQueryItem("decb", QString::number(b));
params.addQueryItem("beam", QString::number(beamWidth));
params.addQueryItem("vmin", "-100.0" );
params.addQueryItem("vmax", "100.0" );
params.addQueryItem("search", "Search data" );
m_networkManager->post(request, params.query(QUrl::FullyEncoded).toUtf8());
}
}
}
if (data)
{
data->toSeries(m_fftLABSeries);
spectrumAutoscale();
}
}
void RadioAstronomyGUI::networkManagerFinished(QNetworkReply *reply)
{
QNetworkReply::NetworkError replyError = reply->error();
if (replyError)
{
qWarning() << "RadioAstronomyGUI::networkManagerFinished:"
<< " error(" << (int) replyError
<< "): " << replyError
<< ": " << reply->errorString();
m_downloadingLAB = false;
}
else
{
QString answer = reply->readAll();
QRegExp re("a href=\\\"download.php([^\"]*)\"");
if (re.indexIn(answer) != -1)
{
QString filename = re.capturedTexts()[1];
qDebug() << "RadioAstronomyGUI: Downloading LAB reference data: " << filename;
m_dlm.download(QUrl("https://www.astro.uni-bonn.de/hisurvey/euhou/LABprofile/download.php" + filename), m_filenameLAB);
}
else
{
qDebug() << "RadioAstronomyGUI::networkManagerFinished - No filename found: " << answer;
m_downloadingLAB = false;
}
}
reply->deleteLater();
}
void RadioAstronomyGUI::downloadFinished(const QString& filename, bool success)
{
if (success)
{
QFile file(filename);
if (file.open(QIODevice::ReadOnly))
{
LABData *data = parseLAB(&file, m_lLAB, m_bLAB);
file.close();
// Check if the data we've downloaded is for the current FFT being displayed
int index = ui->spectrumIndex->value();
if (index < m_fftMeasurements.size())
{
FFTMeasurement *fft = m_fftMeasurements[index];
if (m_lLAB == fft->m_l && m_bLAB == fft->m_b)
{
data->toSeries(m_fftLABSeries);
spectrumAutoscale();
m_downloadingLAB = false;
}
else
{
// Try ploting for current FFT (as we only allow one download at a time, so may have been skipped)
m_downloadingLAB = false;
plotLAB(fft->m_l, fft->m_b, m_beamWidth);
// Don't clear m_downloadingLAB after this point
}
}
} else {
qDebug() << "RadioAstronomyGUI::downloadFinished: Failed to open downloaded file: " << filename;
m_downloadingLAB = false;
}
}
else
{
qDebug() << "RadioAstronomyGUI::downloadFinished: Failed to download: " << filename;
m_downloadingLAB = false;
}
}
void RadioAstronomyGUI::displayRunModeSettings()
{
bool sweep = m_settings.m_runMode == RadioAstronomySettings::SWEEP;
ui->sweep1CoordLabel->setVisible(sweep);
ui->sweepType->setVisible(sweep);
ui->sweep1StartLabel->setVisible(sweep);
ui->sweep1Start->setVisible(sweep);
ui->sweep1StopLabel->setVisible(sweep);
ui->sweep1Stop->setVisible(sweep);
ui->sweep1StepLabel->setVisible(sweep);
ui->sweep1Step->setVisible(sweep);
ui->sweep1DelayLabel->setVisible(sweep);
ui->sweep1Delay->setVisible(sweep);
ui->sweep2CoordLabel->setVisible(sweep);
ui->sweep2StartLabel->setVisible(sweep);
ui->sweep2Start->setVisible(sweep);
ui->sweep2StopLabel->setVisible(sweep);
ui->sweep2Stop->setVisible(sweep);
ui->sweep2StepLabel->setVisible(sweep);
ui->sweep2Step->setVisible(sweep);
ui->sweep2DelayLabel->setVisible(sweep);
ui->sweep2Delay->setVisible(sweep);
ui->sweepStatus->setVisible(sweep);
ui->runLayout->activate(); // Needed otherwise height of rollup doesn't seem to be reduced
ui->statusLayout->activate(); // going from sweep to single/continuous
getRollupContents()->arrangeRollups();
}
void RadioAstronomyGUI::on_runMode_currentIndexChanged(int index)
{
m_settings.m_runMode = (RadioAstronomySettings::RunMode)index;
applySettings();
displayRunModeSettings();
}
void RadioAstronomyGUI::on_sweepType_currentIndexChanged(int index)
{
m_settings.m_sweepType = (RadioAstronomySettings::SweepType)index;
if ((index == 0) || (index == 2))
{
ui->sweep1CoordLabel->setText("Az");
ui->sweep2CoordLabel->setText("El");
}
else if (index == 1)
{
ui->sweep1CoordLabel->setText("l");
ui->sweep2CoordLabel->setText("b");
}
}
void RadioAstronomyGUI::on_sweep1Start_valueChanged(double value)
{
m_settings.m_sweep1Start = value;
applySettings();
}
void RadioAstronomyGUI::on_sweep1Stop_valueChanged(double value)
{
m_settings.m_sweep1Stop = value;
applySettings();
}
void RadioAstronomyGUI::on_sweep1Step_valueChanged(double value)
{
m_settings.m_sweep1Step = value;
applySettings();
}
void RadioAstronomyGUI::on_sweep1Delay_valueChanged(double value)
{
m_settings.m_sweep1Delay = value;
applySettings();
}
void RadioAstronomyGUI::on_sweep2Start_valueChanged(double value)
{
m_settings.m_sweep2Start = value;
applySettings();
}
void RadioAstronomyGUI::on_sweep2Stop_valueChanged(double value)
{
m_settings.m_sweep2Stop = value;
applySettings();
}
void RadioAstronomyGUI::on_sweep2Step_valueChanged(double value)
{
m_settings.m_sweep2Step = value;
applySettings();
}
void RadioAstronomyGUI::on_sweep2Delay_valueChanged(double value)
{
m_settings.m_sweep2Delay = value;
applySettings();
}
void RadioAstronomyGUI::on_sweepStartAtTime_currentIndexChanged(int index)
{
m_settings.m_sweepStartAtTime = ui->sweepStartAtTime->currentIndex() == 1;
ui->sweepStartDateTime->setVisible(index == 1);
getRollupContents()->arrangeRollups();
applySettings();
}
void RadioAstronomyGUI::on_sweepStartDateTime_dateTimeChanged(const QDateTime& dateTime)
{
m_settings.m_sweepStartDateTime = dateTime;
applySettings();
}
void RadioAstronomyGUI::on_startStop_clicked(bool checked)
{
if (checked)
{
ui->startStop->setStyleSheet("QToolButton { background-color : green; }");
applySettings();
if (m_settings.m_power2DLinkSweep)
{
update2DSettingsFromSweep();
create2DImage();
}
m_radioAstronomy->getInputMessageQueue()->push(RadioAstronomy::MsgStartSweep::create());
}
else
{
m_radioAstronomy->getInputMessageQueue()->push(RadioAstronomy::MsgStopSweep::create());
if (m_settings.m_runMode != RadioAstronomySettings::SWEEP) {
ui->startStop->setStyleSheet("QToolButton { background-color : blue; }");
}
}
}
void RadioAstronomyGUI::calcPowerChartTickCount(int width)
{
// These values should probably be dependent on the font used
if (m_powerXAxis) {
if (m_powerXAxisSameDay) {
m_powerXAxis->setTickCount(width > 700 ? 10 : 5);
} else {
m_powerXAxis->setTickCount(width > 1200 ? 10 : 5);
}
}
}
void RadioAstronomyGUI::calcSpectrumChartTickCount(QValueAxis *axis, int width)
{
if (axis) {
axis->setTickCount(width > 700 ? 10 : 5);
}
}
void RadioAstronomyGUI::resizeEvent(QResizeEvent* size)
{
int width = size->size().width();
calcPowerChartTickCount(width);
calcSpectrumChartTickCount(m_fftXAxis, width);
calcSpectrumChartTickCount(m_fftDopplerAxis, width);
calcSpectrumChartTickCount(m_calXAxis, width);
ChannelGUI::resizeEvent(size);
}
void RadioAstronomyGUI::makeUIConnections()
{
QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &RadioAstronomyGUI::on_deltaFrequency_changed);
QObject::connect(ui->sampleRate, &ValueDialZ::changed, this, &RadioAstronomyGUI::on_sampleRate_changed);
QObject::connect(ui->rfBW, &ValueDialZ::changed, this, &RadioAstronomyGUI::on_rfBW_changed);
QObject::connect(ui->integration, &ValueDialZ::changed, this, &RadioAstronomyGUI::on_integration_changed);
QObject::connect(ui->fftSize, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_fftSize_currentIndexChanged);
QObject::connect(ui->fftWindow, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_fftWindow_currentIndexChanged);
QObject::connect(ui->filterFreqs, &QLineEdit::editingFinished, this, &RadioAstronomyGUI::on_filterFreqs_editingFinished);
QObject::connect(ui->starTracker, &QComboBox::currentTextChanged, this, &RadioAstronomyGUI::on_starTracker_currentTextChanged);
QObject::connect(ui->rotator, &QComboBox::currentTextChanged, this, &RadioAstronomyGUI::on_rotator_currentTextChanged);
QObject::connect(ui->showSensors, &QToolButton::clicked, this, &RadioAstronomyGUI::on_showSensors_clicked);
QObject::connect(ui->tempRXSelect, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_tempRXSelect_currentIndexChanged);
QObject::connect(ui->tempRX, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_tempRX_valueChanged);
QObject::connect(ui->tempCMB, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_tempCMB_valueChanged);
QObject::connect(ui->tempGal, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_tempGal_valueChanged);
QObject::connect(ui->tempSP, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_tempSP_valueChanged);
QObject::connect(ui->tempAtm, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_tempAtm_valueChanged);
QObject::connect(ui->tempAir, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_tempAir_valueChanged);
QObject::connect(ui->zenithOpacity, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_zenithOpacity_valueChanged);
QObject::connect(ui->elevation, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_elevation_valueChanged);
QObject::connect(ui->tempAtmLink, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_tempAtmLink_toggled);
QObject::connect(ui->tempAirLink, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_tempAirLink_toggled);
QObject::connect(ui->tempGalLink, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_tempGalLink_toggled);
QObject::connect(ui->elevationLink, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_elevationLink_toggled);
QObject::connect(ui->gainVariation, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_gainVariation_valueChanged);
QObject::connect(ui->omegaAUnits, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_omegaAUnits_currentIndexChanged);
QObject::connect(ui->sourceType, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_sourceType_currentIndexChanged);
QObject::connect(ui->omegaS, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_omegaS_valueChanged);
QObject::connect(ui->omegaSUnits, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_omegaSUnits_currentIndexChanged);
QObject::connect(ui->spectrumChartSelect, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_spectrumChartSelect_currentIndexChanged);
QObject::connect(ui->showCalSettings, &QToolButton::clicked, this, &RadioAstronomyGUI::on_showCalSettings_clicked);
QObject::connect(ui->startCalHot, &QToolButton::clicked, this, &RadioAstronomyGUI::on_startCalHot_clicked);
QObject::connect(ui->startCalCold, &QToolButton::clicked, this, &RadioAstronomyGUI::on_startCalCold_clicked);
QObject::connect(ui->recalibrate, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_recalibrate_toggled);
QObject::connect(ui->spectrumShowLegend, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_spectrumShowLegend_toggled);
QObject::connect(ui->spectrumShowRefLine, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_spectrumShowRefLine_toggled);
QObject::connect(ui->spectrumTemp, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_spectrumTemp_toggled);
QObject::connect(ui->spectrumMarker, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_spectrumMarker_toggled);
QObject::connect(ui->spectrumPeak, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_spectrumPeak_toggled);
QObject::connect(ui->spectrumReverseXAxis, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_spectrumReverseXAxis_toggled);
QObject::connect(ui->savePowerData, &QToolButton::clicked, this, &RadioAstronomyGUI::on_savePowerData_clicked);
QObject::connect(ui->savePowerChartImage, &QToolButton::clicked, this, &RadioAstronomyGUI::on_savePowerChartImage_clicked);
QObject::connect(ui->saveSpectrumData, &QToolButton::clicked, this, &RadioAstronomyGUI::on_saveSpectrumData_clicked);
QObject::connect(ui->loadSpectrumData, &QToolButton::clicked, this, &RadioAstronomyGUI::on_loadSpectrumData_clicked);
QObject::connect(ui->saveSpectrumChartImage, &QToolButton::clicked, this, &RadioAstronomyGUI::on_saveSpectrumChartImage_clicked);
QObject::connect(ui->saveSpectrumChartImages, &QToolButton::clicked, this, &RadioAstronomyGUI::on_saveSpectrumChartImages_clicked);
QObject::connect(ui->clearData, &QToolButton::clicked, this, &RadioAstronomyGUI::on_clearData_clicked);
QObject::connect(ui->clearCal, &QToolButton::clicked, this, &RadioAstronomyGUI::on_clearCal_clicked);
QObject::connect(ui->spectrumAutoscale, &QToolButton::toggled, this, &RadioAstronomyGUI::on_spectrumAutoscale_toggled);
QObject::connect(ui->spectrumAutoscaleX, &QToolButton::clicked, this, &RadioAstronomyGUI::on_spectrumAutoscaleX_clicked);
QObject::connect(ui->spectrumAutoscaleY, &QToolButton::clicked, this, &RadioAstronomyGUI::on_spectrumAutoscaleY_clicked);
QObject::connect(ui->spectrumReference, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_spectrumReference_valueChanged);
QObject::connect(ui->spectrumRange, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_spectrumRange_valueChanged);
QObject::connect(ui->spectrumCenterFreq, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_spectrumCenterFreq_valueChanged);
QObject::connect(ui->spectrumSpan, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_spectrumSpan_valueChanged);
QObject::connect(ui->spectrumYUnits, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_spectrumYUnits_currentIndexChanged);
QObject::connect(ui->spectrumBaseline, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_spectrumBaseline_currentIndexChanged);
QObject::connect(ui->spectrumIndex, &QSlider::valueChanged, this, &RadioAstronomyGUI::on_spectrumIndex_valueChanged);
QObject::connect(ui->spectrumLine, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_spectrumLine_currentIndexChanged);
QObject::connect(ui->spectrumLineFrequency, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_spectrumLineFrequency_valueChanged);
QObject::connect(ui->refFrame, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_refFrame_currentIndexChanged);
QObject::connect(ui->sunDistanceToGC, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_sunDistanceToGC_valueChanged);
QObject::connect(ui->sunOrbitalVelocity, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_sunOrbitalVelocity_valueChanged);
QObject::connect(ui->spectrumGaussianFreq, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_spectrumGaussianFreq_valueChanged);
QObject::connect(ui->spectrumGaussianAmp, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_spectrumGaussianAmp_valueChanged);
QObject::connect(ui->spectrumGaussianFloor, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_spectrumGaussianFloor_valueChanged);
QObject::connect(ui->spectrumGaussianFWHM, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_spectrumGaussianFWHM_valueChanged);
QObject::connect(ui->spectrumGaussianTurb, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_spectrumGaussianTurb_valueChanged);
QObject::connect(ui->spectrumTemperature, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_spectrumTemperature_valueChanged);
QObject::connect(ui->spectrumShowLAB, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_spectrumShowLAB_toggled);
QObject::connect(ui->spectrumShowDistance, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_spectrumShowDistance_toggled);
QObject::connect(ui->tCalHotSelect, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_tCalHotSelect_currentIndexChanged);
QObject::connect(ui->tCalHot, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_tCalHot_valueChanged);
QObject::connect(ui->tCalColdSelect, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_tCalColdSelect_currentIndexChanged);
QObject::connect(ui->tCalCold, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_tCalCold_valueChanged);
QObject::connect(ui->powerChartSelect, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_powerChartSelect_currentIndexChanged);
QObject::connect(ui->powerYUnits, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_powerYUnits_currentIndexChanged);
QObject::connect(ui->powerShowMarker, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_powerShowMarker_toggled);
QObject::connect(ui->powerShowAirTemp, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_powerShowAirTemp_toggled);
QObject::connect(ui->powerShowPeak, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_powerShowPeak_toggled);
QObject::connect(ui->powerShowAvg, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_powerShowAvg_toggled);
QObject::connect(ui->powerShowLegend, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_powerShowLegend_toggled);
QObject::connect(ui->powerAutoscale, &QToolButton::toggled, this, &RadioAstronomyGUI::on_powerAutoscale_toggled);
QObject::connect(ui->powerAutoscaleY, &QToolButton::clicked, this, &RadioAstronomyGUI::on_powerAutoscaleY_clicked);
QObject::connect(ui->powerAutoscaleX, &QToolButton::clicked, this, &RadioAstronomyGUI::on_powerAutoscaleX_clicked);
QObject::connect(ui->powerReference, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_powerReference_valueChanged);
QObject::connect(ui->powerRange, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_powerRange_valueChanged);
QObject::connect(ui->powerStartTime, &WrappingDateTimeEdit::dateTimeChanged, this, &RadioAstronomyGUI::on_powerStartTime_dateTimeChanged);
QObject::connect(ui->powerEndTime, &WrappingDateTimeEdit::dateTimeChanged, this, &RadioAstronomyGUI::on_powerEndTime_dateTimeChanged);
QObject::connect(ui->powerShowGaussian, &ButtonSwitch::clicked, this, &RadioAstronomyGUI::on_powerShowGaussian_clicked);
QObject::connect(ui->powerGaussianCenter, &WrappingDateTimeEdit::dateTimeChanged, this, &RadioAstronomyGUI::on_powerGaussianCenter_dateTimeChanged);
QObject::connect(ui->powerGaussianAmp, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_powerGaussianAmp_valueChanged);
QObject::connect(ui->powerGaussianFloor, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_powerGaussianFloor_valueChanged);
QObject::connect(ui->powerGaussianFWHM, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_powerGaussianFWHM_valueChanged);
QObject::connect(ui->powerGaussianHPBW, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_powerGaussianHPBW_valueChanged);
QObject::connect(ui->runMode, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_runMode_currentIndexChanged);
QObject::connect(ui->sweepType, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_sweepType_currentIndexChanged);
QObject::connect(ui->startStop, &ButtonSwitch::clicked, this, &RadioAstronomyGUI::on_startStop_clicked);
QObject::connect(ui->sweepStartAtTime, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_sweepStartAtTime_currentIndexChanged);
QObject::connect(ui->sweepStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &RadioAstronomyGUI::on_sweepStartDateTime_dateTimeChanged);
QObject::connect(ui->powerColourAutoscale, &QToolButton::toggled, this, &RadioAstronomyGUI::on_powerColourAutoscale_toggled);
QObject::connect(ui->powerColourScaleMin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_powerColourScaleMin_valueChanged);
QObject::connect(ui->powerColourScaleMax, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_powerColourScaleMax_valueChanged);
QObject::connect(ui->powerColourPalette, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_powerColourPalette_currentIndexChanged);
QObject::connect(ui->powerTable, &QTableWidget::cellDoubleClicked, this, &RadioAstronomyGUI::on_powerTable_cellDoubleClicked);
}
void RadioAstronomyGUI::updateAbsoluteCenterFrequency()
{
setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset);
}