///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2023-2024 Jon Beniston, M7RCE                                   //
// Copyright (C) 2020 Edouard Griffiths, F4EXB                                   //
//                                                                               //
// 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 <QDesktopServices>
#include <QAction>
#include <QClipboard>
#include <QMediaPlayer>
#include <QVideoWidget>

#include "feature/featureuiset.h"
#include "feature/featurewebapiutils.h"
#include "channel/channelwebapiutils.h"
#include "gui/crightclickenabler.h"
#include "gui/basicfeaturesettingsdialog.h"
#include "gui/dialogpositioner.h"
#include "util/csv.h"
#include "util/astronomy.h"
#include "util/vlftransmitters.h"
#include "maincore.h"

#include "ui_sidgui.h"
#include "sid.h"
#include "sidgui.h"
#include "sidsettingsdialog.h"
#include "sidaddchannelsdialog.h"

#include "SWGMapItem.h"

SIDGUI* SIDGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature)
{
    SIDGUI* gui = new SIDGUI(pluginAPI, featureUISet, feature);
    return gui;
}

void SIDGUI::destroy()
{
    delete this;
}

void SIDGUI::resetToDefaults()
{
    m_settings.resetToDefaults();
    displaySettings();
    applyAllSettings();
}

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

bool SIDGUI::deserialize(const QByteArray& data)
{
    if (m_settings.deserialize(data))
    {
        m_feature->setWorkspaceIndex(m_settings.m_workspaceIndex);
        displaySettings();
        applyAllSettings();
        return true;
    }
    else
    {
        resetToDefaults();
        return false;
    }
}

bool SIDGUI::handleMessage(const Message& message)
{
    if (SIDMain::MsgConfigureSID::match(message))
    {
        qDebug("SIDGUI::handleMessage: SID::MsgConfigureSID");
        const SIDMain::MsgConfigureSID& cfg = (SIDMain::MsgConfigureSID&) message;

        if (cfg.getForce()) {
            m_settings = cfg.getSettings();
        } else {
            m_settings.applySettings(cfg.getSettingsKeys(), cfg.getSettings());
        }

        blockApplySettings(true);
        displaySettings();
        blockApplySettings(false);

        return true;
    }
    else if (SIDMain::MsgMeasurement::match(message))
    {
        // Measurements from SIDWorker
        const SIDMain::MsgMeasurement& measurementsMsg = (SIDMain::MsgMeasurement&) message;
        QDateTime dt = measurementsMsg.getDateTime();
        const QStringList& ids = measurementsMsg.getIds();
        const QList<double>& measurements = measurementsMsg.getMeasurements();

        for (int i = 0; i < ids.size(); i++) {
            addMeasurement(ids[i], dt, measurements[i]);
        }
        return true;
    }

    return false;
}

void SIDGUI::handleInputMessages()
{
    Message* message;

    while ((message = getInputMessageQueue()->pop()))
    {
        if (handleMessage(*message)) {
            delete message;
        }
    }
}

void SIDGUI::onWidgetRolled(QWidget* widget, bool rollDown)
{
    (void) widget;
    (void) rollDown;

    RollupContents *rollupContents = getRollupContents();
    rollupContents->saveState(m_rollupState);
    applySetting("rollupState");
}

SIDGUI::SIDGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) :
    FeatureGUI(parent),
    ui(new Ui::SIDGUI),
    m_pluginAPI(pluginAPI),
    m_featureUISet(featureUISet),
    m_doApplySettings(true),
    m_lastFeatureState(0),
    m_fileDialog(nullptr, "Select CSV file", "", "*.csv"),
    m_chartXAxis(nullptr),
    m_chartY1Axis(nullptr),
    m_chartY2Axis(nullptr),
    m_minMeasurement(std::numeric_limits<double>::quiet_NaN()),
    m_maxMeasurement(std::numeric_limits<double>::quiet_NaN()),
    m_xRayChartXAxis(nullptr),
    m_xRayChartYAxis(nullptr),
    m_goesXRay(nullptr),
    m_solarDynamicsObservatory(nullptr),
    m_player(nullptr),
    m_grb(nullptr),
    m_grbSeries(nullptr),
    m_stix(nullptr),
    m_stixSeries(nullptr),
    m_availableFeatureHandler({"sdrangel.feature.map"}),
    m_availableChannelHandler({}, "RM")
{
    m_feature = feature;
    setAttribute(Qt::WA_DeleteOnClose, true);
    m_helpURL = "plugins/feature/sid/readme.md";
    RollupContents *rollupContents = getRollupContents();
	ui->setupUi(rollupContents);
    rollupContents->arrangeRollups();
	connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));

    m_sid = reinterpret_cast<SIDMain*>(feature);
    m_sid->setMessageQueueToGUI(&m_inputMessageQueue);

    connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &)));
    connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));

    ui->startDateTime->blockSignals(true);
    ui->endDateTime->blockSignals(true);
    ui->startDateTime->setDateTime(QDateTime(QDate::currentDate(), QTime(0, 0, 0)));
    ui->endDateTime->setDateTime(QDateTime(QDate::currentDate().addDays(1), QTime(0, 0, 0)));
    ui->startDateTime->blockSignals(false);
    ui->endDateTime->blockSignals(false);

    // Initialise chart
    ui->chart->setRenderHint(QPainter::Antialiasing);
    ui->xRayChart->setRenderHint(QPainter::Antialiasing);

    connect(&m_statusTimer, &QTimer::timeout, this, &SIDGUI::updateStatus);
    m_statusTimer.start(250);

    connect(&m_autosaveTimer, &QTimer::timeout, this, &SIDGUI::autosave);

    m_settings.setRollupState(&m_rollupState);

    CRightClickEnabler *autoscaleXRightClickEnabler = new CRightClickEnabler(ui->autoscaleX);
    connect(autoscaleXRightClickEnabler, &CRightClickEnabler::rightClick, this, &SIDGUI::autoscaleXRightClicked);
    CRightClickEnabler *autoscaleYRightClickEnabler = new CRightClickEnabler(ui->autoscaleY);
    connect(autoscaleYRightClickEnabler, &CRightClickEnabler::rightClick, this, &SIDGUI::autoscaleYRightClicked);
    CRightClickEnabler *todayRightClickEnabler = new CRightClickEnabler(ui->today);
    connect(todayRightClickEnabler, &CRightClickEnabler::rightClick, this, &SIDGUI::todayRightClicked);

    makeUIConnections(); // Enable connections before displaySettings, so autoscaling works
    displaySettings();
    applyAllSettings();
    m_resizer.enableChildMouseTracking();

    // Initialisation for Solar Dynamics Observatory image/video display
    ui->sdoEnabled->setChecked(true);
    ui->sdoProgressBar->setVisible(false);
    ui->sdoImage->setStyleSheet("background-color: black;");
    ui->sdoVideo->setStyleSheet("background-color: black;");
    m_solarDynamicsObservatory = SolarDynamicsObservatory::create();
    if (m_solarDynamicsObservatory)
    {
        m_player = new QMediaPlayer();
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
        connect(m_player, qOverload<QMediaPlayer::Error>(&QMediaPlayer::error), this, &SIDGUI::sdoVideoError);
        connect(m_player, &QMediaPlayer::bufferStatusChanged, this, &SIDGUI::sdoBufferStatusChanged);
#else
        connect(m_player, &QMediaPlayer::errorOccurred, this, &SIDGUI::sdoVideoError);
        connect(m_player, &QMediaPlayer::bufferProgressChanged, this, &SIDGUI::sdoBufferProgressChanged);
#endif
        connect(m_player, &QMediaPlayer::mediaStatusChanged, this, &SIDGUI::sdoVideoStatusChanged);
        m_player->setVideoOutput(ui->sdoVideo);

        ui->sdoData->blockSignals(true);
        connect(m_solarDynamicsObservatory, &SolarDynamicsObservatory::imageUpdated, this, &SIDGUI::sdoImageUpdated);
        for (const auto& name : SolarDynamicsObservatory::getImageNames()) {
            ui->sdoData->addItem(name);
        }
        ui->sdoData->blockSignals(false);
        ui->sdoData->setCurrentIndex(1);
        m_settings.m_sdoData = ui->sdoData->currentText();
    }

    // Initialisation for GOES X-Ray data
    m_goesXRay = GOESXRay::create();
    if (m_goesXRay)
    {
        connect(m_goesXRay, &GOESXRay::xRayDataUpdated, this, &SIDGUI::xRayDataUpdated);
        connect(m_goesXRay, &GOESXRay::protonDataUpdated, this, &SIDGUI::protonDataUpdated);
        m_goesXRay->getDataPeriodically();
    }

    // Get Gamma Ray Bursts
    m_grb = GRB::create();
    if (m_grb)
    {
        connect(m_grb, &GRB::dataUpdated, this, &SIDGUI::grbDataUpdated);
        m_grb->getDataPeriodically();
    }

    // Get STIX Solar Flare data
    m_stix = STIX::create();
    if (m_stix)
    {
         connect(m_stix, &STIX::dataUpdated, this, &SIDGUI::stixDataUpdated);
         m_stix->getDataPeriodically();
    }

    plotChart();

    QObject::connect(
        &m_availableFeatureHandler,
        &AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged,
        this,
        &SIDGUI::featuresChanged
    );
    m_availableFeatureHandler.scanAvailableChannelsAndFeatures();
    QObject::connect(
        &m_availableChannelHandler,
        &AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged,
        this,
        &SIDGUI::channelsChanged
    );
    m_availableChannelHandler.scanAvailableChannelsAndFeatures();

    QObject::connect(ui->chartSplitter, &QSplitter::splitterMoved, this, &SIDGUI::chartSplitterMoved);
    QObject::connect(ui->sdoSplitter, &QSplitter::splitterMoved, this, &SIDGUI::sdoSplitterMoved);
}

SIDGUI::~SIDGUI()
{
    delete m_grb;
    delete m_stix;

    m_statusTimer.stop();

    clearFromMap();

    delete m_goesXRay;
    if (m_solarDynamicsObservatory)
    {
        delete m_player;
        delete m_solarDynamicsObservatory;
    }
    delete ui;
}


void SIDGUI::connectDataUpdates()
{
    if (m_goesXRay)
    {
        connect(m_goesXRay, &GOESXRay::xRayDataUpdated, this, &SIDGUI::xRayDataUpdated);
        connect(m_goesXRay, &GOESXRay::protonDataUpdated, this, &SIDGUI::protonDataUpdated);
    }
}

void SIDGUI::disconnectDataUpdates()
{
    if (m_goesXRay)
    {
        disconnect(m_goesXRay, &GOESXRay::xRayDataUpdated, this, &SIDGUI::xRayDataUpdated);
        disconnect(m_goesXRay, &GOESXRay::protonDataUpdated, this, &SIDGUI::protonDataUpdated);
    }
}

void SIDGUI::getData()
{
    if (m_goesXRay) {
        m_goesXRay->getData();
    }
}

void SIDGUI::setWorkspaceIndex(int index)
{
    m_settings.m_workspaceIndex = index;
    m_feature->setWorkspaceIndex(index);
    m_settingsKeys.append("workspaceIndex");
}

