///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2016 Edouard Griffiths, F4EXB                                   //
// Copyright (C) 2021-2024 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 <QRegularExpression>
#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/db.h"
#include "util/astronomy.h"
#include "util/interpolation.h"
#include "util/png.h"
#include "util/units.h"
#include "gui/basicchannelsettingsdialog.h"
#include "dsp/dspengine.h"
#include "gui/timedelegate.h"
#include "gui/decimaldelegate.h"
#include "gui/dialogpositioner.h"
#include "gui/crightclickenabler.h"
#include "channel/channelwebapiutils.h"
#include "maincore.h"
#include "feature/feature.h"
#include "webapi/webapiutils.h"

#include "radioastronomy.h"
#include "radioastronomysensordialog.h"
#include "radioastronomycalibrationdialog.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:
    TimeDeltaDelegate(QObject *parent = nullptr) :
        QStyledItemDelegate(parent) {}

    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:
    HMSDelegate(QObject *parent = nullptr) :
        QStyledItemDelegate(parent) {}

    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:
    DMSDelegate(QObject *parent = nullptr) :
        QStyledItemDelegate(parent) {}

    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->setItem(row, POWER_COL_UTC, new QTableWidgetItem("15/04/2016 10:17:00"));
    ui->powerTable->setItem(row, POWER_COL_ROT_AZ, new QTableWidgetItem("359.0"));
    ui->powerTable->setItem(row, POWER_COL_ROT_EL, new QTableWidgetItem("-90.0"));
    ui->powerTable->setItem(row, POWER_COL_ROT_AZ_OFF, new QTableWidgetItem("-10.0"));
    ui->powerTable->setItem(row, POWER_COL_ROT_EL_OFF, new QTableWidgetItem("-10.0"));
    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 = 0.0;
        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)
{
    ui->powerTable->setSortingEnabled(false);
    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();
    QTableWidgetItem* utcItem = new QTableWidgetItem();
    QTableWidgetItem* rotAzItem = new QTableWidgetItem();
    QTableWidgetItem* rotElItem = new QTableWidgetItem();
    QTableWidgetItem* rotAzOffItem = new QTableWidgetItem();
    QTableWidgetItem* rotElOffItem = 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);
    ui->powerTable->setItem(row, POWER_COL_UTC, utcItem);
    ui->powerTable->setItem(row, POWER_COL_ROT_AZ, rotAzItem);
    ui->powerTable->setItem(row, POWER_COL_ROT_EL, rotElItem);
    ui->powerTable->setItem(row, POWER_COL_ROT_AZ_OFF, rotAzOffItem);
    ui->powerTable->setItem(row, POWER_COL_ROT_EL_OFF, rotElOffItem);

    ui->powerTable->setSortingEnabled(true);

    QDateTime dateTime = fft->m_dateTime;
    dateItem->setData(Qt::DisplayRole, dateTime.date());
    timeItem->setData(Qt::DisplayRole, dateTime.time());
    utcItem->setData(Qt::DisplayRole, dateTime.toUTC());

    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]);
    if (fft->m_rotValid)
    {
        rotAzItem->setData(Qt::DisplayRole, fft->m_rotAz);
        rotElItem->setData(Qt::DisplayRole, fft->m_rotEl);
        rotAzOffItem->setData(Qt::DisplayRole, fft->m_rotAzOff);
        rotElOffItem->setData(Qt::DisplayRole, fft->m_rotElOff);
    }

    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(QStringList("powerAutoscale"));
}

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(QStringList("powerReference"));
}

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(QStringList("powerRange"));
}

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(QStringList(), true);
}

QByteArray RadioAstronomyGUI::serialize() const
{
    return m_settings.serialize();
}

bool RadioAstronomyGUI::deserialize(const QByteArray& data)
{
    if(m_settings.deserialize(data)) {
        displaySettings();
        applySettings(QStringList(), true);
        return true;
    } else {
        resetToDefaults();
        return false;
    }
}

void RadioAstronomyGUI::updateAvailableFeatures(const AvailableChannelOrFeatureList& availableFeatures, const QStringList& renameFrom, const QStringList& renameTo)
{
    // Update starTracker setting if it has been renamed
    if (renameFrom.contains(m_settings.m_starTracker))
    {
        m_settings.m_starTracker = renameTo[renameFrom.indexOf(m_settings.m_starTracker)];
        applySettings(QStringList("starTracker"));
    }

    ui->starTracker->blockSignals(true);
    ui->starTracker->clear();

    for (const auto& feature : availableFeatures) {
        ui->starTracker->addItem(feature.getLongId());
    }

    int idx = ui->starTracker->findText(m_settings.m_starTracker);
    if (idx >= 0) {
        ui->starTracker->setCurrentIndex(idx);
    } else  {
        ui->starTracker->setCurrentIndex(0);
    }

    ui->starTracker->blockSignals(false);

    QString newText = ui->starTracker->currentText();
    if (m_settings.m_starTracker != newText)
    {
       m_settings.m_starTracker = newText;
       applySettings(QStringList("starTracker"));
    }
}

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;
        updateAvailableFeatures(report.getFeatures(), report.getRenameFrom(), report.getRenameTo());
        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(), report.getRenameFrom(), report.getRenameTo());
        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(QStringList("inputFrequencyOffset"));
}

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, 100, sr);
}

