1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-12-23 10:05:46 -05:00
sdrangel/plugins/feature/sid/sidgui.cpp
srcejon 24d80f8ab0 ADS-B: Allow plugin to be compiled as static lib. Allow building without texttospeech, location or process.
VOR localizer: Allow plugin to be compiled as static lib.
SID feature: Allow plugin to be compiled as static lib.
Satellite Tracker: Allow building without texttospeech or timezone.
Map: Allow plugin to be compiled as static lib. Allow building without SSL.
APT: Allow plugin to be compiled as static lib.
Radio Astronomy: Allow plugin to be compiled as static lib. Allow building without process.
ChannelWebAPIUtils: Fix memory leaks.
DeviceSampleSource: Add signals that indicate when the position or direction of a device (or it's antenna) has changed. This is to support per device position, which is useful for remote devices not at My Position.
2024-09-17 09:47:45 +01:00

2535 lines
82 KiB
C++

///////////////////////////////////////////////////////////////////////////////////
// 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.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;
for (int i = 0; i < colNames.size() - 1; i++) {
measurements.append(nullptr);
}
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;
}
measurements[i-1] = &addMeasurements(id);
// 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();
measurements[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]));
}
}
}
}