void SIDGUI::blockApplySettings(bool block)
{
    m_doApplySettings = !block;
}

void SIDGUI::displaySettings()
{
    setTitleColor(m_settings.m_rgbColor);
    setWindowTitle(m_settings.m_title);
    setTitle(m_settings.m_title);
    blockApplySettings(true);

    ui->samples->setValue(m_settings.m_samples);
    ui->separateCharts->setChecked(m_settings.m_separateCharts);
    ui->displayLegend->setChecked(m_settings.m_displayLegend);
    ui->plotXRayLongPrimary->setChecked(m_settings.m_plotXRayLongPrimary);
    ui->plotXRayLongSecondary->setChecked(m_settings.m_plotXRayLongSecondary);
    ui->plotXRayShortPrimary->setChecked(m_settings.m_plotXRayShortPrimary);
    ui->plotXRayShortSecondary->setChecked(m_settings.m_plotXRayShortSecondary);
    ui->plotGRB->setChecked(m_settings.m_plotGRB);
    ui->plotSTIX->setChecked(m_settings.m_plotSTIX);
    ui->plotProton->setChecked(m_settings.m_plotProton);
    ui->autoscaleX->setChecked(m_settings.m_autoscaleX);
    ui->autoscaleY->setChecked(m_settings.m_autoscaleY);
    ui->startDateTime->clearMaximumDateTime();
    ui->endDateTime->clearMinimumDateTime();
    if (m_settings.m_startDateTime.isValid()) {
        ui->startDateTime->setDateTime(m_settings.m_startDateTime);
    }
    if (m_settings.m_endDateTime.isValid()) {
        ui->endDateTime->setDateTime(m_settings.m_endDateTime);
    }
    ui->startDateTime->setMaximumDateTime(ui->endDateTime->dateTime());
    ui->endDateTime->setMinimumDateTime(ui->startDateTime->dateTime());
    ui->y1Min->setValue(m_settings.m_y1Min);
    ui->y1Max->setValue(m_settings.m_y1Max);
    setAutoscaleX();
    setAutoscaleY();
    setXAxisRange();
    setY1AxisRange();
    setAutosaveTimer();

    ui->sdoEnabled->setChecked(m_settings.m_sdoEnabled);
    ui->sdoVideoEnabled->setChecked(m_settings.m_sdoVideoEnabled);
    ui->sdoData->setCurrentText(m_settings.m_sdoData);
    ui->sdoNow->setChecked(m_settings.m_sdoNow);
    ui->sdoDateTime->setEnabled(!m_settings.m_sdoNow);
    ui->mapLabel->setEnabled(!m_settings.m_sdoNow);
    ui->map->setEnabled(!m_settings.m_sdoNow);
    ui->sdoDateTime->setDateTime(m_settings.m_sdoDateTime);
    ui->map->setCurrentText(m_settings.m_map);
    applySDO();
    applyDateTime();

    if (m_settings.m_autoload) {
        readCSV(m_settings.m_filename, true);
    }

    getRollupContents()->restoreState(m_rollupState);

    if (m_settings.m_chartSplitterSizes.size() > 0) {
        ui->chartSplitter->setSizes(m_settings.m_chartSplitterSizes);
    }
    if (m_settings.m_sdoSplitterSizes.size() > 0) {
        ui->sdoSplitter->setSizes(m_settings.m_sdoSplitterSizes);
    }

    blockApplySettings(false);
    getRollupContents()->arrangeRollups();
}

void SIDGUI::setAutosaveTimer()
{
    if (m_settings.m_autosave) {
        m_autosaveTimer.start(1000*60*m_settings.m_autosavePeriod);
    } else {
        m_autosaveTimer.stop();
    }
}

void SIDGUI::onMenuDialogCalled(const QPoint &p)
{
    if (m_contextMenuType == ContextMenuType::ContextMenuChannelSettings)
    {
        BasicFeatureSettingsDialog dialog(this);
        dialog.setTitle(m_settings.m_title);
        dialog.setUseReverseAPI(m_settings.m_useReverseAPI);
        dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress);
        dialog.setReverseAPIPort(m_settings.m_reverseAPIPort);
        dialog.setReverseAPIFeatureSetIndex(m_settings.m_reverseAPIFeatureSetIndex);
        dialog.setReverseAPIFeatureIndex(m_settings.m_reverseAPIFeatureIndex);
        dialog.setDefaultTitle(m_displayedName);

        dialog.move(p);
        new DialogPositioner(&dialog, false);
        dialog.exec();

        m_settings.m_title = dialog.getTitle();
        m_settings.m_useReverseAPI = dialog.useReverseAPI();
        m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress();
        m_settings.m_reverseAPIPort = dialog.getReverseAPIPort();
        m_settings.m_reverseAPIFeatureSetIndex = dialog.getReverseAPIFeatureSetIndex();
        m_settings.m_reverseAPIFeatureIndex = dialog.getReverseAPIFeatureIndex();

        setTitle(m_settings.m_title);
        setTitleColor(m_settings.m_rgbColor);

        QStringList settingsKeys({
            "rgbColor",
            "title",
            "useReverseAPI",
            "reverseAPIAddress",
            "reverseAPIPort",
            "reverseAPIDeviceIndex",
            "reverseAPIChannelIndex"
        });

        applySettings(m_settingsKeys);
    }

    resetContextMenuType();
}

void SIDGUI::applySetting(const QString& settingsKey)
{
    applySettings({settingsKey});
}

void SIDGUI::applySettings(const QStringList& settingsKeys, bool force)
{
    m_settingsKeys.append(settingsKeys);
    if (m_doApplySettings)
    {
        SIDMain::MsgConfigureSID* message = SIDMain::MsgConfigureSID::create(m_settings, m_settingsKeys, force);
        m_sid->getInputMessageQueue()->push(message);
        m_settingsKeys.clear();
    }

    m_settingsKeys.clear();
}

void SIDGUI::applyAllSettings()
{
    applySettings(QStringList(), true);
}

void SIDGUI::chartSplitterMoved(int pos, int index)
{
    (void) pos;
    (void) index;

    m_settings.m_chartSplitterSizes = ui->chartSplitter->sizes();
    applySetting("chartSplitterSizes");
}

void SIDGUI::sdoSplitterMoved(int pos, int index)
{
    (void) pos;
    (void) index;

    m_settings.m_sdoSplitterSizes = ui->sdoSplitter->sizes();
    applySetting("chartSplitterSizes");
}

void SIDGUI::on_samples_valueChanged(int value)
{
    m_settings.m_samples = value;
    applySetting("samples");
    plotChart();
}

void SIDGUI::on_separateCharts_toggled(bool checked)
{
    m_settings.m_separateCharts = checked;
    applySetting("separateCharts");
    plotChart();
}

void SIDGUI::on_displayLegend_toggled(bool checked)
{
    m_settings.m_displayLegend = checked;
    applySetting("displayLegend");
    plotChart();
}

void SIDGUI::on_plotXRayLongPrimary_toggled(bool checked)
{
    m_settings.m_plotXRayLongPrimary = checked;
    applySetting("plotXRayLongPrimary");
    plotChart();
}

void SIDGUI::on_plotXRayLongSecondary_toggled(bool checked)
{
    m_settings.m_plotXRayLongSecondary = checked;
    applySetting("plotXRayLongSecondary");
    plotChart();
}

void SIDGUI::on_plotXRayShortPrimary_toggled(bool checked)
{
    m_settings.m_plotXRayShortPrimary = checked;
    applySetting("plotXRayShortPrimary");
    plotChart();
}

void SIDGUI::on_plotXRayShortSecondary_toggled(bool checked)
{
    m_settings.m_plotXRayShortSecondary = checked;
    applySetting("plotXRayShortSecondary");
    plotChart();
}

void SIDGUI::on_plotGRB_toggled(bool checked)
{
    m_settings.m_plotGRB = checked;
    applySetting("plotGRB");
    plotChart();
}

void SIDGUI::on_plotSTIX_toggled(bool checked)
{
    m_settings.m_plotSTIX = checked;
    applySetting("plotSTIX");
    plotChart();
}

void SIDGUI::on_plotProton_toggled(bool checked)
{
    m_settings.m_plotProton = checked;
    applySetting("plotProton");
    plotChart();
}

void SIDGUI::on_startStop_toggled(bool checked)
{
    if (m_doApplySettings)
    {
        SIDMain::MsgStartStop *message = SIDMain::MsgStartStop::create(checked);
        m_sid->getInputMessageQueue()->push(message);
    }
}

void SIDGUI::createGRBSeries(QChart *chart, QDateTimeAxis *xAxis, QLogValueAxis *yAxis)
{
    bool secondaryAxis = plotAnyXRay() || m_settings.m_plotSTIX;
    yAxis->setLabelFormat("%.0e");
    yAxis->setGridLineVisible(!secondaryAxis);
    yAxis->setTitleText("GRB Fluence (erg/cm<sup>2</sup>)");
    yAxis->setTitleVisible(m_settings.m_displayAxisTitles);
    yAxis->setVisible(!secondaryAxis || m_settings.m_displaySecondaryAxis);

    if (m_settings.m_plotGRB)
    {
        m_grbSeries = new QScatterSeries();
        m_grbSeries->setName("GRB");
        m_grbSeries->setColor(m_settings.m_grbColor);
        m_grbSeries->setBorderColor(m_settings.m_grbColor);
        m_grbSeries->setMarkerSize(8);

        for (int i = 0; i < m_grbData.size(); i++)
        {
            float value = m_grbData[i].m_fluence;
            if ((value <= 0.0f) || std::isnan(value)) {
                value = m_grbMin; // <= 0 will result in series not being plotted, as log axis used
            }
            m_grbSeries->append(m_grbData[i].m_dateTime.toMSecsSinceEpoch(), value);
        }
        yAxis->setMin(m_grbMin);
        yAxis->setMax(m_grbMax);
        chart->addSeries(m_grbSeries);
        m_grbSeries->attachAxis(xAxis);
        m_grbSeries->attachAxis(yAxis);
    }
    else
    {
        m_grbSeries = nullptr;
    }
}

void SIDGUI::createFlareAxis(QCategoryAxis *yAxis)
{
    // Solar flare classification
    yAxis->setMin(-8);
    yAxis->setMax(-3);
    yAxis->setStartValue(-8);
    yAxis->append("A", -7);
    yAxis->append("B", -6);
    yAxis->append("C", -5);
    yAxis->append("M", -4);
    yAxis->append("X", -3);
    yAxis->setTitleText("Flare Class");
    yAxis->setTitleVisible(m_settings.m_displayAxisTitles);
    yAxis->setLineVisible(m_settings.m_displaySecondaryAxis);
    yAxis->setGridLineVisible(m_settings.m_separateCharts);
}