void RadioAstronomyGUI::on_deltaFrequency_changed(qint64 value)
{
    m_channelMarker.setCenterFrequency(value);
    m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
    updateAbsoluteCenterFrequency();
    applySettings(QStringList("inputFrequencyOffset"));
}

void RadioAstronomyGUI::on_sampleRate_changed(qint64 value)
{
    float sr = value;
    m_settings.m_sampleRate = sr;
    updateBWLimits();
    updateIntegrationTime();
    applySettings(QStringList("sampleRate"));
}

void RadioAstronomyGUI::on_rfBW_changed(qint64 value)
{
    float bw = value;
    m_channelMarker.setBandwidth(bw);
    m_settings.m_rfBandwidth = bw;
    applySettings(QStringList("rfBandwidth"));
}

void RadioAstronomyGUI::on_integration_changed(qint64 value)
{
    m_settings.m_integration = value;
    updateIntegrationTime();
    applySettings(QStringList("integration"));
}

void RadioAstronomyGUI::on_recalibrate_toggled(bool checked)
{
    m_settings.m_recalibrate = checked;
    applySettings(QStringList("recalibrate"));
    if (checked) {
        recalibrate();
    }
}

void RadioAstronomyGUI::on_showCalSettings_clicked()
{
    RadioAstronomyCalibrationDialog dialog(&m_settings);
    if (dialog.exec() == QDialog::Accepted) {
        applySettings(QStringList({"gpioEnabled", "gpioPin", "gpioSense", "startCalCommand", "stopCalCommand", "calCommandDelay"}));
    }
}

// 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()
{
    auto clearCalibration = [this]() {
        ui->calYFactor->setText("");
        ui->calTrx->setText("");
    };

    if (!m_calHot || !m_calCold || (m_calHot->m_fftSize != m_calCold->m_fftSize))
    {
        clearCalibration();
        return;
    }

    // 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];
    }

    if (sumC == 0.0)
    {
        clearCalibration();
        return;
    }

    double y = sumH/sumC;

    // The Y-factor method is undefined when hot and cold measurements have the same power
    // (y == 1). Use qFuzzyCompare to account for floating-point rounding near unity.
    if (qFuzzyCompare(y, 1.0))
    {
        clearCalibration();
        return;
    }

    // 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));
}

// Estimate spillover temperature (This is typically very Az/El dependent 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();
}

void RadioAstronomyGUI::savePowerData(const QString& filename)
{
    QFile file(filename);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QMessageBox::critical(this, "Radio Astronomy", QString("Failed to open file %1").arg(filename));
        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";
    }
}

// Save power data in table to a CSV file
void RadioAstronomyGUI::on_savePowerData_clicked(bool checked)
{
    (void) checked;

    ui->savePowerData->setChecked(!m_settings.m_powerAutoSaveCSVFilename.isEmpty());

    // Get filename to save to
    QFileDialog fileDialog(nullptr, "Select file to save data to", "", "*.csv");
    fileDialog.setDefaultSuffix("csv");
    fileDialog.setAcceptMode(QFileDialog::AcceptSave);
    if (fileDialog.exec())
    {
        QStringList fileNames = fileDialog.selectedFiles();
        if (fileNames.size() > 0) {
            savePowerData(fileNames[0]);
        }
    }
}

void RadioAstronomyGUI::on_savePowerData_rightClicked(const QPoint& point)
{
    (void) point;

    if (!ui->savePowerData->isChecked())
    {
        // Get filename to save to
        QFileDialog fileDialog(nullptr, "Select file to auto save data to", "", "*.csv");
        fileDialog.setDefaultSuffix("csv");
        fileDialog.setAcceptMode(QFileDialog::AcceptSave);
        if (fileDialog.exec())
        {
            QStringList fileNames = fileDialog.selectedFiles();
            if (fileNames.size() > 0)
            {
                m_settings.m_powerAutoSaveCSVFilename = fileNames[0];
                ui->savePowerData->setChecked(true);
                ui->savePowerData->setToolTip(QString("Left click to save data to a .csv file.\nRight click to disable auto save.\nAuto saving to %1").arg(m_settings.m_powerAutoSaveCSVFilename));
                applySettings(QStringList("powerAutoSaveCSVFilename"));
                savePowerData(m_settings.m_powerAutoSaveCSVFilename);
            }
        }
    }
    else
    {
        ui->savePowerData->setChecked(false);
        ui->savePowerData->setToolTip("Left click to save data to a .csv file.\nRight click to auto-save data to a .csv file");
        m_settings.m_powerAutoSaveCSVFilename = "";
        applySettings(QStringList("powerAutoSaveCSVFilename"));
    }
}

// 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(Qt::ISODateWithMs);
    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::saveSpectrumData(const QString& filename)
{
    QFile file(filename);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QMessageBox::critical(this, "Radio Astronomy", QString("Failed to open file %1").arg(filename));
        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_saveSpectrumData_clicked(bool checked)
{
    (void) checked;

    ui->saveSpectrumData->setChecked(!m_settings.m_spectrumAutoSaveCSVFilename.isEmpty());

    // Get filename to save to
    QFileDialog fileDialog(nullptr, "Select file to save data to", "", "*.csv");
    fileDialog.setDefaultSuffix("csv");
    fileDialog.setAcceptMode(QFileDialog::AcceptSave);
    if (fileDialog.exec())
    {
        QStringList fileNames = fileDialog.selectedFiles();
        if (fileNames.size() > 0) {
            saveSpectrumData(fileNames[0]);
        }
    }
}

void RadioAstronomyGUI::on_saveSpectrumData_rightClicked(const QPoint &point)
{
    (void) point;

    if (!ui->saveSpectrumData->isChecked())
    {
        // Get filename to save to
        QFileDialog fileDialog(nullptr, "Select file to auto save data to", "", "*.csv");
        fileDialog.setDefaultSuffix("csv");
        fileDialog.setAcceptMode(QFileDialog::AcceptSave);
        if (fileDialog.exec())
        {
            QStringList fileNames = fileDialog.selectedFiles();
            if (fileNames.size() > 0)
            {
                m_settings.m_spectrumAutoSaveCSVFilename = fileNames[0];
                ui->saveSpectrumData->setChecked(true);
                ui->saveSpectrumData->setToolTip(QString("Left click to save data to a .csv file.\nRight click to disable auto save.\nAuto saving to %1").arg(m_settings.m_spectrumAutoSaveCSVFilename));
                applySettings(QStringList("spectrumAutoSaveCSVFilename"));
                saveSpectrumData(m_settings.m_spectrumAutoSaveCSVFilename);
            }
        }
    }
    else
    {
        ui->saveSpectrumData->setChecked(false);
        ui->saveSpectrumData->setToolTip("Left click to save data to a .csv file.\nRight click to auto-save data to a .csv file");
        m_settings.m_spectrumAutoSaveCSVFilename = "";
        applySettings(QStringList("spectrumAutoSaveCSVFilename"));
    }
}

void RadioAstronomyGUI::on_loadSpectrumData_clicked()
{
    // Get filename to load from
    QFileDialog fileDialog(nullptr, "Select file to load data from", "", "*.csv");
    fileDialog.setDefaultSuffix("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(m_radioAstronomy, "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;

    getRollupContents()->saveState(m_rollupState);
    applySettings(QStringList());
}

void RadioAstronomyGUI::onMenuDialogCalled(const QPoint &p)
{
    if (m_contextMenuType == ContextMenuType::ContextMenuChannelSettings)
    {
        BasicChannelSettingsDialog dialog(&m_channelMarker, this);
        dialog.setUseReverseAPI(m_settings.m_useReverseAPI);
        dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress);
        dialog.setReverseAPIPort(m_settings.m_reverseAPIPort);
        dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex);
        dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex);
        dialog.setDefaultTitle(m_displayedName);

        if (m_deviceUISet->m_deviceMIMOEngine)
        {
            dialog.setNumberOfStreams(m_radioAstronomy->getNumberOfDeviceStreams());
            dialog.setStreamIndex(m_settings.m_streamIndex);
        }

        dialog.move(p);
        new DialogPositioner(&dialog, false);
        if (dialog.exec() == QDialog::Accepted)
        {
            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(QStringList({"title", "rgbColor", "useReverseAPI", "reverseAPIAddress",
                                        "reverseAPIPort", "reverseAPIDeviceIndex", "reverseAPIChannelIndex",
                                        "streamIndex"}));
        }
    }

    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_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

    CRightClickEnabler *saveSpectrumDataClickEnabler = new CRightClickEnabler(ui->saveSpectrumData);
    connect(saveSpectrumDataClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(on_saveSpectrumData_rightClicked(const QPoint &)));

    CRightClickEnabler *savePowerDataClickEnabler = new CRightClickEnabler(ui->savePowerData);
    connect(savePowerDataClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(on_savePowerData_rightClicked(const QPoint &)));

    connect(&m_autoSaveTimer, &QTimer::timeout, this, &RadioAstronomyGUI::autosave);
    m_autoSaveTimer.setInterval(5 * 60 * 1000);
    m_autoSaveTimer.setSingleShot(false);
    m_autoSaveTimer.start();

    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, 100, 99999999);
    ui->sampleRate->setColorMapper(ColorMapper(ColorMapper::GrayGold));
    ui->sampleRate->setValue(m_settings.m_sampleRate);
    ui->sampleRate->setValueRange(true, 8, 1000, 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("hh:mm:ss", ui->powerTable));
    //ui->powerTable->setItemDelegateForColumn(POWER_COL_POWER, new DecimalDelegate(6, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_POWER_DB, new DecimalDelegate(1, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_POWER_DBM, new DecimalDelegate(1, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_TSYS, new DecimalDelegate(0, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_TSYS0, new DecimalDelegate(0, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_TSOURCE, new DecimalDelegate(0, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_TB, new DecimalDelegate(0, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_TSKY, new DecimalDelegate(0, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_FLUX, new DecimalDelegate(2, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_SIGMA_T, new DecimalDelegate(2, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_SIGMA_S, new DecimalDelegate(1, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_RA, new HMSDelegate(ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_DEC, new DMSDelegate(ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_GAL_LAT, new DecimalDelegate(0, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_GAL_LON, new DecimalDelegate(0, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_AZ, new DecimalDelegate(0, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_EL, new DecimalDelegate(0, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_VBCRS, new DecimalDelegate(1, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_VLSR, new DecimalDelegate(1, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_AIR_TEMP, new DecimalDelegate(1, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_ROT_AZ, new DecimalDelegate(0, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_ROT_EL, new DecimalDelegate(0, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_ROT_AZ_OFF, new DecimalDelegate(0, ui->powerTable));
    ui->powerTable->setItemDelegateForColumn(POWER_COL_ROT_EL_OFF, new DecimalDelegate(0, ui->powerTable));

    resizeSpectrumMarkerTable();
    ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_FREQ, new DecimalDelegate(6, ui->spectrumMarkerTable));
    ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_VALUE, new DecimalDelegate(1, ui->spectrumMarkerTable));
    ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_DELTA_X, new DecimalDelegate(6, ui->spectrumMarkerTable));
    ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_DELTA_Y, new DecimalDelegate(1, ui->spectrumMarkerTable));
    ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_VR, new DecimalDelegate(2, ui->spectrumMarkerTable));
    ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_R, new DecimalDelegate(1, ui->spectrumMarkerTable));
    ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_D, new DecimalDelegate(1, ui->spectrumMarkerTable));
    ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_R_MIN, new DecimalDelegate(1, ui->spectrumMarkerTable));
    ui->spectrumMarkerTable->setItemDelegateForColumn(SPECTRUM_MARKER_COL_V, new DecimalDelegate(1, ui->spectrumMarkerTable));

    // 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("hh:mm:ss", ui->powerMarkerTable));
    ui->powerMarkerTable->setItemDelegateForColumn(POWER_MARKER_COL_VALUE, new DecimalDelegate(1, ui->powerMarkerTable));
    ui->powerMarkerTable->setItemDelegateForColumn(POWER_MARKER_COL_DELTA_X, new TimeDeltaDelegate(ui->powerMarkerTable));
    ui->powerMarkerTable->setItemDelegateForColumn(POWER_MARKER_COL_DELTA_Y, new DecimalDelegate(1, ui->powerMarkerTable));

    // 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(QStringList(), true); // Force apply all settings
    m_resizer.enableChildMouseTracking();

    create2DImage();

    plotCalSpectrum();
    plotSpectrum();
    plotPowerChart();

    m_radioAstronomy->getInputMessageQueue()->push(RadioAstronomy::MsgScanAvailableFeatures::create());
}

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()
{
    m_autoSaveTimer.stop();
    delete m_networkManager;
    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(const QStringList& settingsKeys, bool force)
{
    if (m_doApplySettings)
    {
        RadioAstronomy::MsgConfigureRadioAstronomy* message = RadioAstronomy::MsgConfigureRadioAstronomy::create(settingsKeys, 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->savePowerData->setChecked(!m_settings.m_powerAutoSaveCSVFilename.isEmpty());
    ui->saveSpectrumData->setChecked(!m_settings.m_spectrumAutoSaveCSVFilename.isEmpty());

    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(EnterEventType* 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 AvailableChannelOrFeatureList& rotators, const QStringList& renameFrom, const QStringList& renameTo)
{
    // Update rotator setting if it has been renamed
    if (renameFrom.contains(m_settings.m_rotator))
    {
        m_settings.m_rotator = renameTo[renameFrom.indexOf(m_settings.m_rotator)];
        applySettings(QStringList("rotator"));
    }

    // Update list of rotators
    ui->rotator->blockSignals(true);
    ui->rotator->clear();
    ui->rotator->addItem("None");

    for (const auto& rotator : rotators) {
        ui->rotator->addItem(rotator.getLongId());
    }

    // 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);
        setColumnPrecisionFromRotator();
    }
    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(QStringList("fftSize"));
    updateIntegrationTime();
}

void RadioAstronomyGUI::on_fftWindow_currentIndexChanged(int index)
{
    m_settings.m_fftWindow = (RadioAstronomySettings::FFTWindow)index;
    applySettings(QStringList("fftWindow"));
}

void RadioAstronomyGUI::on_filterFreqs_editingFinished()
{
    m_settings.m_filterFreqs = ui->filterFreqs->text();
    applySettings(QStringList("filterFreqs"));
}

void RadioAstronomyGUI::on_gainVariation_valueChanged(double value)
{
    m_settings.m_gainVariation = value;
    applySettings(QStringList("gainVariation"));
    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(QStringList("sourceType"));
    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(QStringList("omegaS"));
}

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(QStringList("omegaAUnits"));
}

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(QStringList("omegaSUnits"));
}

void RadioAstronomyGUI::on_starTracker_currentTextChanged(const QString& text)
{
    m_settings.m_starTracker = text;
    applySettings(QStringList("starTracker"));
}

void RadioAstronomyGUI::on_rotator_currentTextChanged(const QString& text)
{
    m_settings.m_rotator = text;
    applySettings(QStringList("rotator"));
    setColumnPrecisionFromRotator();
}

void RadioAstronomyGUI::setColumnPrecisionFromRotator()
{
    // Match rotator precision
    unsigned int featureSetIndex, featureIndex;

    if (MainCore::getFeatureIndexFromId(m_settings.m_rotator, featureSetIndex, featureIndex))
    {
        int precision = 0;
        if (ChannelWebAPIUtils::getFeatureSetting(featureSetIndex, featureIndex, "precision", precision))
        {
            int old = ((DecimalDelegate *)ui->powerTable->itemDelegateForColumn(POWER_COL_GAL_LAT))->getPrecision();
            ((DecimalDelegate *)ui->powerTable->itemDelegateForColumn(POWER_COL_GAL_LAT))->setPrecision(precision);
            ((DecimalDelegate *)ui->powerTable->itemDelegateForColumn(POWER_COL_GAL_LON))->setPrecision(precision);
            ((DecimalDelegate *)ui->powerTable->itemDelegateForColumn(POWER_COL_AZ))->setPrecision(precision);
            ((DecimalDelegate *)ui->powerTable->itemDelegateForColumn(POWER_COL_EL))->setPrecision(precision);
            ((DecimalDelegate *)ui->powerTable->itemDelegateForColumn(POWER_COL_ROT_AZ))->setPrecision(precision);
            ((DecimalDelegate *)ui->powerTable->itemDelegateForColumn(POWER_COL_ROT_EL))->setPrecision(precision);
            ((DecimalDelegate *)ui->powerTable->itemDelegateForColumn(POWER_COL_ROT_AZ_OFF))->setPrecision(precision);
            ((DecimalDelegate *)ui->powerTable->itemDelegateForColumn(POWER_COL_ROT_EL_OFF))->setPrecision(precision);
            if (precision > old)
            {
                ui->powerTable->resizeColumnToContents(POWER_COL_GAL_LAT);
                ui->powerTable->resizeColumnToContents(POWER_COL_GAL_LON);
                ui->powerTable->resizeColumnToContents(POWER_COL_AZ);
                ui->powerTable->resizeColumnToContents(POWER_COL_EL);
                ui->powerTable->resizeColumnToContents(POWER_COL_ROT_AZ);
                ui->powerTable->resizeColumnToContents(POWER_COL_ROT_EL);
                ui->powerTable->resizeColumnToContents(POWER_COL_ROT_AZ_OFF);
                ui->powerTable->resizeColumnToContents(POWER_COL_ROT_EL_OFF);
            }
            ui->powerTable->viewport()->update();
        }
    }
}

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(QStringList("sensorName"));
    }
}

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(QStringList("powerYData"));
}

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(QStringList("powerYUnits"));
    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();
}