void SIDGUI::createXRaySeries(QChart *chart, QDateTimeAxis *xAxis, QCategoryAxis *yAxis)
{
    createFlareAxis(yAxis);

    for (int i = 0; i < 2; i++)
    {
        QString name = i == 0 ? "Primary" : "Secondary";

        if (((i == 0) && m_settings.m_plotXRayShortPrimary) || ((i == 1) && m_settings.m_plotXRayShortSecondary))
        {
            m_xrayShortMeasurements[i].m_series = new QLineSeries();
            m_xrayShortMeasurements[i].m_series->setName(QString("0.05-0.4nm X-Ray %1").arg(name));
            m_xrayShortMeasurements[i].m_series->setColor(m_settings.m_xrayShortColors[i]);
            for (int j = 0; j < m_xrayShortMeasurements[i].m_measurements.size(); j++) {
                m_xrayShortMeasurements[i].m_series->append(m_xrayShortMeasurements[i].m_measurements[j].m_dateTime.toMSecsSinceEpoch(), m_xrayShortMeasurements[i].m_measurements[j].m_measurement);
            }
            chart->addSeries(m_xrayShortMeasurements[i].m_series);
            m_xrayShortMeasurements[i].m_series->attachAxis(xAxis);
            m_xrayShortMeasurements[i].m_series->attachAxis(yAxis);
        }
        else
        {
            m_xrayShortMeasurements[i].m_series = nullptr;
        }

        if (((i == 0) && m_settings.m_plotXRayLongPrimary) || ((i == 1) && m_settings.m_plotXRayLongSecondary))
        {
            m_xrayLongMeasurements[i].m_series = new QLineSeries();
            m_xrayLongMeasurements[i].m_series->setName(QString("0.1-0.8nm X-Ray %1").arg(name));
            m_xrayLongMeasurements[i].m_series->setColor(m_settings.m_xrayLongColors[i]);
            for (int j = 0; j < m_xrayLongMeasurements[i].m_measurements.size(); j++) {
                m_xrayLongMeasurements[i].m_series->append(m_xrayLongMeasurements[i].m_measurements[j].m_dateTime.toMSecsSinceEpoch(), m_xrayLongMeasurements[i].m_measurements[j].m_measurement);
            }
            chart->addSeries(m_xrayLongMeasurements[i].m_series);
            m_xrayLongMeasurements[i].m_series->attachAxis(xAxis);
            m_xrayLongMeasurements[i].m_series->attachAxis(yAxis);
        }
        else
        {
            m_xrayLongMeasurements[i].m_series = nullptr;
        }
    }
}

const QStringList SIDGUI::m_protonEnergies = {"10 MeV", "50 MeV", "100 MeV", "500 MeV"};


void SIDGUI::createProtonSeries(QChart *chart, QDateTimeAxis *xAxis, QLogValueAxis *yAxis)
{
    bool secondaryAxis = plotAnyXRay() || m_settings.m_plotSTIX || m_settings.m_plotGRB;
    yAxis->setLabelFormat("%.0e");
    yAxis->setMin(0.01);
    yAxis->setMax(1000.0);
    yAxis->setGridLineVisible(!secondaryAxis);
    yAxis->setTitleText("Proton Flux (Particles / (cm<sup>2</sup> s sr))");
    yAxis->setTitleVisible(m_settings.m_displayAxisTitles);
    yAxis->setVisible(!secondaryAxis || m_settings.m_displaySecondaryAxis);

    for (int i = 0; i < 4; i += 2) // Only plot 10 and 100 MeV so graph isn't too cluttered
    //for (int i = 0; i < 4; i++)
    {
        m_protonMeasurements[i].m_series = new QLineSeries();
        m_protonMeasurements[i].m_series->setName(QString("%1 Proton").arg(SIDGUI::m_protonEnergies[i]));
        m_protonMeasurements[i].m_series->setColor(m_settings.m_protonColors[i]);

        for (int j = 0; j < m_protonMeasurements[i].m_measurements.size(); j++)
        {
            double value =  m_protonMeasurements[i].m_measurements[j].m_measurement;
            if (value >= 0.0) {
                m_protonMeasurements[i].m_series->append(m_protonMeasurements[i].m_measurements[j].m_dateTime.toMSecsSinceEpoch(), value);
            }
        }
        chart->addSeries(m_protonMeasurements[i].m_series);
        m_protonMeasurements[i].m_series->attachAxis(xAxis);
        m_protonMeasurements[i].m_series->attachAxis(yAxis);
    }
}

void SIDGUI::createSTIXSeries(QChart *chart, QDateTimeAxis *xAxis, QCategoryAxis *yAxis)
{
    createFlareAxis(yAxis);

    if (m_settings.m_plotSTIX)
    {
        m_stixSeries = new QScatterSeries();
        m_stixSeries->setName("STIX");
        m_stixSeries->setColor(m_settings.m_stixColor);
        m_stixSeries->setBorderColor(m_settings.m_stixColor);
        m_stixSeries->setMarkerSize(5);
        for (int i = 0; i < m_stixData.size(); i++)
        {
            double value = m_stixData[i].m_flux;
            if (value == 0.0)
            {
                value = -8;
            }
            else
            {
                value = log10(value);
            }
            m_stixSeries->append(m_stixData[i].m_startDateTime.toMSecsSinceEpoch(), value);
        }
        chart->addSeries(m_stixSeries);
        m_stixSeries->attachAxis(xAxis);
        m_stixSeries->attachAxis(yAxis);
    }
    else
    {
        m_stixSeries = nullptr;
    }
}

void SIDGUI::plotChart()
{
    QChart *oldChart = ui->chart->chart();
    QChart *chart;

    chart = new QChart();
    chart->layout()->setContentsMargins(0, 0, 0, 0);
    chart->setMargins(QMargins(1, 1, 1, 1));
    chart->setTheme(QChart::ChartThemeDark);
    chart->legend()->setVisible(m_settings.m_displayLegend);
    chart->legend()->setAlignment(m_settings.m_legendAlignment);

    m_chartXAxis = new QDateTimeAxis();
    m_chartY1Axis = new QValueAxis();
    m_chartY2Axis = nullptr;
    m_chartY3Axis = nullptr;
    m_chartProtonAxis = nullptr;
    if (!m_settings.m_separateCharts)
    {
        // XRay flux
        if (plotAnyXRay() || m_settings.m_plotSTIX)
        {
            m_chartY2Axis = new QCategoryAxis();
            chart->addAxis(m_chartY2Axis, Qt::AlignRight);
        }

        // GRB fluence
        if (m_settings.m_plotGRB)
        {
            m_chartY3Axis = new QLogValueAxis();
            chart->addAxis(m_chartY3Axis, Qt::AlignRight);
        }

        // Proton flux
        if (m_settings.m_plotProton)
        {
            m_chartProtonAxis = new QLogValueAxis();
            chart->addAxis(m_chartProtonAxis, Qt::AlignRight);
        }
    }

    chart->addAxis(m_chartXAxis, Qt::AlignBottom);
    chart->addAxis(m_chartY1Axis, Qt::AlignLeft);
    m_chartY1Axis->setTitleText("Power (dB)");
    m_chartY1Axis->setTitleVisible(m_settings.m_displayAxisTitles);

    // Power measurements
    for (auto& measurement : m_channelMeasurements)
    {
        SIDSettings::ChannelSettings *channelSettings = m_settings.getChannelSettings(measurement.m_id);
        if (!channelSettings) {
            qDebug() << "SIDGUI::plotChart: No settings for channel" << measurement.m_id;
        }
        if (channelSettings && channelSettings->m_enabled)
        {
            QLineSeries *series = new QLineSeries();
            series->setName(channelSettings->m_label);
            series->setColor(channelSettings->m_color);

            measurement.newSeries(series, m_settings.m_samples);
            for (int i = 0; i < measurement.m_measurements.size(); i++)
            {
                measurement.appendSeries(measurement.m_measurements[i].m_dateTime, measurement.m_measurements[i].m_measurement);
                updateMeasurementRange(measurement.m_measurements[i].m_measurement);
                updateTimeRange(measurement.m_measurements[i].m_dateTime);
            }
            chart->addSeries(measurement.m_series);
            measurement.m_series->attachAxis(m_chartXAxis);
            measurement.m_series->attachAxis(m_chartY1Axis);
        }
    }

    for (int i = 0; i < 2; i++)
    {
        m_xrayShortMeasurements[i].m_series = nullptr;
        m_xrayLongMeasurements[i].m_series = nullptr;
    }
    m_grbSeries = nullptr;
    m_stixSeries = nullptr;
    for (int i = 0; i < 4; i++) {
        m_protonMeasurements[i].m_series = nullptr;
    }

    if (!m_settings.m_separateCharts)
    {
        // XRay
        if (plotAnyXRay()) {
            createXRaySeries(chart, m_chartXAxis, m_chartY2Axis);
        }

        // GRB
        if (m_settings.m_plotGRB) {
            createGRBSeries(chart, m_chartXAxis, m_chartY3Axis);
        }

        // STIX flares
        if (m_settings.m_plotSTIX) {
            createSTIXSeries(chart, m_chartXAxis, m_chartY2Axis);
        }

        // Proton flux
        if (m_settings.m_plotProton) {
            createProtonSeries(chart, m_chartXAxis, m_chartProtonAxis);
        }
    }

    autoscaleX();
    autoscaleY();
    setXAxisRange();
    setY1AxisRange();

    ui->chart->setChart(chart);
    ui->chart->installEventFilter(this);

    delete oldChart;

    const auto markers = chart->legend()->markers();
    for (QLegendMarker *marker : markers)
    {
        connect(marker, &QLegendMarker::clicked, this, &SIDGUI::legendMarkerClicked);
    }

    for (const auto series : chart->series())
    {
        QXYSeries *s = qobject_cast<QXYSeries *>(series);
        if (s) {
            connect(s, &QXYSeries::clicked, this, &SIDGUI::seriesClicked);
        }
    }

    if (m_settings.m_separateCharts)
    {
        ui->xRayChart->setVisible(true);
        plotXRayChart();
    }
    else
    {
        ui->xRayChart->setVisible(false);
    }
}

bool SIDGUI::plotAnyXRay() const
{
    return m_settings.m_plotXRayLongPrimary || m_settings.m_plotXRayLongSecondary
        || m_settings.m_plotXRayShortPrimary || m_settings.m_plotXRayShortSecondary;
}