// Calculate 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;

        if (qFuzzyIsNull(m_settings.m_sweep1Step) || qFuzzyIsNull(m_settings.m_sweep2Step))
        {
            ui->power2DWidth->setValue(0);
            ui->power2DHeight->setValue(0);
            return;
        }

        // 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:
                qWarning() << "Unexpected power unit:" << m_settings.m_powerYUnits
                     << "Falling back to totalPowerdBFS";
                intensity = fft->m_totalPowerdBFS;
                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(QStringList("power2DLinkSweep"));
}

void RadioAstronomyGUI::on_power2DSweepType_currentIndexChanged(int index)
{
    m_settings.m_power2DSweepType = (RadioAstronomySettings::SweepType)index;
    applySettings(QStringList("power2DSweepType"));
    plot2DChart();
}

void RadioAstronomyGUI::on_power2DWidth_valueChanged(int value)
{
    m_settings.m_power2DWidth = value;
    applySettings(QStringList("power2DWidth"));
    create2DImage();
    plot2DChart();
}

void RadioAstronomyGUI::on_power2DHeight_valueChanged(int value)
{
    m_settings.m_power2DHeight = value;
    applySettings(QStringList("power2DHeight"));
    create2DImage();
    plot2DChart();
}

void RadioAstronomyGUI::on_power2DXMin_valueChanged(double value)
{
    m_settings.m_power2DXMin = value;
    applySettings(QStringList("power2DXMin"));
    if (m_2DXAxis)
    {
        m_2DXAxis->setMin(m_settings.m_power2DXMin);
        plot2DChart();
    }
}