void SIDGUI::plotXRayChart()
{
    QChart *oldChart = ui->xRayChart->chart();
    QChart *chart;

    chart = new QChart();
    chart->layout()->setContentsMargins(0, 0, 0, 0);
    chart->setMargins(QMargins(1, 1, 1, 1));
    chart->setTheme(QChart::ChartThemeDark);
    chart->legend()->setVisible(m_settings.m_displayLegend);
    chart->legend()->setAlignment(m_settings.m_legendAlignment);

    m_xRayChartXAxis = new QDateTimeAxis();
    chart->addAxis(m_xRayChartXAxis, Qt::AlignBottom);

    if (plotAnyXRay() || m_settings.m_plotSTIX)
    {
        m_xRayChartYAxis = new QCategoryAxis();
        chart->addAxis(m_xRayChartYAxis, Qt::AlignLeft);
    }

    if (m_settings.m_plotGRB)
    {
        m_chartY3Axis = new QLogValueAxis();
        chart->addAxis(m_chartY3Axis, (plotAnyXRay() || m_settings.m_plotSTIX) ? Qt::AlignRight : Qt::AlignLeft);
    }

    if (m_settings.m_plotProton)
    {
        m_chartProtonAxis = new QLogValueAxis();
        chart->addAxis(m_chartProtonAxis, (plotAnyXRay() || m_settings.m_plotSTIX || m_settings.m_plotGRB) ? Qt::AlignRight : Qt::AlignLeft);
    }

    // XRay
    if (plotAnyXRay()) {
        createXRaySeries(chart, m_xRayChartXAxis, m_xRayChartYAxis);
    }

    // GRB
    if (m_settings.m_plotGRB) {
        createGRBSeries(chart, m_xRayChartXAxis, m_chartY3Axis);
    }

    // STIX flares
    if (m_settings.m_plotSTIX) {
        createSTIXSeries(chart, m_xRayChartXAxis, m_xRayChartYAxis);
    }

    // Proton flux
    if (m_settings.m_plotProton) {
        createProtonSeries(chart, m_xRayChartXAxis, m_chartProtonAxis);
    }

    setXAxisRange();

    ui->xRayChart->setChart(chart);
    ui->xRayChart->installEventFilter(this);

    delete oldChart;

    const auto markers = chart->legend()->markers();
    for (QLegendMarker *marker : markers) {
        connect(marker, &QLegendMarker::clicked, this, &SIDGUI::legendMarkerClicked);
    }

    for (const auto series : chart->series())
    {
        QXYSeries *s = qobject_cast<QXYSeries *>(series);
        if (s) {
            connect(s, &QXYSeries::clicked, this, &SIDGUI::seriesClicked);
        }
    }

    if (!(plotAnyXRay() || m_settings.m_plotGRB || m_settings.m_plotSTIX || m_settings.m_plotProton))
    {
        ui->xRayChart->setVisible(false); // Hide empty chart
    }
}

void SIDGUI::legendMarkerClicked()
{
    QLegendMarker* marker = qobject_cast<QLegendMarker*>(sender());
    marker->series()->setVisible(!marker->series()->isVisible());
    marker->setVisible(true);

    // Dim the marker, if series is not visible
    qreal alpha = 1.0;

    if (!marker->series()->isVisible()) {
        alpha = 0.5;
    }

    QColor color;
    QBrush brush = marker->labelBrush();
    color = brush.color();
    color.setAlphaF(alpha);
    brush.setColor(color);
    marker->setLabelBrush(brush);

    brush = marker->brush();
    color = brush.color();
    color.setAlphaF(alpha);
    brush.setColor(color);
    marker->setBrush(brush);

    QPen pen = marker->pen();
    color = pen.color();
    color.setAlphaF(alpha);
    pen.setColor(color);
    marker->setPen(pen);
}

void SIDGUI::seriesClicked(const QPointF &point)
{
    QDateTime dt = QDateTime::fromMSecsSinceEpoch(point.x());
    ui->sdoDateTime->setDateTime(dt);
}

static qreal distance(const QPointF& a, const QPointF& b)
{
    qreal dx = a.x() - b.x();
    qreal dy = a.y() - b.y();
    return qSqrt(dx * dx + dy * dy);
}

qreal SIDGUI::pixelDistance(QChart *chart, QAbstractSeries *series, QPointF a, QPointF b)
{
    a = chart->mapToPosition(a, series);
    b = chart->mapToPosition(b, series);
    return distance(a, b);
}

void SIDGUI::sendToSkyMap(const AvailableChannelOrFeature& skymap, float ra, float dec)
{
    QString target = QString("%1 %2").arg(ra).arg(dec);
    FeatureWebAPIUtils::skyMapFind(target, skymap.m_superIndex, skymap.m_index);
}

void SIDGUI::showGRBContextMenu(QContextMenuEvent *contextEvent, QChartView *chartView, int closestPoint)
{
    QMenu *contextMenu = new QMenu(chartView);
    connect(contextMenu, &QMenu::aboutToHide, contextMenu, &QMenu::deleteLater);

    contextMenu->addSection(m_grbData[closestPoint].m_name);

    // Display GRB Fermi data
    QString url = m_grbData[closestPoint].getFermiURL();
    if (!url.isEmpty())
    {
        QAction* fermiDataAction = new QAction("View Fermi data directory...", contextMenu);
        connect(fermiDataAction, &QAction::triggered, this, [url]()->void {
            QDesktopServices::openUrl(QUrl(url));
        });
        contextMenu->addAction(fermiDataAction);

        QString plotURL = m_grbData[closestPoint].getFermiPlotURL();
        QAction* fermiPlotAction = new QAction("View Fermi data plot...", contextMenu);
        connect(fermiPlotAction, &QAction::triggered, this, [plotURL]()->void {
            QDesktopServices::openUrl(QUrl(plotURL));
        });
        contextMenu->addAction(fermiPlotAction);

        QString mapURL = m_grbData[closestPoint].getFermiSkyMapURL();
        QAction* fermiMapDataAction = new QAction("View Fermi sky map...", contextMenu);
        connect(fermiMapDataAction, &QAction::triggered, this, [mapURL]()->void {
            QDesktopServices::openUrl(QUrl(mapURL));
        });
        contextMenu->addAction(fermiMapDataAction);
    }

    // Display Swift link
    if (!m_grbData[closestPoint].m_name.endsWith("*"))
    {
        QAction* swiftDataAction = new QAction("View Swift data...", contextMenu);
        QString switftURL = m_grbData[closestPoint].getSwiftURL();
        connect(swiftDataAction, &QAction::triggered, this, [switftURL]()->void {
            QDesktopServices::openUrl(QUrl(switftURL));
        });
        contextMenu->addAction(swiftDataAction);
    }

    // View GRB ra/dec in SkyMap
    AvailableChannelOrFeatureHandler skymaps({"sdrangel.feature.skymap"});
    skymaps.scanAvailableChannelsAndFeatures();
    if (skymaps.getAvailableChannelOrFeatureList().size() > 0)
    {
        for (const auto& skymap : skymaps.getAvailableChannelOrFeatureList())
        {
            QString label = QString("View coords in %1...").arg(skymap.getLongId());
            QAction* skyMapAction = new QAction(label, contextMenu);
            float ra = m_grbData[closestPoint].m_ra;
            float dec = m_grbData[closestPoint].m_dec;
            connect(skyMapAction, &QAction::triggered, this, [this, skymap, ra, dec]()->void {
                sendToSkyMap(skymap, ra, dec);
            });
            contextMenu->addAction(skyMapAction);
        }
    }
    else
    {
        QAction* skyMapAction = new QAction("View coords in SkyMap...", contextMenu);
        float ra = m_grbData[closestPoint].m_ra;
        float dec = m_grbData[closestPoint].m_dec;
        QString target = QString("%1 %2").arg(ra).arg(dec);
        connect(skyMapAction, &QAction::triggered, this, [target]()->void {
            FeatureWebAPIUtils::openSkyMapAndFind(target);
        });
        contextMenu->addAction(skyMapAction);
    }

    contextMenu->popup(chartView->viewport()->mapToGlobal(contextEvent->pos()));
}

void SIDGUI::showStixContextMenu(QContextMenuEvent *contextEvent, QChartView *chartView, int closestPoint)
{
    QMenu *contextMenu = new QMenu(chartView);
    connect(contextMenu, &QMenu::aboutToHide, contextMenu, &QMenu::deleteLater);

    contextMenu->addSection(m_stixData[closestPoint].m_id);

    // Display GRB Fermi data
    QString lcURL = m_stixData[closestPoint].getLightCurvesURL();
    QAction* lcAction = new QAction("View light curves...", contextMenu);
    connect(lcAction, &QAction::triggered, this, [lcURL]()->void {
        QDesktopServices::openUrl(QUrl(lcURL));
    });
    contextMenu->addAction(lcAction);

    QString dataURL = m_stixData[closestPoint].getDataURL();
    QAction* stixDataAction = new QAction("View STIX data...", contextMenu);
    connect(stixDataAction, &QAction::triggered, this, [dataURL]()->void {
        QDesktopServices::openUrl(QUrl(dataURL));
    });
    contextMenu->addAction(stixDataAction);

    contextMenu->popup(chartView->viewport()->mapToGlobal(contextEvent->pos()));
}

bool SIDGUI::findClosestPoint(QContextMenuEvent *contextEvent, QChart *chart, QScatterSeries *series, int& closestPoint)
{
    QPointF point = chart->mapToValue(contextEvent->pos(), series);
    QDateTime dt = QDateTime::fromMSecsSinceEpoch(point.x());

    // Find nearest point - GRB/Stix data is ordered newest first
    QVector<QPointF> points = series->pointsVector();
    if (points.size() > 0)
    {
        qint64 startTime = m_settings.m_startDateTime.toMSecsSinceEpoch();
        qreal closestDistance = pixelDistance(chart, series, point, points[0]);
        closestPoint = 0;

        for (int i = 1; i < points.size(); i++)
        {
            qreal d = pixelDistance(chart, series, point, points[i]);
            if (d < closestDistance)
            {
                closestDistance = d;
                closestPoint = i;
            }
            if (points[i].x() < startTime) {
                break;
            }
        }
        return closestDistance <= series->markerSize();
    }
    else
    {
        return false;
    }
}

void SIDGUI::showContextMenu(QContextMenuEvent *contextEvent)
{
    QChartView *chartView;

    if (m_settings.m_separateCharts) {
        chartView = ui->xRayChart;
    } else {
        chartView = ui->chart;
    }

    if (chartView)
    {
        int closestPoint;

        if (m_grbSeries && findClosestPoint(contextEvent, chartView->chart(), m_grbSeries, closestPoint)) {
            showGRBContextMenu(contextEvent, chartView, closestPoint);
        } else if (m_stixSeries && findClosestPoint(contextEvent, chartView->chart(), m_stixSeries, closestPoint)) {
            showStixContextMenu(contextEvent, chartView, closestPoint);
        }
    }
}

bool SIDGUI::eventFilter(QObject *obj, QEvent *event)
{
    if ((obj == ui->chart) || (obj == ui->xRayChart))
    {
        if (event->type() == QEvent::ContextMenu)
        {
            // Show context menu on chart for GRBs/Flares
            QContextMenuEvent *contextEvent = static_cast<QContextMenuEvent *>(event);

            showContextMenu(contextEvent);
            contextEvent->accept();
            return true;
        }
        else if (event->type() == QEvent::Wheel)
        {
            // Use wheel to zoom in / out of X axis or Y axis if shift held
            QWheelEvent *wheelEvent = static_cast<QWheelEvent *>(event);

            int delta = wheelEvent->angleDelta().y(); // delta is typically 120 for one click of wheel

            if (wheelEvent->modifiers() & Qt::ShiftModifier)
            {
                double min = ui->y1Min->value();
                double max = ui->y1Max->value();
                double adj = (max - min) * 0.20 * delta / 120.0;
                min += adj;
                max -= adj;
                ui->y1Min->setValue(min);
                ui->y1Max->setValue(max);
            }
            else
            {
                QDateTime start = ui->startDateTime->dateTime();
                QDateTime end = ui->endDateTime->dateTime();
                qint64 startMS = start.toMSecsSinceEpoch();
                qint64 endMS = end.toMSecsSinceEpoch();
                qint64 diff = endMS - startMS;
                qint64 adj = diff * 0.20 * delta / 120.0;
                endMS -= adj;
                startMS += adj;
                start = QDateTime::fromMSecsSinceEpoch(startMS);
                end = QDateTime::fromMSecsSinceEpoch(endMS);
                ui->startDateTime->setDateTime(start);
                ui->endDateTime->setDateTime(end);
            }

            wheelEvent->accept();
            return true;
        }
    }
    return FeatureGUI::eventFilter(obj, event);
}

void SIDGUI::updateMeasurementRange(double measurement)
{
    if (std::isnan(m_minMeasurement)) {
        m_minMeasurement = measurement;
    } else {
        m_minMeasurement = std::min(m_minMeasurement, measurement);
    }
    if (std::isnan(m_maxMeasurement)) {
        m_maxMeasurement = measurement;
    } else {
        m_maxMeasurement = std::max(m_maxMeasurement, measurement);
    }
}

void SIDGUI::updateTimeRange(QDateTime dateTime)
{
    if (!m_minDateTime.isValid() || (dateTime < m_minDateTime)) {
        m_minDateTime = dateTime;
    }
    if (!m_maxDateTime.isValid() || (dateTime > m_maxDateTime)) {
        m_maxDateTime = dateTime;
    }
}

void SIDGUI::setXAxisRange()
{
    if (m_chartXAxis) {
        m_chartXAxis->setRange(m_settings.m_startDateTime, m_settings.m_endDateTime);
    }
    if (m_xRayChartXAxis) {
        m_xRayChartXAxis->setRange(m_settings.m_startDateTime, m_settings.m_endDateTime);
    }
}

void SIDGUI::setY1AxisRange()
{
    if (m_chartY1Axis) {
        m_chartY1Axis->setRange(m_settings.m_y1Min, m_settings.m_y1Max);
    }
}

void SIDGUI::setButtonBackground(QToolButton *button, bool checked)
{
    if (!checked)
    {
        button->setStyleSheet("");
    }
    else
    {
        button->setStyleSheet(QString("QToolButton{ background-color: %1;  }")
            .arg(palette().highlight().color().darker(150).name()));
    }
}

void SIDGUI::setAutoscaleX()
{
    setButtonBackground(ui->autoscaleX, m_settings.m_autoscaleX);
}

void SIDGUI::setAutoscaleY()
{
    setButtonBackground(ui->autoscaleY, m_settings.m_autoscaleY);
}

void SIDGUI::on_autoscaleX_clicked()
{
    ui->startDateTime->clearMaximumDateTime();
    ui->endDateTime->clearMinimumDateTime();

    if (m_minDateTime.isValid())
    {
        ui->startDateTime->setDateTime(m_minDateTime);
    }
    if (m_maxDateTime.isValid())
    {
        ui->endDateTime->setDateTime(m_maxDateTime);
    }

    ui->startDateTime->setMaximumDateTime(ui->endDateTime->dateTime());
    ui->endDateTime->setMinimumDateTime(ui->startDateTime->dateTime());
}

void SIDGUI::on_autoscaleY_clicked()
{
    if (!std::isnan(m_minMeasurement) && !std::isnan(m_maxMeasurement) && (m_minMeasurement == m_maxMeasurement))
    {
        // Graph doesn't display properly if min is the same as max
        ui->y1Min->setValue(m_minMeasurement * 0.99);
        ui->y1Max->setValue(m_maxMeasurement * 1.01);
    }
    else
    {
        if (!std::isnan(m_minMeasurement)) {
            ui->y1Min->setValue(m_minMeasurement);
        }
        if (!std::isnan(m_maxMeasurement)) {
            ui->y1Max->setValue(m_maxMeasurement);
        }
    }
}

void SIDGUI::on_today_clicked()
{
    QDate today = QDate::currentDate();
    QDateTime start = QDateTime(today, QTime(0,0));
    QDateTime end = QDateTime(today.addDays(1), QTime(0,0));

    ui->startDateTime->clearMaximumDateTime();
    ui->endDateTime->clearMinimumDateTime();

    ui->startDateTime->setDateTime(start);
    ui->endDateTime->setDateTime(end);

    ui->startDateTime->setMaximumDateTime(ui->endDateTime->dateTime());
    ui->endDateTime->setMinimumDateTime(ui->startDateTime->dateTime());
}

void SIDGUI::todayRightClicked()
{
    float stationLatitude = MainCore::instance()->getSettings().getLatitude();
    float stationLongitude = MainCore::instance()->getSettings().getLongitude();

    QDate today = QDate::currentDate();

    QDateTime sunRise, sunSet;
    Astronomy::sunrise(today, stationLatitude, stationLongitude, sunRise, sunSet);

    ui->startDateTime->clearMaximumDateTime();
    ui->endDateTime->clearMinimumDateTime();

    ui->startDateTime->setDateTime(sunRise);
    ui->endDateTime->setDateTime(sunSet);

    ui->startDateTime->setMaximumDateTime(ui->endDateTime->dateTime());
    ui->endDateTime->setMinimumDateTime(ui->startDateTime->dateTime());
}

void SIDGUI::on_prevDay_clicked()
{
    ui->startDateTime->clearMaximumDateTime();
    ui->endDateTime->clearMinimumDateTime();

    ui->startDateTime->setDateTime(ui->startDateTime->dateTime().addDays(-1));
    ui->endDateTime->setDateTime(ui->endDateTime->dateTime().addDays(-1));

    ui->startDateTime->setMaximumDateTime(ui->endDateTime->dateTime());
    ui->endDateTime->setMinimumDateTime(ui->startDateTime->dateTime());
}

void SIDGUI::on_nextDay_clicked()
{
    ui->startDateTime->clearMaximumDateTime();
    ui->endDateTime->clearMinimumDateTime();

    ui->endDateTime->setDateTime(ui->endDateTime->dateTime().addDays(1));
    ui->startDateTime->setDateTime(ui->startDateTime->dateTime().addDays(1));

    ui->startDateTime->setMaximumDateTime(ui->endDateTime->dateTime());
    ui->endDateTime->setMinimumDateTime(ui->startDateTime->dateTime());
}

void SIDGUI::autoscaleXRightClicked()
{
    m_settings.m_autoscaleX = !m_settings.m_autoscaleX;
    applySetting("autoscaleX");
    setAutoscaleX();
}

void SIDGUI::autoscaleYRightClicked()
{
    m_settings.m_autoscaleY = !m_settings.m_autoscaleY;
    applySetting("autoscaleY");
    setAutoscaleY();
}

void SIDGUI::on_startDateTime_dateTimeChanged(QDateTime value)
{
    m_settings.m_startDateTime = value;
    applySetting("startDateTime");
    setXAxisRange();
    ui->endDateTime->setMinimumDateTime(value);
}

void SIDGUI::on_endDateTime_dateTimeChanged(QDateTime value)
{
    m_settings.m_endDateTime = value;
    applySetting("endDateTime");
    setXAxisRange();
    ui->startDateTime->setMaximumDateTime(value);
}

void SIDGUI::on_y1Min_valueChanged(double value)
{
    m_settings.m_y1Min = (float) value;
    applySetting("y1Min");
    setY1AxisRange();
}

void SIDGUI::on_y1Max_valueChanged(double value)
{
    m_settings.m_y1Max = (float) value;
    applySetting("y1Max");
    setY1AxisRange();
}

void SIDGUI::clearMinMax()
{
    m_minDateTime = QDateTime();
    m_maxDateTime = QDateTime();
    m_minMeasurement = std::numeric_limits<double>::quiet_NaN();
    m_maxMeasurement = std::numeric_limits<double>::quiet_NaN();
}

void SIDGUI::clearAllData()
{
    m_channelMeasurements.clear();
    for (int i = 0; i < 2; i++)
    {
        m_xrayShortMeasurements[i].clear();
        m_xrayLongMeasurements[i].clear();
    }
    for (int i = 0; i < 4; i++) {
        m_protonMeasurements[i].clear();
    }
    clearMinMax();
}

void SIDGUI::on_deleteAll_clicked()
{
    clearAllData();
    plotChart();
    getData();
}

void SIDGUI::on_addChannels_clicked()
{
    SIDAddChannelsDialog dialog(&m_settings);

    new DialogPositioner(&dialog, true);

    dialog.exec();
}


void SIDGUI::on_settings_clicked()
{
    SIDSettingsDialog dialog(&m_settings);

    QObject::connect(
        &dialog,
        &SIDSettingsDialog::removeChannels,
        this,
        &SIDGUI::removeChannels
    );

    new DialogPositioner(&dialog, true);

    if (dialog.exec() == QDialog::Accepted)
    {
        setAutosaveTimer();
        QStringList settingsKeys;
        settingsKeys.append("period");
        settingsKeys.append("autosave");
        settingsKeys.append("autoload");
        settingsKeys.append("filename");
        settingsKeys.append("autosavePeriod");
        settingsKeys.append("legendAlignment");
        settingsKeys.append("displayAxisTitles");
        settingsKeys.append("displayAxisLabels");
        settingsKeys.append("channelSettings");
        settingsKeys.append("xrayShortColors");
        settingsKeys.append("xrayLongColors");
        settingsKeys.append("protonColors");
        settingsKeys.append("grbColor");
        settingsKeys.append("stixColor");
        applySettings(settingsKeys);
        plotChart();
    }
}

void SIDGUI::updateStatus()
{
    int state = m_sid->getState();

    if (m_lastFeatureState != state)
    {
        // We set checked state of start/stop button, in case it was changed via API
        bool oldState;
        switch (state)
        {
            case Feature::StNotStarted:
                ui->startStop->setStyleSheet("QToolButton { background:rgb(79,79,79); }");
                break;
            case Feature::StIdle:
                oldState = ui->startStop->blockSignals(true);
                ui->startStop->setChecked(false);
                ui->startStop->blockSignals(oldState);
                ui->startStop->setStyleSheet("QToolButton { background-color : blue; }");
                break;
            case Feature::StRunning:
                oldState = ui->startStop->blockSignals(true);
                ui->startStop->setChecked(true);
                ui->startStop->blockSignals(oldState);
                ui->startStop->setStyleSheet("QToolButton { background-color : green; }");
                break;
            case Feature::StError:
                ui->startStop->setStyleSheet("QToolButton { background-color : red; }");
                QMessageBox::critical(this, m_settings.m_title, m_sid->getErrorMessage());
                break;
            default:
                break;
        }

        m_lastFeatureState = state;
    }
}