void RadioAstronomyGUI::on_power2DXMax_valueChanged(double value)
{
    m_settings.m_power2DXMax = value;
    applySettings(QStringList("power2DXMax"));
    if (m_2DXAxis)
    {
        m_2DXAxis->setMax(m_settings.m_power2DXMax);
        plot2DChart();
    }
}

void RadioAstronomyGUI::on_power2DYMin_valueChanged(double value)
{
    m_settings.m_power2DYMin = value;
    applySettings(QStringList("power2DYMin"));
    if (m_2DYAxis)
    {
        m_2DYAxis->setMin(m_settings.m_power2DYMin);
        plot2DChart();
    }
}

void RadioAstronomyGUI::on_power2DYMax_valueChanged(double value)
{
    m_settings.m_power2DYMax = value;
    applySettings(QStringList("power2DYMax"));
    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(QStringList("powerColourAutoscale"));
    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(QStringList("powerColourScaleMin"));
    recolour2DImage();
}

void RadioAstronomyGUI::on_powerColourScaleMax_valueChanged(double value)
{
    m_settings.m_powerColourScaleMax = value;
    updatePowerColourScaleStep();
    applySettings(QStringList("powerColourScaleMax"));
    recolour2DImage();
}

void RadioAstronomyGUI::on_powerColourPalette_currentIndexChanged(int index)
{
    (void) index;
    m_settings.m_powerColourPalette = ui->powerColourPalette->currentText();
    applySettings(QStringList("powerColourPalette"));
    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;
    getRotatorData(fft);


    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(QStringList("spectrumReference"));
    }
}

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(QStringList("spectrumRange"));
    }
}

void RadioAstronomyGUI::on_spectrumSpan_valueChanged(double value)
{
    m_settings.m_spectrumSpan = value;
    spectrumUpdateXRange();
    applySettings(QStringList("spectrumSpan"));
}

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(QStringList("spectrumCenterFreqOffset"));
}

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(QStringList("spectrumAutoscale"));
}

// 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(QStringList("spectrumYScale"));
}

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(QStringList("spectrumBaseline"));
}