void SIDGUI::makeUIConnections()
{
    QObject::connect(ui->startStop, &ButtonSwitch::toggled, this, &SIDGUI::on_startStop_toggled);
    QObject::connect(ui->samples, QOverload<int>::of(&QSpinBox::valueChanged), this, &SIDGUI::on_samples_valueChanged);
    QObject::connect(ui->separateCharts, &ButtonSwitch::toggled, this, &SIDGUI::on_separateCharts_toggled);
    QObject::connect(ui->displayLegend, &ButtonSwitch::toggled, this, &SIDGUI::on_displayLegend_toggled);
    QObject::connect(ui->plotXRayLongPrimary, &ButtonSwitch::toggled, this, &SIDGUI::on_plotXRayLongPrimary_toggled);
    QObject::connect(ui->plotXRayLongSecondary, &ButtonSwitch::toggled, this, &SIDGUI::on_plotXRayLongSecondary_toggled);
    QObject::connect(ui->plotXRayShortPrimary, &ButtonSwitch::toggled, this, &SIDGUI::on_plotXRayShortPrimary_toggled);
    QObject::connect(ui->plotXRayShortSecondary, &ButtonSwitch::toggled, this, &SIDGUI::on_plotXRayShortSecondary_toggled);
    QObject::connect(ui->plotGRB, &ButtonSwitch::toggled, this, &SIDGUI::on_plotGRB_toggled);
    QObject::connect(ui->plotSTIX, &ButtonSwitch::toggled, this, &SIDGUI::on_plotSTIX_toggled);
    QObject::connect(ui->plotProton, &ButtonSwitch::toggled, this, &SIDGUI::on_plotProton_toggled);
    QObject::connect(ui->sdoEnabled, &ButtonSwitch::toggled, this, &SIDGUI::on_sdoEnabled_toggled);
    QObject::connect(ui->sdoVideoEnabled, &ButtonSwitch::toggled, this, &SIDGUI::on_sdoVideoEnabled_toggled);
    QObject::connect(ui->sdoData, qOverload<int>(&QComboBox::currentIndexChanged), this, &SIDGUI::on_sdoData_currentIndexChanged);
    QObject::connect(ui->sdoNow, &ButtonSwitch::toggled, this, &SIDGUI::on_sdoNow_toggled);
    QObject::connect(ui->sdoDateTime, &WrappingDateTimeEdit::dateTimeChanged, this, &SIDGUI::on_sdoDateTime_dateTimeChanged);
    QObject::connect(ui->showSats, &QToolButton::clicked, this, &SIDGUI::on_showSats_clicked);
    QObject::connect(ui->map, &QComboBox::currentTextChanged, this, &SIDGUI::on_map_currentTextChanged);
    QObject::connect(ui->showPaths, &QToolButton::clicked, this, &SIDGUI::on_showPaths_clicked);
    QObject::connect(ui->autoscaleX, &QPushButton::clicked, this, &SIDGUI::on_autoscaleX_clicked);
    QObject::connect(ui->autoscaleY, &QPushButton::clicked, this, &SIDGUI::on_autoscaleY_clicked);
    QObject::connect(ui->today, &QPushButton::clicked, this, &SIDGUI::on_today_clicked);
    QObject::connect(ui->prevDay, &QPushButton::clicked, this, &SIDGUI::on_prevDay_clicked);
    QObject::connect(ui->nextDay, &QPushButton::clicked, this, &SIDGUI::on_nextDay_clicked);
    QObject::connect(ui->startDateTime, &WrappingDateTimeEdit::dateTimeChanged, this, &SIDGUI::on_startDateTime_dateTimeChanged);
    QObject::connect(ui->endDateTime, &WrappingDateTimeEdit::dateTimeChanged, this, &SIDGUI::on_endDateTime_dateTimeChanged);
    QObject::connect(ui->y1Min, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &SIDGUI::on_y1Min_valueChanged);
    QObject::connect(ui->y1Max, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &SIDGUI::on_y1Max_valueChanged);
    QObject::connect(ui->deleteAll, &QToolButton::clicked, this, &SIDGUI::on_deleteAll_clicked);
    QObject::connect(ui->saveData, &QToolButton::clicked, this, &SIDGUI::on_saveData_clicked);
    QObject::connect(ui->loadData, &QToolButton::clicked, this, &SIDGUI::on_loadData_clicked);
    QObject::connect(ui->saveChartImage, &QToolButton::clicked, this, &SIDGUI::on_saveChartImage_clicked);
    QObject::connect(ui->addChannels, &QToolButton::clicked, this, &SIDGUI::on_addChannels_clicked);
    QObject::connect(ui->settings, &QToolButton::clicked, this, &SIDGUI::on_settings_clicked);
}

SIDGUI::ChannelMeasurement& SIDGUI::addMeasurements(const QString& id)
{
    ChannelMeasurement measurements = ChannelMeasurement(id, m_settings.m_samples);
    m_channelMeasurements.append(measurements);
    return m_channelMeasurements.last();
}

SIDGUI::ChannelMeasurement& SIDGUI::getMeasurements(const QString& id)
{
    for (int i = 0; i < m_channelMeasurements.size(); i++)
    {
        if (m_channelMeasurements[i].m_id == id)
        {
            return m_channelMeasurements[i];
        }
    }
    return addMeasurements(id);
}

void SIDGUI::addMeasurement(const QString& id, QDateTime dateTime, double measurement)
{
    ChannelMeasurement& measurements = getMeasurements(id);
    measurements.append(dateTime, measurement);
    if (m_chartXAxis)
    {
        if (measurements.m_series)
        {
            updateMeasurementRange(measurement);
            updateTimeRange(dateTime);
            autoscaleX();
            autoscaleY();
        }
        else
        {
            qDebug() << "addMeasurement - measurement has no series calling plotChart";
            plotChart();
        }
    }
    else
    {
        qDebug() << "addMeasurement with no m_chartXAxis - calling plotChart";
        plotChart();
    }
}

void SIDGUI::autoscaleX()
{
    if (m_settings.m_autoscaleX)
    {
        if (m_maxDateTime.isValid() && (!m_settings.m_endDateTime.isValid() || (m_maxDateTime > m_settings.m_endDateTime))) {
            ui->endDateTime->setDateTime(m_maxDateTime);
        }
        if (m_minDateTime.isValid() && (!m_settings.m_startDateTime.isValid() || (m_minDateTime < m_settings.m_startDateTime))) {
            ui->startDateTime->setDateTime(m_minDateTime);
        }
    }
}

void SIDGUI::autoscaleY()
{
    if (m_settings.m_autoscaleY)
    {
        if (!std::isnan(m_minMeasurement) && !std::isnan(m_maxMeasurement) && (m_minMeasurement == m_maxMeasurement))
        {
            // Graph doesn't display properly if min is the same as max
            ui->y1Min->setValue(m_minMeasurement * 0.99);
            ui->y1Max->setValue(m_maxMeasurement * 1.01);
        }
        else
        {
            if (!std::isnan(m_minMeasurement) && (m_minMeasurement != m_settings.m_y1Min)) {
                ui->y1Min->setValue(m_minMeasurement);
            }
            if (!std::isnan(m_maxMeasurement) && (m_maxMeasurement != m_settings.m_y1Max)) {
                ui->y1Max->setValue(m_maxMeasurement);
            }
        }
    }
}

void SIDGUI::xRayDataUpdated(const QList<GOESXRay::XRayData>& data, bool primary)
{
    // Data is at 1-minute intervals, for last 6 hours, so we want to merge with data with already have
    // Assuems oldest data is first in the array
    QDateTime start;
    int idx = primary ? 0 : 1;
    if (m_xrayShortMeasurements[idx].m_measurements.size() > 0) {
        start = m_xrayShortMeasurements[idx].m_measurements.last().m_dateTime;
    }

    for (const auto& measurement : data)
    {
        if (!start.isValid() || (measurement.m_dateTime > start))
        {
            ChannelMeasurement* measurements;

            switch (measurement.m_band)
            {
            case GOESXRay::XRayData::SHORT:
                measurements = &m_xrayShortMeasurements[idx];
                break;
            case GOESXRay::XRayData::LONG:
                measurements = &m_xrayLongMeasurements[idx];
                break;
            default:
                measurements = nullptr;
                break;
            }
            // Ignore flux measurements of 0, as log10(0) is -Inf
            if (measurements && (measurement.m_flux != 0.0))
            {
                double logFlux = log10(measurement.m_flux);
                measurements->append(measurement.m_dateTime, logFlux);
            }
        }
    }

    plotChart();
}

void SIDGUI::protonDataUpdated(const QList<GOESXRay::ProtonData>& data, bool primary)
{
    (void) primary;

    QDateTime start;

    if (m_protonMeasurements[0].m_measurements.size() > 0) {
        start = m_protonMeasurements[0].m_measurements.last().m_dateTime;
    }
    for (const auto& measurement : data)
    {
        if (!start.isValid() || (measurement.m_dateTime > start))
        {
            ChannelMeasurement* measurements = nullptr;

            switch (measurement.m_energy)
            {
            case 10:
                measurements = &m_protonMeasurements[0];
                break;
            case 50:
                measurements = &m_protonMeasurements[1];
                break;
            case 100:
                measurements = &m_protonMeasurements[2];
                break;
            case 500:
                measurements = &m_protonMeasurements[3];
                break;
            }

            if (measurements) {
                measurements->append(measurement.m_dateTime, measurement.m_flux);
            }
        }
    }

    plotChart();
}

void SIDGUI::stixDataUpdated(const QList<STIX::FlareData>& data)
{
    m_stixData = data;
    plotChart();
}

void SIDGUI::grbDataUpdated(const QList<GRB::Data>& data)
{
    m_grbData = data;

    // Calculate min/max of data
    if (m_grbData.size() > 0)
    {
        m_grbMin = std::numeric_limits<float>::max();
        m_grbMax = std::numeric_limits<float>::min();
        for (int i = 0; i < m_grbData.size(); i++)
        {
            if ((m_grbData[i].m_fluence != 0.0f) && (m_grbData[i].m_fluence != -999.0f))
            {
                m_grbMin = std::min(m_grbMin, m_grbData[i].m_fluence);
                m_grbMax = std::max(m_grbMax, m_grbData[i].m_fluence);
            }
        }
    }

    plotChart();
}

void SIDGUI::sdoImageUpdated(const QImage& image)
{
    bool setSize = ui->sdoImage->pixmap(Qt::ReturnByValueConstant()).isNull();

    QPixmap pixmap;
    pixmap.convertFromImage(image);
    ui->sdoImage->setPixmap(pixmap);

    if (setSize)
    {
        QList<int> sizes = ui->sdoSplitter->sizes();
        if (!((sizes[0] == 0) && (sizes[1] == 0)))
        {
            sizes[1] = std::max(sizes[1], 256); // Default size can be a bit small
            ui->sdoSplitter->setSizes(sizes);
        }
    }
}

void SIDGUI::on_sdoEnabled_toggled(bool checked)
{
    m_settings.m_sdoEnabled = checked;
    ui->sdoData->setVisible(checked);
    ui->sdoVideoEnabled->setVisible(checked);
    ui->sdoContainer->setVisible(checked);
    ui->sdoNow->setVisible(checked);
    ui->sdoDateTime->setVisible(checked);
    applySetting("sdoEnabled");
    applySDO();
}

void SIDGUI::on_sdoVideoEnabled_toggled(bool checked)
{
    m_settings.m_sdoVideoEnabled = checked;
    applySetting("sdoVideoEnabled");

    QString currentText = ui->sdoData->currentText();
    ui->sdoData->blockSignals(true);
    ui->sdoData->clear();
    if (checked)
    {
        for (const auto& name : SolarDynamicsObservatory::getVideoNames()) {
            ui->sdoData->addItem(name);
        }
    }
    else
    {
        for (const auto& name : SolarDynamicsObservatory::getImageNames()) {
            ui->sdoData->addItem(name);
        }
    }
    ui->sdoData->blockSignals(false);
    int idx = ui->sdoData->findText(currentText);
    if (idx != -1) {
        ui->sdoData->setCurrentIndex(idx);
    } else {
        ui->sdoData->setCurrentIndex(0);
    }

    applySDO();
}