// 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 receding - 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 receding
    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(m_radioAstronomy, "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 receding)").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;

        // Estimate 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::getRotatorData(FFTMeasurement *fft)
{
    unsigned int rotatorFeatureSetIndex, rotatorFeatureIndex;

    if (MainCore::getFeatureIndexFromId(m_settings.m_rotator, rotatorFeatureSetIndex, rotatorFeatureIndex))
    {
        SWGSDRangel::SWGFeatureReport featureReport;
        double value;
        qDebug() << m_settings.m_rotator << rotatorFeatureSetIndex << rotatorFeatureIndex;

        if (ChannelWebAPIUtils::getFeatureReport(rotatorFeatureSetIndex, rotatorFeatureIndex, featureReport))
        {
            QJsonObject *jsonObj = featureReport.asJsonObject();
            qDebug() << *jsonObj;
            if (WebAPIUtils::getSubObjectDouble(*jsonObj, "currentAzimuth", value)) {
                fft->m_rotAz = value;
            } else {
                qDebug() << "RadioAstronomyGUI::getRotatorData: getSubObjectDouble currentAzimuth failed";
            }
            if (WebAPIUtils::getSubObjectDouble(*jsonObj, "currentElevation", value)) {
                fft->m_rotEl = value;
            } else {
                qDebug() << "RadioAstronomyGUI::getRotatorData: getSubObjectDouble currentElevation failed";
            }
        }
        else
        {
            qDebug() << "RadioAstronomyGUI::getRotatorData: getFeatureReport failed";
        }

        SWGSDRangel::SWGFeatureSettings featureSettingsResponse;
        Feature *feature;
        if (ChannelWebAPIUtils::getFeatureSettings(rotatorFeatureSetIndex, rotatorFeatureIndex, featureSettingsResponse, feature))
        {
            QJsonObject *jsonObj = featureSettingsResponse.asJsonObject();
            qDebug() << *jsonObj;
            if (WebAPIUtils::getSubObjectDouble(*jsonObj, "azimuthOffset", value)) {
                fft->m_rotAzOff = value;
            } else {
                qDebug() << "RadioAstronomyGUI::getRotatorData: getSubObjectDouble azimuthOffset failed";
            }
            if (WebAPIUtils::getSubObjectDouble(*jsonObj, "elevationOffset", value)) {
                fft->m_rotElOff = value;
            } else {
                qDebug() << "RadioAstronomyGUI::getRotatorData: getSubObjectDouble elevationOffset ";
            }
        }
        else
        {
            qDebug() << "RadioAstronomyGUI::getRotatorData: getFeatureSettings failed";
        }

        fft->m_rotValid = true;
    }
    else
        qDebug() << "Couldn't parse rotator feature " << m_settings.m_rotator;
}

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;
    getRotatorData(fft);

    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(m_radioAstronomy, "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(QStringList("tempRX"));
}

void RadioAstronomyGUI::on_tempCMB_valueChanged(double value)
{
    m_settings.m_tempCMB = value;
    updateTSys0();
    applySettings(QStringList("tempCMB"));
}

void RadioAstronomyGUI::on_tempGal_valueChanged(double value)
{
    m_settings.m_tempGal = value;
    updateTSys0();
    applySettings(QStringList("tempGal"));
}

void RadioAstronomyGUI::on_tempSP_valueChanged(double value)
{
    m_settings.m_tempSP = value;
    updateTSys0();
    applySettings(QStringList("tempSP"));
}

void RadioAstronomyGUI::on_tempAtm_valueChanged(double value)
{
    m_settings.m_tempAtm = value;
    updateTSys0();
    applySettings(QStringList("tempAtm"));
}

void RadioAstronomyGUI::on_tempAir_valueChanged(double value)
{
    m_settings.m_tempAir = value;
    if (m_settings.m_tempAtmLink) {
        calcAtmosphericTemp();
    }
    applySettings(QStringList("tempAir"));
}

void RadioAstronomyGUI::on_zenithOpacity_valueChanged(double value)
{
    m_settings.m_zenithOpacity = value;
    if (m_settings.m_tempAtmLink) {
        calcAtmosphericTemp();
    }
    applySettings(QStringList("zenithOpacity"));
}

void RadioAstronomyGUI::on_elevation_valueChanged(double value)
{
    m_settings.m_elevation = value;
    if (m_settings.m_tempAtmLink) {
        calcAtmosphericTemp();
    }
    applySettings(QStringList("elevation"));
}

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(QStringList("elevationLink"));
}

void RadioAstronomyGUI::on_tempAtmLink_toggled(bool checked)
{
    m_settings.m_tempAtmLink = checked;
    ui->tempAtm->setEnabled(!m_settings.m_tempAtmLink);
    if (checked) {
        calcAtmosphericTemp();
    }
    applySettings(QStringList("tempAtmLink"));
}

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(QStringList("tempAirLink"));
}

void RadioAstronomyGUI::on_tempGalLink_toggled(bool checked)
{
    m_settings.m_tempGalLink = checked;
    if (checked) {
        calcGalacticBackgroundTemp();
    }
    ui->tempGal->setEnabled(!m_settings.m_tempGalLink);
    applySettings(QStringList("tempGalLink"));
}

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(QStringList("tCalHot"));
}

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(QStringList("tCalCold"));
}

void RadioAstronomyGUI::on_spectrumLine_currentIndexChanged(int value)
{
    m_settings.m_line = (RadioAstronomySettings::Line)value;
    displaySpectrumLineFrequency();
    plotFFTMeasurement();
    applySettings(QStringList("line"));
}

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(QStringList("lineCustomFrequency"));
}