void SIDGUI::on_sdoNow_toggled(bool checked)
{
    m_settings.m_sdoNow = checked;
    applySetting("sdoNow");
    ui->sdoDateTime->setEnabled(!m_settings.m_sdoNow);
    ui->mapLabel->setEnabled(!m_settings.m_sdoNow);
    ui->map->setEnabled(!m_settings.m_sdoNow);
    applySDO();
    applyDateTime();
}

void SIDGUI::on_sdoData_currentIndexChanged(int index)
{
    (void) index;

    m_settings.m_sdoData = ui->sdoData->currentText();
    applySetting("sdoData");
    applySDO();
}

void SIDGUI::on_sdoDateTime_dateTimeChanged(QDateTime value)
{
    m_settings.m_sdoDateTime = value;
    applySetting("sdoDateTime");
    if (!m_settings.m_sdoNow)
    {
        applySDO();
        applyDateTime();
    }
}

void SIDGUI::applySDO()
{
    if (m_solarDynamicsObservatory)
    {
        ui->sdoImage->setVisible(!m_settings.m_sdoVideoEnabled);
        ui->sdoVideo->setVisible(m_settings.m_sdoVideoEnabled);
        if (m_player) {
            m_player->stop();
        }
        if (m_settings.m_sdoVideoEnabled)
        {
            QString videoURL = SolarDynamicsObservatory::getVideoURL(m_settings.m_sdoData);
            if (!videoURL.isEmpty() && m_player)
            {
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
                m_player->setMedia(QUrl(videoURL));
#else
                m_player->setSource(QUrl(videoURL));
#endif
                m_player->play();
            }
            // Stop image updates
            m_solarDynamicsObservatory->getImagePeriodically(m_settings.m_sdoData, 512, 0);
        }
        else
        {
            if (m_settings.m_sdoNow) {
                m_solarDynamicsObservatory->getImagePeriodically(m_settings.m_sdoData);
            } else {
                m_solarDynamicsObservatory->getImage(m_settings.m_sdoData, m_settings.m_sdoDateTime);
            }
        }
    }
}

#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
// This doesn't seem to get called on Qt5 on Windows
void SIDGUI::sdoBufferStatusChanged(int percentFilled)
{
    ui->sdoProgressBar->setValue(percentFilled);
}
#else
void SIDGUI::sdoBufferProgressChanged(float filled)
{
    ui->sdoProgressBar->setValue((int)std::round(filled * 100.0f));
}
#endif

void SIDGUI::sdoVideoError(QMediaPlayer::Error error)
{
    qWarning() << "SIDGUI::sdoVideoError: " << error << m_player->errorString();
#ifdef _MSC_VER
    // Qt5/Windows doesn't support mp4 by default, so suggest K-Lite codecs
    // Qt6 doesn't need these
    if (error == QMediaPlayer::FormatError) {
        QMessageBox::warning(this, "Video Error", "Unable to play video. Please try installing mp4/h264 codec, such as: <a href='https://www.codecguide.com/download_k-lite_codec_pack_basic.htm'>K-Lite codedcs</a>.");
    }
#elif LINUX
    if (error == QMediaPlayer::FormatError) {
        QMessageBox::warning(this, "Video Error", "Unable to play video. Please try installing mp4/h264 codec, such as gstreamer libav.");
    }
#else
    if (error == QMediaPlayer::FormatError) {
        QMessageBox::warning(this, "Video Error", "Unable to play video. Please try installing an mp4/h264 codec.");
    }
#endif
}

void SIDGUI::sdoVideoStatusChanged(QMediaPlayer::MediaStatus status)
{
    if (status == QMediaPlayer::LoadingMedia)
    {
        ui->sdoProgressBar->setValue(0);
        ui->sdoProgressBar->setVisible(true);
    }
    else if (status == QMediaPlayer::BufferedMedia)
    {
        ui->sdoProgressBar->setValue(100);
        ui->sdoProgressBar->setVisible(false);
    }
    else if (status == QMediaPlayer::EndOfMedia)
    {
        m_player->setPosition(0);
        m_player->play();
    }
}

void SIDGUI::applyDateTime()
{
    if (!m_settings.m_map.isEmpty() && (m_settings.m_map != "None"))
    {
        if (m_settings.m_sdoNow) {
            FeatureWebAPIUtils::mapSetDateTime(QDateTime::currentDateTime());
        } else {
            FeatureWebAPIUtils::mapSetDateTime(m_settings.m_sdoDateTime);
        }
    }
}

void SIDGUI::on_showSats_clicked()
{
    // Create a Satellite Tracker feature
    MainCore *mainCore = MainCore::instance();
    PluginAPI::FeatureRegistrations *featureRegistrations = mainCore->getPluginManager()->getFeatureRegistrations();
    int nbRegistrations = featureRegistrations->size();
    int index = 0;

    for (; index < nbRegistrations; index++)
    {
        if (featureRegistrations->at(index).m_featureId == "SatelliteTracker") {
            break;
        }
    }

    if (index < nbRegistrations)
    {
        connect(mainCore, &MainCore::featureAdded, this, &SIDGUI::onSatTrackerAdded);

        MainCore::MsgAddFeature *msg = MainCore::MsgAddFeature::create(0, index);
        mainCore->getMainMessageQueue()->push(msg);
    }
    else
    {
        QMessageBox::warning(this, "Error", "Satellite Tracker feature not available");
    }
}

void SIDGUI::onSatTrackerAdded(int featureSetIndex, Feature *feature)
{
    if (feature->getURI() == "sdrangel.feature.satellitetracker")
    {
        disconnect(MainCore::instance(), &MainCore::featureAdded, this, &SIDGUI::onSatTrackerAdded);

        QJsonArray sats = {"SDO", "GOES 16", "GOES-18"};

        ChannelWebAPIUtils::patchFeatureSetting(featureSetIndex, feature->getIndexInFeatureSet(), "satellites", sats);

        ChannelWebAPIUtils::patchFeatureSetting(featureSetIndex, feature->getIndexInFeatureSet(), "target", "SDO");

        ChannelWebAPIUtils::runFeature(featureSetIndex, feature->getIndexInFeatureSet());
    }
}

void SIDGUI::on_map_currentTextChanged(const QString& text)
{
    m_settings.m_map = text;
    applySetting("map");
    applyDateTime();
}

// Plot paths from transmitters to receivers on map
void SIDGUI::on_showPaths_clicked()
{
    clearFromMap();

    for (int i = 0; i < m_settings.m_channelSettings.size(); i++)
    {
        unsigned int deviceSetIndex;
        unsigned int channelIndex;

        if (MainCore::getDeviceAndChannelIndexFromId(m_settings.m_channelSettings[i].m_id, deviceSetIndex, channelIndex))
        {
            // Get position of device, defaulting to My Position
            QGeoCoordinate rxPosition;
            float latitude, longitude, altitude;

            if (ChannelWebAPIUtils::getDevicePosition(deviceSetIndex, latitude, longitude, altitude))
            {
                rxPosition.setLatitude(latitude);
                rxPosition.setLongitude(longitude);
                rxPosition.setAltitude(altitude);
            }
            else
            {
                rxPosition.setLatitude(MainCore::instance()->getSettings().getLatitude());
                rxPosition.setLongitude(MainCore::instance()->getSettings().getLongitude());
                rxPosition.setAltitude(MainCore::instance()->getSettings().getAltitude());
            }

            // Get position of transmitter
            if (VLFTransmitters::m_callsignHash.contains(m_settings.m_channelSettings[i].m_label))
            {
                const VLFTransmitters::Transmitter *transmitter = VLFTransmitters::m_callsignHash.value(m_settings.m_channelSettings[i].m_label);
                QGeoCoordinate txPosition;
                txPosition.setLatitude(transmitter->m_latitude);
                txPosition.setLongitude(transmitter->m_longitude);
                txPosition.setAltitude(0);

                // Calculate mid point for position of label
                qreal distance = txPosition.distanceTo(rxPosition);
                qreal az = txPosition.azimuthTo(rxPosition);
                QGeoCoordinate midPoint = txPosition.atDistanceAndAzimuth(distance / 2.0, az);

                // Create a path from transmitter to receiver
                QList<ObjectPipe*> mapPipes;
                MainCore::instance()->getMessagePipes().getMessagePipes(m_sid, "mapitems", mapPipes);
                if (mapPipes.size() > 0)
                {
                    for (const auto& pipe : mapPipes)
                    {
                        MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
                        SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();

                        QString deviceId = QString("%1%2").arg(m_settings.m_channelSettings[i].m_id[0]).arg(deviceSetIndex);

                        QString name = QString("SID %1 to %2").arg(m_settings.m_channelSettings[i].m_label).arg(deviceId);
                        QString details = QString("%1<br>Distance: %2 km").arg(name).arg((int) std::round(distance / 1000.0));

                        swgMapItem->setName(new QString(name));
                        swgMapItem->setLatitude(midPoint.latitude());
                        swgMapItem->setLongitude(midPoint.longitude());
                        swgMapItem->setAltitude(midPoint.altitude());
                        QString image = QString("none");
                        swgMapItem->setImage(new QString(image));
                        swgMapItem->setImageRotation(0);
                        swgMapItem->setText(new QString(details));   // Not used - label is used instead for now
                        swgMapItem->setFixedPosition(true);
                        swgMapItem->setLabel(new QString(details));
                        swgMapItem->setAltitudeReference(0);
                        QList<SWGSDRangel::SWGMapCoordinate *> *coords = new QList<SWGSDRangel::SWGMapCoordinate *>();

                        SWGSDRangel::SWGMapCoordinate* c = new SWGSDRangel::SWGMapCoordinate();
                        c->setLatitude(rxPosition.latitude());
                        c->setLongitude(rxPosition.longitude());
                        c->setAltitude(rxPosition.altitude());
                        coords->append(c);

                        c = new SWGSDRangel::SWGMapCoordinate();
                        c->setLatitude(txPosition.latitude());
                        c->setLongitude(txPosition.longitude());
                        c->setAltitude(txPosition.altitude());
                        coords->append(c);

                        swgMapItem->setColorValid(1);
                        swgMapItem->setColor(m_settings.m_channelSettings[i].m_color.rgba());

                        swgMapItem->setCoordinates(coords);
                        swgMapItem->setType(3);

                        MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_sid, swgMapItem);
                        messageQueue->push(msg);

                        m_mapItemNames.append(name);
                    }
                }
            }
        }
    }
}

void SIDGUI::clearFromMap()
{
    QList<ObjectPipe*> mapPipes;
    MainCore::instance()->getMessagePipes().getMessagePipes(m_sid, "mapitems", mapPipes);

    for (const auto& name : m_mapItemNames)
    {
        for (const auto& pipe : mapPipes)
        {
            MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
            SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
            swgMapItem->setName(new QString(name));
            swgMapItem->setImage(new QString(""));
            swgMapItem->setType(3);
            MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_sid, swgMapItem);
            messageQueue->push(msg);
        }
    }
}