void RadioAstronomyGUI::on_refFrame_currentIndexChanged(int value)
{
    m_settings.m_refFrame = (RadioAstronomySettings::RefFrame)value;
    plotFFTMeasurement();
    applySettings(QStringList("refFrame"));
}

void RadioAstronomyGUI::on_sunDistanceToGC_valueChanged(double value)
{
    m_settings.m_sunDistanceToGC = value;
    applySettings(QStringList("sunDistanceToGC"));
    calcDistances();
}

void RadioAstronomyGUI::on_sunOrbitalVelocity_valueChanged(double value)
{
    m_settings.m_sunOrbitalVelocity = value;
    applySettings(QStringList("sunOrbitalVelocity"));
    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(QStringList("spectrumReverseXAxis"));
    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(QStringList("powerPeaks"));
    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(QStringList("spectrumPeaks"));
    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(QStringList("powerMarkers"));
    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(QStringList("powerAvg"));
    ui->powerChartAvgWidgets->setVisible(checked);
    getRollupContents()->arrangeRollups();
    if (checked) {
        calcAverages();
    }
}

void RadioAstronomyGUI::on_powerShowLegend_toggled(bool checked)
{
    m_settings.m_powerLegend = checked;
    applySettings(QStringList("powerLegend"));
    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(QStringList("powerShowTsys0"));
    if (m_powerTsys0Series) {
        m_powerTsys0Series->setVisible(checked);
    }
}

void RadioAstronomyGUI::on_powerShowAirTemp_toggled(bool checked)
{
    m_settings.m_powerShowAirTemp = checked;
    applySettings(QStringList("powerShowAirTemp"));
    m_airTemps.clicked(checked);
}

void RadioAstronomyGUI::on_powerShowSensor1_toggled(bool checked)
{
    m_settings.m_sensorVisible[0] = checked;
    applySettings(QStringList("sensorVisible"));
    m_sensors[0].clicked(checked);
}

void RadioAstronomyGUI::on_powerShowSensor2_toggled(bool checked)
{
    m_settings.m_sensorVisible[1] = checked;
    applySettings(QStringList("sensorVisible"));
    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(QStringList("spectrumMarkers"));
    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(QStringList("spectrumTemp"));
    ui->spectrumGaussianWidgets->setVisible(checked);
    m_fftGaussianSeries->setVisible(checked);
    updateSpectrumSelect();
    getRollupContents()->arrangeRollups();
}

void RadioAstronomyGUI::on_spectrumShowLegend_toggled(bool checked)
{
    m_settings.m_spectrumLegend = checked;
    applySettings(QStringList("spectrumLegend"));
    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(QStringList("spectrumRefLine"));
    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(QStringList("spectrumLAB"));
    m_fftLABSeries->setVisible(m_settings.m_spectrumLAB);
    if (m_settings.m_spectrumLAB) {
        plotLAB(); // Replot in case 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(QStringList("spectrumDistance"));
    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);
            if (fft) {
                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);
            if (fft) {
                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(QStringList("powerShowGaussian"));
    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(QStringList("powerShowFiltered"));
    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(QStringList("powerFilter"));
    plotPowerFiltered();
}

void RadioAstronomyGUI::on_powerFilterN_valueChanged(int value)
{
    m_settings.m_powerFilterN = value;
    applySettings(QStringList("powerFilterN"));
    plotPowerFiltered();
}

void RadioAstronomyGUI::on_powerShowMeasurement_clicked(bool checked)
{
    m_settings.m_powerShowMeasurement = checked;
    applySettings(QStringList("powerShowMeasurement"));
    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();
        QRegularExpression re("a href=\\\"download.php([^\"]*)\"");
        QRegularExpressionMatch match = re.match(answer);
        if (match.hasMatch())
        {
            QString filename = match.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 plotting 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(QStringList("runMode"));
    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(QStringList("sweep1Start"));
}

void RadioAstronomyGUI::on_sweep1Stop_valueChanged(double value)
{
    m_settings.m_sweep1Stop = value;
    applySettings(QStringList("sweep1Stop"));
}

void RadioAstronomyGUI::on_sweep1Step_valueChanged(double value)
{
    m_settings.m_sweep1Step = value;
    applySettings(QStringList("sweep1Step"));
}

void RadioAstronomyGUI::on_sweep1Delay_valueChanged(double value)
{
    m_settings.m_sweep1Delay = value;
    applySettings(QStringList("sweep1Delay"));
}

void RadioAstronomyGUI::on_sweep2Start_valueChanged(double value)
{
    m_settings.m_sweep2Start = value;
    applySettings(QStringList("sweep2Start"));
}

void RadioAstronomyGUI::on_sweep2Stop_valueChanged(double value)
{
    m_settings.m_sweep2Stop = value;
    applySettings(QStringList("sweep2Stop"));
}

void RadioAstronomyGUI::on_sweep2Step_valueChanged(double value)
{
    m_settings.m_sweep2Step = value;
    applySettings(QStringList("sweep2Step"));
}

void RadioAstronomyGUI::on_sweep2Delay_valueChanged(double value)
{
    m_settings.m_sweep2Delay = value;
    applySettings(QStringList("sweep2Delay"));
}

void RadioAstronomyGUI::on_sweepStartAtTime_currentIndexChanged(int index)
{
    m_settings.m_sweepStartAtTime = ui->sweepStartAtTime->currentIndex() == 1;
    ui->sweepStartDateTime->setVisible(index == 1);
    getRollupContents()->arrangeRollups();
    applySettings(QStringList("sweepStartAtTime"));
}

void RadioAstronomyGUI::on_sweepStartDateTime_dateTimeChanged(const QDateTime& dateTime)
{
    m_settings.m_sweepStartDateTime = dateTime;
    applySettings(QStringList("sweepStartDateTime"));
}

void RadioAstronomyGUI::on_startStop_clicked(bool checked)
{
    if (checked)
    {
        QString errorMessage;
        if (qFuzzyIsNull(m_settings.m_sweep1Step) && qFuzzyIsNull(m_settings.m_sweep2Step))
        {
            errorMessage = tr("Azimuth and elevation sweep steps cannot be zero.");
        }
        else if (qFuzzyIsNull(m_settings.m_sweep1Step))
        {
            errorMessage = tr("Azimuth sweep step cannot be zero.");
        }
        else if (qFuzzyIsNull(m_settings.m_sweep2Step))
        {
            errorMessage = tr("Elevation sweep step cannot be zero.");
        }
        if (!errorMessage.isEmpty())
        {
            QMessageBox::warning(this,
                tr("Invalid Sweep Configuration"),
                errorMessage);

            ui->startStop->blockSignals(true);
            ui->startStop->setChecked(false);
            ui->startStop->blockSignals(false);
            return;
        }

        ui->startStop->setStyleSheet("QToolButton { background-color : green; }");
        applySettings(QStringList("startStop"));
        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; }");
        }
        applySettings(QStringList("startStop"));
    }
}

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::autosave()
{
    if (!m_settings.m_powerAutoSaveCSVFilename.isEmpty())
    {
        qDebug() << "RadioAstronomyGUI: Autosaving power data to" << m_settings.m_powerAutoSaveCSVFilename;
        savePowerData(m_settings.m_powerAutoSaveCSVFilename);
    }
    if (!m_settings.m_spectrumAutoSaveCSVFilename.isEmpty())
    {
        qDebug() << "RadioAstronomyGUI: Autosaving spectrum data to" << m_settings.m_spectrumAutoSaveCSVFilename;
        saveSpectrumData(m_settings.m_spectrumAutoSaveCSVFilename);
    }
}

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, &ButtonSwitch::clicked, this, &RadioAstronomyGUI::on_savePowerData_clicked);
    QObject::connect(ui->savePowerChartImage, &QToolButton::clicked, this, &RadioAstronomyGUI::on_savePowerChartImage_clicked);
    QObject::connect(ui->saveSpectrumData, &ButtonSwitch::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->sweep1Start, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_sweep1Start_valueChanged);
    QObject::connect(ui->sweep1Stop, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_sweep1Stop_valueChanged);
    QObject::connect(ui->sweep1Step, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_sweep1Step_valueChanged);
    QObject::connect(ui->sweep1Delay, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_sweep1Delay_valueChanged);
    QObject::connect(ui->sweep2Start, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_sweep2Start_valueChanged);
    QObject::connect(ui->sweep2Stop, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_sweep2Stop_valueChanged);
    QObject::connect(ui->sweep2Step, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_sweep2Step_valueChanged);
    QObject::connect(ui->sweep2Delay, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_sweep2Delay_valueChanged);
    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->startStop, &ButtonSwitch::clicked, this, &RadioAstronomyGUI::on_startStop_clicked);
    QObject::connect(ui->power2DAutoscale, &QToolButton::clicked, this, &RadioAstronomyGUI::on_power2DAutoscale_clicked);
    QObject::connect(ui->power2DLinkSweep, &ButtonSwitch::clicked, this, &RadioAstronomyGUI::on_power2DLinkSweep_toggled);
    QObject::connect(ui->power2DSweepType, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_power2DSweepType_currentIndexChanged);
    QObject::connect(ui->power2DWidth, QOverload<int>::of(&QSpinBox::valueChanged), this, &RadioAstronomyGUI::on_power2DWidth_valueChanged);
    QObject::connect(ui->power2DHeight, QOverload<int>::of(&QSpinBox::valueChanged), this, &RadioAstronomyGUI::on_power2DHeight_valueChanged);
    QObject::connect(ui->power2DXMin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_power2DXMin_valueChanged);
    QObject::connect(ui->power2DXMax, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_power2DXMax_valueChanged);
    QObject::connect(ui->power2DYMin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_power2DYMin_valueChanged);
    QObject::connect(ui->power2DYMax, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &RadioAstronomyGUI::on_power2DYMax_valueChanged);
    QObject::connect(ui->powerShowSensor1, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_powerShowSensor1_toggled);
    QObject::connect(ui->powerShowSensor2, &ButtonSwitch::toggled, this, &RadioAstronomyGUI::on_powerShowSensor1_toggled);
    QObject::connect(ui->powerShowFiltered, &ButtonSwitch::clicked, this, &RadioAstronomyGUI::on_powerShowFiltered_clicked);
    QObject::connect(ui->powerFilter, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RadioAstronomyGUI::on_powerFilter_currentIndexChanged);
    QObject::connect(ui->powerFilterN, QOverload<int>::of(&QSpinBox::valueChanged), this, &RadioAstronomyGUI::on_powerFilterN_valueChanged);
    QObject::connect(ui->powerShowMeasurement, &ButtonSwitch::clicked, this, &RadioAstronomyGUI::on_powerShowMeasurement_clicked);
    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_centerFrequency + m_settings.m_inputFrequencyOffset);
}