void SIDGUI::featuresChanged(const QStringList& renameFrom, const QStringList& renameTo)
{
    const AvailableChannelOrFeatureList availableFeatures = m_availableFeatureHandler.getAvailableChannelOrFeatureList();

    if (renameFrom.contains(m_settings.m_map))
    {
        m_settings.m_map = renameTo[renameFrom.indexOf(m_settings.m_map)];
        applySetting("map");
    }

    ui->map->blockSignals(true);
    ui->map->clear();
    ui->map->addItem("None");
    for (const auto& map : availableFeatures) {
        ui->map->addItem(map.getId());
    }

    int idx = ui->map->findText(m_settings.m_map);
    if (idx >= 0) {
        ui->map->setCurrentIndex(idx);
    } else {
        ui->map->setCurrentIndex(-1);
    }

    ui->map->blockSignals(false);

    // If no setting, default to first available map
    if (m_settings.m_map.isEmpty() && (ui->map->count() >= 2)) {
        ui->map->setCurrentIndex(1);
    }
}

void SIDGUI::channelsChanged(const QStringList& renameFrom, const QStringList& renameTo, const QStringList& removed, const QStringList& added)
{
    removeChannels(removed);

    // Rename measurements and settings that have had their id changed
    for (int i = 0; i < renameFrom.size(); i++)
    {
        for (int j = 0; j < m_channelMeasurements.size(); j++)
        {
            if (m_channelMeasurements[j].m_id == renameFrom[i]) {
                m_channelMeasurements[j].m_id = renameTo[i];
            }
        }
        for (int j = 0; j < m_settings.m_channelSettings.size(); j++)
        {
            if (m_settings.m_channelSettings[j].m_id == renameFrom[i]) {
                m_settings.m_channelSettings[j].m_id = renameTo[i];
            }
        }
    }

    // Create settings for any new channels
    // Don't call createChannelSettings when channels are removed, as ids might not have been updated yet
    if (added.size() > 0)
    {
        if (m_settings.createChannelSettings()) {
            applySetting("channelSettings");
        }
    }
}

void SIDGUI::removeChannels(const QStringList& ids)
{
    for (int i = 0; i < ids.size(); i++)
    {
        for (int j = 0; j < m_channelMeasurements.size(); j++)
        {
            if (ids[i] == m_channelMeasurements[j].m_id)
            {
                m_channelMeasurements.removeAt(j);
                break;
            }
        }

        for (int j = 0; j < m_settings.m_channelSettings.size(); j++)
        {
            if (ids[i] == m_settings.m_channelSettings[j].m_id)
            {
                m_settings.m_channelSettings.removeAt(j);
                break;
            }
        }
    }
}

void SIDGUI::autosave()
{
    qDebug() << "SIDGUI::autosave start";
    writeCSV(m_settings.m_filename);
    qDebug() << "SIDGUI::autosave done";
}

void SIDGUI::on_saveData_clicked()
{
    m_fileDialog.setDefaultSuffix("csv");
    m_fileDialog.setAcceptMode(QFileDialog::AcceptSave);
    if (m_fileDialog.exec())
    {
        QStringList fileNames = m_fileDialog.selectedFiles();
        if (fileNames.size() > 0) {
            writeCSV(fileNames[0]);
        }
    }
}

void SIDGUI::on_loadData_clicked()
{
    m_fileDialog.setAcceptMode(QFileDialog::AcceptOpen);
    if (m_fileDialog.exec())
    {
        QStringList fileNames = m_fileDialog.selectedFiles();
        if (fileNames.size() > 0) {
            readCSV(fileNames[0], false);
        }
    }
}

void SIDGUI::writeCSV(const QString& filename)
{
    if (m_channelMeasurements.size() < 1) {
        return;
    }

    QFile file(filename);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        QMessageBox::critical(this, "SID", QString("Failed to open file %1").arg(filename));
        return;
    }
    QTextStream out(&file);

    // Create a CSV file from the values in the table
    QList<int> idx;
    QList<ChannelMeasurement *> measurements;
    out << "Date and Time,";
    for (int i = 0; i < m_channelMeasurements.size(); i++)
    {
        SIDSettings::ChannelSettings *channelSettings = m_settings.getChannelSettings(m_channelMeasurements[i].m_id);
        QString name = m_channelMeasurements[i].m_id;
        if (channelSettings)
        {
            name.append("-");
            name.append(channelSettings->m_label);
        }
        out <<  name << ",";
        measurements.append(&m_channelMeasurements[i]);
        idx.append(0);
    }

    out << "X-Ray Primary Short,";
    measurements.append(&m_xrayShortMeasurements[0]);
    idx.append(0);
    out << "X-Ray Primary Long,";
    measurements.append(&m_xrayLongMeasurements[0]);
    idx.append(0);
    out << "X-Ray Secondary Short,";
    measurements.append(&m_xrayShortMeasurements[1]);
    idx.append(0);
    out << "X-Ray Secondary Long,";
    measurements.append(&m_xrayLongMeasurements[1]);
    idx.append(0);

    for (int i = 0; i < 4; i++)
    {
        out << QString("%1 Proton,").arg(SIDGUI::m_protonEnergies[i]);
        measurements.append(&m_protonMeasurements[i]);
        idx.append(0);
    }

    out << "\n";

    // Find earliest time
    QDateTime t;
    for (int i = 0; i < measurements.size(); i++)
    {
        ChannelMeasurement *cm = measurements[i];
        Measurement *m = &cm->m_measurements[idx[i]];
        if (!t.isValid() || (m->m_dateTime < t)) {
            t = m->m_dateTime;
        }
    }

    bool done = false;
    while (!done)
    {
        out << t.toUTC().toString(Qt::ISODateWithMs);
        out << ",";

        // Output data at this time
        for (int i = 0; i < measurements.size(); i++)
        {
            ChannelMeasurement *cm = measurements[i];
            if (cm->m_measurements.size() > idx[i])
            {
                Measurement *m = &cm->m_measurements[idx[i]];
                if (m->m_dateTime == t)
                {
                    out << m->m_measurement;
                    idx[i]++;
                }
            }
            out << ",";
        }
        out << "\n";

        // Find next time
        t = QDateTime();
        for (int i = 0; i < measurements.size(); i++)
        {
            ChannelMeasurement *cm = measurements[i];
            if (cm->m_measurements.size() > idx[i])
            {
                Measurement *m = &cm->m_measurements[idx[i]];
                if (!t.isValid() || (m->m_dateTime < t)) {
                    t = m->m_dateTime;
                }
            }
        }
        if (!t.isValid()) {
            done = true;
        }
    }
}

void SIDGUI::readCSV(const QString& filename, bool autoload)
{
    QFile file(filename);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        if (!autoload) {
            QMessageBox::critical(this, "SID", QString("Failed to open file %1").arg(filename));
        }
        return;
    }
    QTextStream in(&file);

    // Prevent data updates while reading CSV
    disconnectDataUpdates();

    // Delete existing data

    clearAllData();

    // Get list of colors to use
    QList<QRgb> colors = SIDSettings::m_defaultColors;
    for (const auto& channelSettings : m_settings.m_channelSettings) {
        colors.removeAll(channelSettings.m_color.rgb());
    }

    bool channelSettingsChanged = false;
    QStringList colNames;
    if (CSV::readRow(in, &colNames))
    {
        QList<ChannelMeasurement *> measurements;
        QList<int> measurementIdx;
        for (int i = 0; i < colNames.size() - 1; i++)
        {
            measurements.append(nullptr);
            measurementIdx.append(-1);
        }
        for (int i = 1; i < colNames.size(); i++)
        {
            QString name = colNames[i];
            if (name == "X-Ray Primary Short")
            {
                measurements[i-1] = &m_xrayShortMeasurements[0];
            }
            else if (name == "X-Ray Primary Long")
            {
                measurements[i-1] = &m_xrayLongMeasurements[0];
            }
            else if (name == "X-Ray Secondary Short")
            {
                measurements[i-1] = &m_xrayShortMeasurements[1];
            }
            else if (name == "X-Ray Secondary Long")
            {
                measurements[i-1] = &m_xrayLongMeasurements[1];
            }
            else if (name.endsWith("Proton"))
            {
                for (int j = 0; j < m_protonEnergies.size(); j++)
                {
                    if (name.startsWith(m_protonEnergies[j]))
                    {
                        measurements[i-1] = &m_protonMeasurements[j];
                        break;
                    }
                }
            }
            else if (name.contains(":"))
            {
                QString id;

                int idx = name.indexOf('-');
                if (idx >= 0) {
                    id = name.left(idx);
                } else {
                    id = name;
                }
                addMeasurements(id);
                measurementIdx[i-1] = m_channelMeasurements.size() - 1;

                // Create settings, if we don't have them
                SIDSettings::ChannelSettings *channelSettings = m_settings.getChannelSettings(id);
                if (!channelSettings)
                {
                    if (colors.size() == 0) {
                        colors = SIDSettings::m_defaultColors;
                    }

                    SIDSettings::ChannelSettings newSettings;
                    newSettings.m_id = id;
                    newSettings.m_enabled = true;
                    newSettings.m_label = name.mid(idx + 1);
                    newSettings.m_color = colors.takeFirst();
                    m_settings.m_channelSettings.append(newSettings);
                    channelSettingsChanged = true;
                }
            }
        }

        QMessageBox dialog(this);
        dialog.setText("Reading data");
        dialog.addButton(QMessageBox::Cancel);
        dialog.show();
        QApplication::processEvents();

        bool cancelled = false;
        QStringList cols;
        int row = 1;

        while(!cancelled && CSV::readRow(in, &cols))
        {
            if (cols.size() == measurements.size() + 1)
            {
                QDateTime dateTime = QDateTime::fromString(cols[0], Qt::ISODateWithMs);

                for (int i = 0; i < measurements.size(); i++)
                {
                    QString valueStr = cols[i+1];
                    if (!valueStr.isEmpty())
                    {
                        double value = valueStr.toDouble();
                        if (measurements[i]) {
                            measurements[i]->append(dateTime, value, false);
                        } else {
                            m_channelMeasurements[measurementIdx[i]].append(dateTime, value, false);
                        }
                    }
                }
            }
            else
            {
                qDebug() << "SIDGUI::readCSV: Not enough data on row " << row;
            }
            if (row % 10000 == 0)
            {
                QApplication::processEvents();
                if (dialog.clickedButton()) {
                    cancelled = true;
                }
            }
            row++;
        }

        dialog.close();

        autoscaleX();
        autoscaleY();
        plotChart();
        connectDataUpdates();
        getData();
        if (channelSettingsChanged) {
            applySetting("channelSettings");
        }
    }
}

void SIDGUI::on_saveChartImage_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->chart->size(), QImage::Format_ARGB32);
            image.fill(Qt::transparent);
            QPainter painter(&image);
            ui->chart->render(&painter);
            if (!image.save(fileNames[0])) {
                QMessageBox::critical(this, "SID", QString("Failed to save image to %1").arg(fileNames[0]));
            }
        }
    }
}
