1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-11-29 19:28:47 -05:00
sdrangel/plugins/feature/satellitetracker/satellitetrackergui.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

1724 lines
61 KiB
C++

///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2021-2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2021 DreamNik <dreamnik@mail.ru> //
// //
// 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 <cmath>
#include <QMessageBox>
#include <QLineEdit>
#include <QtCharts/QChartView>
#include <QtCharts/QLineSeries>
#include <QtCharts/QDateTimeAxis>
#include <QtCharts/QValueAxis>
#include "channel/channelwebapiutils.h"
#include "feature/featureset.h"
#include "feature/featureuiset.h"
#include "feature/featureutils.h"
#include "feature/featurewebapiutils.h"
#include "gui/basicfeaturesettingsdialog.h"
#include "gui/dialogpositioner.h"
#include "util/units.h"
#include "device/deviceapi.h"
#include "device/deviceset.h"
#include "maincore.h"
#include "ui_satellitetrackergui.h"
#include "satellitetracker.h"
#include "satellitetrackergui.h"
#include "satellitetrackerreport.h"
#include "satellitetrackersettingsdialog.h"
#include "satelliteselectiondialog.h"
#include "satelliteradiocontroldialog.h"
#include "satellitetrackersgp4.h"
SatelliteTrackerGUI* SatelliteTrackerGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature)
{
SatelliteTrackerGUI* gui = new SatelliteTrackerGUI(pluginAPI, featureUISet, feature);
return gui;
}
void SatelliteTrackerGUI::destroy()
{
delete this;
}
void SatelliteTrackerGUI::resetToDefaults()
{
m_settings.resetToDefaults();
displaySettings();
applySettings(true);
}
QByteArray SatelliteTrackerGUI::serialize() const
{
return m_settings.serialize();
}
bool SatelliteTrackerGUI::deserialize(const QByteArray& data)
{
if (m_settings.deserialize(data))
{
m_feature->setWorkspaceIndex(m_settings.m_workspaceIndex);
updateSelectedSats();
displaySettings();
applySettings(true);
return true;
}
else
{
resetToDefaults();
return false;
}
}
QString SatelliteTrackerGUI::convertDegreesToText(double degrees)
{
if (m_settings.m_azElUnits == SatelliteTrackerSettings::DMS)
return Units::decimalDegreesToDegreeMinutesAndSeconds(degrees);
else if (m_settings.m_azElUnits == SatelliteTrackerSettings::DM)
return Units::decimalDegreesToDegreesAndMinutes(degrees);
else if (m_settings.m_azElUnits == SatelliteTrackerSettings::D)
return Units::decimalDegreesToDegrees(degrees);
else
return QString("%1").arg(degrees, 0, 'f', 2);
}
bool SatelliteTrackerGUI::handleMessage(const Message& message)
{
if (SatelliteTracker::MsgConfigureSatelliteTracker::match(message))
{
qDebug("SatelliteTrackerGUI::handleMessage: SatelliteTracker::MsgConfigureSatelliteTracker");
const SatelliteTracker::MsgConfigureSatelliteTracker& cfg = (SatelliteTracker::MsgConfigureSatelliteTracker&) 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 (SatelliteTrackerReport::MsgReportSat::match(message))
{
SatelliteTrackerReport::MsgReportSat& satReport = (SatelliteTrackerReport::MsgReportSat&) message;
SatelliteState *satState = satReport.getSatelliteState();
if (satState->m_name == m_settings.m_target)
{
delete m_targetSatState;
m_targetSatState = satState;
ui->azimuth->setText(convertDegreesToText(satState->m_azimuth));
ui->elevation->setText(convertDegreesToText(satState->m_elevation));
plotChart();
if (satState->m_passes.size() > 0)
{
const SatellitePass &pass = satState->m_passes[0];
bool geostationary = !pass.m_aos.isValid() && !pass.m_los.isValid();
if ((m_nextTargetAOS != pass.m_aos) || (m_nextTargetLOS != pass.m_los) || (geostationary != m_geostationarySatVisible))
{
m_nextTargetAOS = pass.m_aos;
m_nextTargetLOS = pass.m_los;
m_geostationarySatVisible = geostationary;
updateTimeToAOS();
}
}
}
updateTable(satState);
if (satState->m_name != m_settings.m_target) {
delete satState;
}
return true;
}
else if (SatelliteTrackerReport::MsgReportAOS::match(message))
{
SatelliteTrackerReport::MsgReportAOS& aosReport = (SatelliteTrackerReport::MsgReportAOS&) message;
aos(aosReport.getSpeech());
return true;
}
else if (SatelliteTrackerReport::MsgReportTarget::match(message))
{
SatelliteTrackerReport::MsgReportTarget& targetReport = (SatelliteTrackerReport::MsgReportTarget&) message;
setTarget(targetReport.getName());
return true;
}
else if (SatelliteTrackerReport::MsgReportLOS::match(message))
{
SatelliteTrackerReport::MsgReportLOS& losReport = (SatelliteTrackerReport::MsgReportLOS&) message;
los(losReport.getSpeech());
return true;
}
else if (SatelliteTracker::MsgSatData::match(message))
{
SatelliteTracker::MsgSatData& satData = (SatelliteTracker::MsgSatData&) message;
m_satellites = satData.getSatellites();
// Remove satellites that no longer exist
QMutableListIterator<QString> itr(m_settings.m_satellites);
while (itr.hasNext())
{
QString satellite = itr.next();
if (!m_satellites.contains(satellite))
itr.remove();
}
if (!m_satellites.contains(m_settings.m_target)) {
setTarget("");
}
// Update GUI
updateSelectedSats();
return true;
}
else if (SatelliteTracker::MsgError::match(message))
{
SatelliteTracker::MsgError& errorMsg = (SatelliteTracker::MsgError&) message;
QString error = errorMsg.getError();
QMessageBox::critical(this, "Satellite Tracker", error);
return true;
}
return false;
}
// Call when m_settings.m_satellites changes
void SatelliteTrackerGUI::updateSelectedSats()
{
// Remove unselects sats from target combo and table
for (int i = 0; i < ui->target->count(); )
{
QString name = ui->target->itemText(i);
int idx = m_settings.m_satellites.indexOf(name);
if (idx == -1)
{
ui->target->removeItem(i);
QList<QTableWidgetItem *> matches = ui->satTable->findItems(name, Qt::MatchExactly);
for (int j = 0; j < matches.length(); j++) {
ui->satTable->removeRow(ui->satTable->row(matches[j]));
}
}
else
{
i++;
}
}
// Add new satellites to target combo
for (int i = 0; i < m_settings.m_satellites.size(); i++)
{
if (ui->target->findText(m_settings.m_satellites[i], Qt::MatchExactly) == -1) {
ui->target->addItem(m_settings.m_satellites[i]);
}
}
// Select current target, if it still exists
int idx = ui->target->findText(m_settings.m_target);
if (idx != -1) {
ui->target->setCurrentIndex(idx);
} else {
setTarget("");
}
}
void SatelliteTrackerGUI::handleInputMessages()
{
Message* message;
while ((message = getInputMessageQueue()->pop()))
{
if (handleMessage(*message)) {
delete message;
}
}
}
void SatelliteTrackerGUI::onWidgetRolled(QWidget* widget, bool rollDown)
{
(void) widget;
(void) rollDown;
RollupContents *rollupContents = getRollupContents();
rollupContents->saveState(m_rollupState);
}
SatelliteTrackerGUI::SatelliteTrackerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) :
FeatureGUI(parent),
ui(new Ui::SatelliteTrackerGUI),
m_pluginAPI(pluginAPI),
m_featureUISet(featureUISet),
m_doApplySettings(true),
m_lastFeatureState(0),
m_lastUpdatingSatData(false),
m_targetSatState(nullptr),
m_plotPass(0),
m_lineChart(nullptr),
m_polarChart(nullptr),
m_geostationarySatVisible(false)
{
m_feature = feature;
setAttribute(Qt::WA_DeleteOnClose, true);
m_helpURL = "plugins/feature/satellitetracker/readme.md";
RollupContents *rollupContents = getRollupContents();
ui->setupUi(rollupContents);
rollupContents->arrangeRollups();
connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));
m_satelliteTracker = reinterpret_cast<SatelliteTracker*>(feature);
m_satelliteTracker->setMessageQueueToGUI(&m_inputMessageQueue);
m_settings.setRollupState(&m_rollupState);
connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &)));
connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
connect(&m_statusTimer, SIGNAL(timeout()), this, SLOT(updateStatus()));
m_statusTimer.start(1000);
connect(&m_redrawTimer, &QTimer::timeout, this, &SatelliteTrackerGUI::plotChart);
// Initialise charts
m_emptyChart.layout()->setContentsMargins(0, 0, 0, 0);
m_emptyChart.setMargins(QMargins(1, 1, 1, 1));
ui->passChart->setChart(&m_emptyChart);
ui->passChart->setRenderHint(QPainter::Antialiasing);
ui->dateTime->setDateTime(m_satelliteTracker->currentDateTime());
ui->deviceFeatureSelect->setVisible(false);
resizeTable();
// Allow user to reorder columns
ui->satTable->horizontalHeader()->setSectionsMovable(true);
// Allow user to sort table by clicking on headers
ui->satTable->setSortingEnabled(true);
// Add context menu to allow hiding/showing of columns
menu = new QMenu(ui->satTable);
for (int i = 0; i < ui->satTable->horizontalHeader()->count(); i++)
{
QString text = ui->satTable->horizontalHeaderItem(i)->text();
menu->addAction(createCheckableItem(text, i, true));
}
ui->satTable->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->satTable->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(columnSelectMenu(QPoint)));
// Get signals when columns change
connect(ui->satTable->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(satTable_sectionMoved(int, int, int)));
connect(ui->satTable->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(satTable_sectionResized(int, int, int)));
#ifdef QT_TEXTTOSPEECH_FOUND
m_speech = new QTextToSpeech(this);
#endif
displaySettings();
applySettings(true);
makeUIConnections();
m_resizer.enableChildMouseTracking();
// Get initial list of satellites
on_updateSatData_clicked();
// Use My Position from preferences, if none set
if ((m_settings.m_latitude == 0.0) && (m_settings.m_longitude == 0.0)) {
on_useMyPosition_clicked();
}
}
SatelliteTrackerGUI::~SatelliteTrackerGUI()
{
delete ui;
}
void SatelliteTrackerGUI::setWorkspaceIndex(int index)
{
m_settings.m_workspaceIndex = index;
m_settingsKeys.append("workspaceIndex");
m_feature->setWorkspaceIndex(index);
}
void SatelliteTrackerGUI::blockApplySettings(bool block)
{
m_doApplySettings = !block;
}
void SatelliteTrackerGUI::displaySettings()
{
setTitleColor(m_settings.m_rgbColor);
setWindowTitle(m_settings.m_title);
setTitle(m_settings.m_title);
blockApplySettings(true);
ui->latitude->setValue(m_settings.m_latitude);
ui->longitude->setValue(m_settings.m_longitude);
ui->target->blockSignals(true);
ui->target->clear();
for (const QString& s : m_settings.m_satellites) {
ui->target->addItem(s);
}
ui->target->blockSignals(false);
ui->target->setCurrentIndex(ui->target->findText(m_settings.m_target));
ui->dateTime->setDateTime(QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs));
ui->dateTimeSelect->setCurrentIndex((int)m_settings.m_dateTimeSelect);
ui->dateTime->setVisible(m_settings.m_dateTimeSelect == SatelliteTrackerSettings::CUSTOM);
ui->autoTarget->setChecked(m_settings.m_autoTarget);
ui->darkTheme->setChecked(m_settings.m_chartsDarkTheme);
ui->satTable->horizontalHeader()->setSortIndicator(m_settings.m_columnSort, m_settings.m_columnSortOrder);
getRollupContents()->restoreState(m_rollupState);
plotChart();
blockApplySettings(false);
}
void SatelliteTrackerGUI::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);
m_settingsKeys.append("title");
m_settingsKeys.append("rgbColor");
m_settingsKeys.append("useReverseAPI");
m_settingsKeys.append("reverseAPIAddress");
m_settingsKeys.append("reverseAPIPort");
m_settingsKeys.append("reverseAPIFeatureSetIndex");
m_settingsKeys.append("reverseAPIFeatureIndex");
applySettings();
}
resetContextMenuType();
}
void SatelliteTrackerGUI::aos(const QString &speech)
{
// Call plotChart() to start the periodic updates with sat position in polar chart
plotChart();
// Give speech notification of pass
if (!speech.isEmpty())
{
#ifdef QT_TEXTTOSPEECH_FOUND
m_speech->say(speech);
#else
qWarning() << "SatelliteTrackerGUI::aos: No TextToSpeech: " << speech;
#endif
}
}
void SatelliteTrackerGUI::los(const QString &speech)
{
// Give speech notification of end of pass
if (!speech.isEmpty())
{
#ifdef QT_TEXTTOSPEECH_FOUND
m_speech->say(speech);
#else
qWarning() << "SatelliteTrackerGUI::los: No TextToSpeech: " << speech;
#endif
}
}
void SatelliteTrackerGUI::on_startStop_toggled(bool checked)
{
if (m_doApplySettings)
{
SatelliteTracker::MsgStartStop *message = SatelliteTracker::MsgStartStop::create(checked);
m_satelliteTracker->getInputMessageQueue()->push(message);
}
}
void SatelliteTrackerGUI::on_latitude_valueChanged(double value)
{
m_settings.m_latitude = value;
m_settingsKeys.append("latitude");
applySettings();
plotChart();
}
void SatelliteTrackerGUI::on_longitude_valueChanged(double value)
{
m_settings.m_longitude = value;
m_settingsKeys.append("longitude");
applySettings();
plotChart();
}
void SatelliteTrackerGUI::setTarget(const QString& target)
{
if (target != m_settings.m_target)
{
m_settings.m_target = target;
m_settingsKeys.append("target");
ui->azimuth->setText("");
ui->elevation->setText("");
ui->aos->setText("");
ui->target->setCurrentIndex(ui->target->findText(m_settings.m_target));
m_nextTargetAOS = QDateTime();
m_nextTargetLOS = QDateTime();
m_geostationarySatVisible = false;
applySettings();
delete m_targetSatState;
m_targetSatState = nullptr;
m_plotPass = 0;
ui->passLabel->setText(QString("%1").arg(m_plotPass));
plotChart();
}
}
void SatelliteTrackerGUI::on_target_currentTextChanged(const QString &text)
{
setTarget(text);
}
void SatelliteTrackerGUI::on_useMyPosition_clicked(bool checked)
{
(void) checked;
double stationLatitude = MainCore::instance()->getSettings().getLatitude();
double stationLongitude = MainCore::instance()->getSettings().getLongitude();
double stationAltitude = MainCore::instance()->getSettings().getAltitude();
ui->latitude->setValue(stationLatitude);
ui->longitude->setValue(stationLongitude);
m_settings.m_heightAboveSeaLevel = stationAltitude;
m_settingsKeys.append("heightAboveSeaLevel");
applySettings();
plotChart();
}
// Show settings dialog
void SatelliteTrackerGUI::on_displaySettings_clicked()
{
SatelliteTrackerSettingsDialog dialog(&m_settings);
new DialogPositioner(&dialog, true);
if (dialog.exec() == QDialog::Accepted)
{
m_settingsKeys.append("heightAboveSeaLevel");
m_settingsKeys.append("predictionPeriod");
m_settingsKeys.append("passStartTime");
m_settingsKeys.append("passFinishTime");
m_settingsKeys.append("minAOSElevation");
m_settingsKeys.append("minPassElevation");
m_settingsKeys.append("rotatorMaxAzimuth");
m_settingsKeys.append("rotatorMaxElevation");
m_settingsKeys.append("aosSpeech");
m_settingsKeys.append("losSpeech");
m_settingsKeys.append("aosCommand");
m_settingsKeys.append("losCommand");
m_settingsKeys.append("updatePeriod");
m_settingsKeys.append("dopplerPeriod");
m_settingsKeys.append("defaultFrequency");
m_settingsKeys.append("azElUnits");
m_settingsKeys.append("groundTrackPoints");
m_settingsKeys.append("drawRotators");
m_settingsKeys.append("dateFormat");
m_settingsKeys.append("utc");
m_settingsKeys.append("tles");
m_settingsKeys.append("replayEnabled");
m_settingsKeys.append("replayStartDateTime");
m_settingsKeys.append("sendTimeToMap");
applySettings();
plotChart();
}
}
void SatelliteTrackerGUI::on_dateTimeSelect_currentIndexChanged(int index)
{
m_settings.m_dateTimeSelect = (SatelliteTrackerSettings::DateTimeSelect)index;
if (m_settings.m_dateTimeSelect != SatelliteTrackerSettings::CUSTOM)
{
m_settings.m_dateTime = "";
ui->dateTime->setVisible(false);
}
else
{
m_settings.m_dateTime = ui->dateTime->dateTime().toString(Qt::ISODateWithMs);
ui->dateTime->setVisible(true);
}
ui->deviceFeatureSelect->setVisible(m_settings.m_dateTimeSelect >= SatelliteTrackerSettings::FROM_MAP);
updateDeviceFeatureCombo();
m_settingsKeys.append("dateTimeSelect");
m_settingsKeys.append("dateTime");
applySettings();
plotChart();
}
void SatelliteTrackerGUI::on_dateTime_dateTimeChanged(const QDateTime &datetime)
{
(void) datetime;
if (ui->dateTimeSelect->currentIndex() == 1)
{
m_settings.m_dateTime = ui->dateTime->dateTime().toString(Qt::ISODateWithMs);
m_settingsKeys.append("dateTime");
applySettings();
plotChart();
}
}
// Find target on the Map
void SatelliteTrackerGUI::on_viewOnMap_clicked()
{
if (!m_settings.m_target.isEmpty()) {
FeatureWebAPIUtils::mapFind(m_settings.m_target);
}
}
void SatelliteTrackerGUI::on_updateSatData_clicked()
{
m_satelliteTracker->getInputMessageQueue()->push(SatelliteTracker::MsgUpdateSatData::create());
}
void SatelliteTrackerGUI::on_selectSats_clicked()
{
SatelliteSelectionDialog dialog(&m_settings, m_satellites);
new DialogPositioner(&dialog, true);
if (dialog.exec() == QDialog::Accepted)
{
updateSelectedSats();
m_settingsKeys.append("satellites");
applySettings();
}
}
void SatelliteTrackerGUI::on_radioControl_clicked()
{
SatelliteRadioControlDialog dialog(&m_settings, m_satellites);
new DialogPositioner(&dialog, true);
if (dialog.exec() == QDialog::Accepted)
{
m_settingsKeys.append("deviceSettings");
applySettings();
}
}
void SatelliteTrackerGUI::on_autoTarget_clicked(bool checked)
{
m_settings.m_autoTarget = checked;
m_settingsKeys.append("autoTarget");
applySettings();
}
void SatelliteTrackerGUI::updateStatus()
{
int state = m_satelliteTracker->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::information(this, tr("Message"), m_satelliteTracker->getErrorMessage());
break;
default:
break;
}
m_lastFeatureState = state;
}
// Indicate if satellite data is being updated
bool updatingSatData = m_satelliteTracker->isUpdatingSatData();
if (updatingSatData != m_lastUpdatingSatData)
{
if (updatingSatData) {
ui->updateSatData->setStyleSheet("QToolButton { background-color : green; }");
} else {
ui->updateSatData->setStyleSheet("QToolButton { background: none; }");
}
m_lastUpdatingSatData = updatingSatData;
}
updateTimeToAOS();
updateDeviceFeatureCombo();
}
// Update time to AOS
void SatelliteTrackerGUI::updateTimeToAOS()
{
if (m_geostationarySatVisible)
{
ui->aos->setText("Now");
}
else if (m_nextTargetAOS.isValid())
{
QDateTime currentTime = m_satelliteTracker->currentDateTime(); // FIXME: UTC
int secondsToAOS = m_nextTargetAOS.toSecsSinceEpoch() - currentTime.toSecsSinceEpoch();
if (secondsToAOS > 0)
{
int seconds = secondsToAOS % 60;
int minutes = (secondsToAOS / 60) % 60;
int hours = (secondsToAOS / (60 * 60)) % 24;
int days = secondsToAOS / (60 * 60 * 24);
if (days == 1)
{
ui->aos->setText(QString("1 day"));
}
else if (days > 0)
{
ui->aos->setText(QString("%1 days").arg(days));
}
else
{
ui->aos->setText(QString("%1:%2:%3")
.arg(hours, 2, 10, QLatin1Char('0'))
.arg(minutes, 2, 10, QLatin1Char('0'))
.arg(seconds, 2, 10, QLatin1Char('0')));
}
}
else if (m_nextTargetLOS < currentTime)
{
ui->aos->setText("");
}
else
{
ui->aos->setText("Now");
}
}
else
ui->aos->setText("");
}
void SatelliteTrackerGUI::applySettings(bool force)
{
if (m_doApplySettings)
{
SatelliteTracker::MsgConfigureSatelliteTracker* message = SatelliteTracker::MsgConfigureSatelliteTracker::create(
m_settings, m_settingsKeys, force);
m_satelliteTracker->getInputMessageQueue()->push(message);
}
m_settingsKeys.clear();
}
void SatelliteTrackerGUI::on_nextPass_clicked()
{
if (m_targetSatState != nullptr)
{
if (m_plotPass < m_targetSatState->m_passes.size() - 1)
{
m_plotPass++;
ui->passLabel->setText(QString("%1").arg(m_plotPass));
plotChart();
}
}
}
void SatelliteTrackerGUI::on_prevPass_clicked()
{
if (m_plotPass > 0)
{
m_plotPass--;
ui->passLabel->setText(QString("%1").arg(m_plotPass));
plotChart();
}
}
void SatelliteTrackerGUI::on_darkTheme_clicked(bool checked)
{
m_settings.m_chartsDarkTheme = checked;
plotChart();
m_settingsKeys.append("chartsDarkTheme");
applySettings();
}
void SatelliteTrackerGUI::on_chartSelect_currentIndexChanged(int index)
{
(void) index;
plotChart();
}
void SatelliteTrackerGUI::plotChart()
{
if (ui->chartSelect->currentIndex() == 0) {
plotPolarChart();
} else {
plotAzElChart();
}
}
// Linear interpolation
static double interpolate(double x0, double y0, double x1, double y1, double x)
{
return (y0*(x1-x) + y1*(x-x0)) / (x1-x0);
}
// Reduce az/el range from 450,180 to 360,90
void SatelliteTrackerGUI::limitAzElRange(double& azimuth, double& elevation) const
{
if (elevation > 90.0)
{
elevation = 180.0 - elevation;
if (azimuth < 180.0) {
azimuth += 180.0;
} else {
azimuth -= 180.0;
}
}
if (azimuth > 360.0) {
azimuth -= 360.0f;
}
if (azimuth == 0) {
azimuth = 360.0;
}
}
// Plot pass in polar coords
void SatelliteTrackerGUI::plotPolarChart()
{
if ((m_targetSatState == nullptr) || !m_satellites.contains(m_settings.m_target) || (m_targetSatState->m_passes.size() == 0))
{
ui->passChart->setChart(&m_emptyChart);
return;
}
QChart *oldChart = m_polarChart;
if (m_plotPass >= m_targetSatState->m_passes.size() - 1)
{
m_plotPass = m_targetSatState->m_passes.size() - 1;
ui->passLabel->setText(QString("%1").arg(m_plotPass));
}
const SatellitePass &pass = m_targetSatState->m_passes[m_plotPass];
// Always create a new chart, otherwise sometimes they aren't drawn properly
m_polarChart = new QPolarChart();
m_polarChart->setTheme(m_settings.m_chartsDarkTheme ? QChart::ChartThemeDark : QChart::ChartThemeLight);
QValueAxis *angularAxis = new QValueAxis();
QCategoryAxis *radialAxis = new QCategoryAxis();
angularAxis->setTickCount(9);
angularAxis->setMinorTickCount(1);
angularAxis->setLabelFormat("%d");
angularAxis->setRange(0, 360);
radialAxis->setMin(0);
radialAxis->setMax(90);
radialAxis->append("90", 0);
radialAxis->append("60", 30);
radialAxis->append("30", 60);
radialAxis->append("0", 90);
radialAxis->setLabelsPosition(QCategoryAxis::AxisLabelsPositionOnValue);
m_polarChart->addAxis(angularAxis, QPolarChart::PolarOrientationAngular);
m_polarChart->addAxis(radialAxis, QPolarChart::PolarOrientationRadial);
m_polarChart->legend()->hide();
m_polarChart->layout()->setContentsMargins(0, 0, 0, 0);
m_polarChart->setMargins(QMargins(1, 1, 1, 1));
SatNogsSatellite *sat = m_satellites.value(m_settings.m_target);
if (pass.m_aos.isValid() && pass.m_los.isValid())
{
QString title;
if (m_settings.m_utc) {
title = pass.m_aos.date().toString(m_settings.m_dateFormat);
} else {
title = pass.m_aos.toLocalTime().date().toString(m_settings.m_dateFormat);
}
m_polarChart->setTitle(QString("%1").arg(title));
QLineSeries *polarSeries = new QLineSeries();
getPassAzEl(
nullptr,
nullptr,
polarSeries,
sat->m_tle->m_tle0,
sat->m_tle->m_tle1,
sat->m_tle->m_tle2,
m_settings.m_latitude,
m_settings.m_longitude,
m_settings.m_heightAboveSeaLevel/1000.0,
pass.m_aos, pass.m_los
);
// Polar charts can't handle points that are more than 180 degrees apart, so
// we need to split passes that cross from 359 -> 0 degrees (or the reverse)
QList<QLineSeries *> series;
series.append(new QLineSeries());
QLineSeries *s = series.first();
QPen pen(QColor(32, 159, 223), 2, Qt::SolidLine);
s->setPen(pen);
qreal prevAz = polarSeries->at(0).x();
qreal prevEl = polarSeries->at(0).y();
for (int i = 1; i < polarSeries->count(); i++)
{
qreal az = polarSeries->at(i).x();
qreal el = polarSeries->at(i).y();
if ((prevAz > 270.0) && (az <= 90.0))
{
double elMid = interpolate(prevAz, prevEl, az+360.0, el, 360.0);
s->append(360.0, elMid);
series.append(new QLineSeries());
s = series.last();
s->setPen(pen);
s->append(0.0, elMid);
s->append(az, el);
}
else if ((prevAz <= 90.0) && (az > 270.0))
{
double elMid = interpolate(prevAz, prevEl, az-360.0, el, 0.0);
s->append(0.0, elMid);
series.append(new QLineSeries());
s = series.last();
s->setPen(pen);
s->append(360.0, elMid);
s->append(az, el);
}
else
{
s->append(polarSeries->at(i));
}
prevAz = az;
prevEl = el;
}
for (int i = 0; i < series.length(); i++)
{
m_polarChart->addSeries(series[i]);
series[i]->attachAxis(angularAxis);
series[i]->attachAxis(radialAxis);
}
int redrawTime = 0;
if (m_settings.m_drawRotators != SatelliteTrackerSettings::NO_ROTATORS)
{
// Plot rotator position
QString ourSourceName = QString("F:%1 %2").arg(m_satelliteTracker->getIndexInFeatureSet()).arg(m_satelliteTracker->getIdentifier());
std::vector<FeatureSet*>& featureSets = MainCore::instance()->getFeatureeSets();
for (int featureSetIndex = 0; featureSetIndex < (int)featureSets.size(); featureSetIndex++)
{
FeatureSet *featureSet = featureSets[featureSetIndex];
for (int featureIndex = 0; featureIndex < featureSet->getNumberOfFeatures(); featureIndex++)
{
Feature *feature = featureSet->getFeatureAt(featureIndex);
if (FeatureUtils::compareFeatureURIs(feature->getURI(), "sdrangel.feature.gs232controller"))
{
QString source;
ChannelWebAPIUtils::getFeatureSetting(featureSetIndex, featureIndex, "source", source); // Will return false if source isn't set in Controller
int track = 0;
ChannelWebAPIUtils::getFeatureSetting(featureSetIndex, featureIndex, "track", track);
if ((m_settings.m_drawRotators == SatelliteTrackerSettings::ALL_ROTATORS) || ((source == ourSourceName) && track))
{
int onTarget = 0;
ChannelWebAPIUtils::getFeatureReportValue(featureSetIndex, featureIndex, "onTarget", onTarget);
if (!onTarget)
{
// Target azimuth red dotted line
double targetAzimuth, targetElevation;
bool targetAzimuthOk = ChannelWebAPIUtils::getFeatureReportValue(featureSetIndex, featureIndex, "targetAzimuth", targetAzimuth);
bool targetElevationOk = ChannelWebAPIUtils::getFeatureReportValue(featureSetIndex, featureIndex, "targetElevation", targetElevation);
if (targetAzimuthOk && targetElevationOk)
{
limitAzElRange(targetAzimuth, targetElevation);
QScatterSeries *rotatorSeries = new QScatterSeries();
QColor color(255, 0, 0, 150);
QPen pen(color);
rotatorSeries->setPen(pen);
rotatorSeries->setColor(color.darker());
rotatorSeries->setMarkerSize(20);
rotatorSeries->append(targetAzimuth, 90-targetElevation);
m_polarChart->addSeries(rotatorSeries);
rotatorSeries->attachAxis(angularAxis);
rotatorSeries->attachAxis(radialAxis);
redrawTime = 333;
}
}
// Current azimuth line. Yellow while off target, green on target.
double currentAzimuth, currentElevation;
bool currentAzimuthOk = ChannelWebAPIUtils::getFeatureReportValue(featureSetIndex, featureIndex, "currentAzimuth", currentAzimuth);
bool currentElevationOk = ChannelWebAPIUtils::getFeatureReportValue(featureSetIndex, featureIndex, "currentElevation", currentElevation);
if (currentAzimuthOk && currentElevationOk)
{
limitAzElRange(currentAzimuth, currentElevation);
QScatterSeries *rotatorSeries = new QScatterSeries();
QColor color;
if (onTarget) {
color = QColor(0, 255, 0, 150);
} else {
color = QColor(255, 255, 0, 150);
}
rotatorSeries->setPen(QPen(color));
rotatorSeries->setColor(color.darker());
rotatorSeries->setMarkerSize(20);
rotatorSeries->append(currentAzimuth, 90-currentElevation);
m_polarChart->addSeries(rotatorSeries);
rotatorSeries->attachAxis(angularAxis);
rotatorSeries->attachAxis(radialAxis);
redrawTime = 333;
}
}
}
}
}
}
// Create series with single point, so we can plot time of AOS
QLineSeries *aosSeries = new QLineSeries();
aosSeries->append(polarSeries->at(0));
QTime time;
if (m_settings.m_utc) {
time = pass.m_aos.time();
} else {
time = pass.m_aos.toLocalTime().time();
}
if (m_settings.m_utc) {
aosSeries->setPointLabelsFormat(QString("AOS %1").arg(time.toString("hh:mm")));
} else {
aosSeries->setPointLabelsFormat(QString("AOS %1").arg(time.toString("hh:mm")));
}
aosSeries->setPointLabelsVisible(true);
aosSeries->setPointLabelsClipping(false);
m_polarChart->addSeries(aosSeries);
aosSeries->attachAxis(angularAxis);
aosSeries->attachAxis(radialAxis);
// Create series with single point, so we can plot time of LOS
QLineSeries *losSeries = new QLineSeries();
losSeries->append(polarSeries->at(polarSeries->count()-1));
if (m_settings.m_utc) {
time = pass.m_los.time();
} else {
time = pass.m_los.toLocalTime().time();
}
losSeries->setPointLabelsFormat(QString("LOS %1").arg(time.toString("hh:mm")));
losSeries->setPointLabelsVisible(true);
losSeries->setPointLabelsClipping(false);
m_polarChart->addSeries(losSeries);
losSeries->attachAxis(angularAxis);
losSeries->attachAxis(radialAxis);
QDateTime currentTime;
if (m_settings.m_dateTime == "") {
currentTime = m_satelliteTracker->currentDateTimeUtc();
} else if (m_settings.m_utc) {
currentTime = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs);
} else {
currentTime = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs).toUTC();
}
if ((currentTime >= pass.m_aos) && (currentTime <= pass.m_los))
{
// Create series with single point, so we can plot current time
QScatterSeries *nowSeries = new QScatterSeries();
nowSeries->setMarkerSize(3);
// Find closest point to current time
int idx = std::round(polarSeries->count() * (currentTime.toMSecsSinceEpoch() - pass.m_aos.toMSecsSinceEpoch())
/ (pass.m_los.toMSecsSinceEpoch() - pass.m_aos.toMSecsSinceEpoch()));
nowSeries->append(polarSeries->at(idx));
nowSeries->setPointLabelsFormat(m_settings.m_target);
nowSeries->setPointLabelsVisible(true);
nowSeries->setPointLabelsClipping(false);
m_polarChart->addSeries(nowSeries);
nowSeries->attachAxis(angularAxis);
nowSeries->attachAxis(radialAxis);
}
if (redrawTime > 0)
{
// Redraw to show updated rotator position
m_redrawTimer.setSingleShot(true);
m_redrawTimer.start(redrawTime);
}
delete polarSeries;
}
else
{
// Possibly geostationary, just plot current position
QDateTime currentTime;
if (m_settings.m_dateTime == "") {
currentTime = m_satelliteTracker->currentDateTimeUtc();
} else if (m_settings.m_utc) {
currentTime = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs);
} else {
currentTime = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs).toUTC();
}
QString title;
if (m_settings.m_utc) {
title = currentTime.date().toString(m_settings.m_dateFormat);
} else {
title = currentTime.toLocalTime().date().toString(m_settings.m_dateFormat);
}
m_polarChart->setTitle(QString("%1").arg(title));
QLineSeries *nowSeries = new QLineSeries();
QDateTime endTime = currentTime.addSecs(1);
getPassAzEl(
nullptr,
nullptr,
nowSeries,
sat->m_tle->m_tle0,
sat->m_tle->m_tle1,
sat->m_tle->m_tle2,
m_settings.m_latitude,
m_settings.m_longitude,
m_settings.m_heightAboveSeaLevel/1000.0,
currentTime, endTime
);
nowSeries->setPointLabelsFormat(m_settings.m_target);
nowSeries->setPointLabelsVisible(true);
nowSeries->setPointLabelsClipping(false);
m_polarChart->addSeries(nowSeries);
nowSeries->attachAxis(angularAxis);
nowSeries->attachAxis(radialAxis);
}
ui->passChart->setChart(m_polarChart);
delete oldChart;
}
// Plot target elevation/azimuth for the next pass
void SatelliteTrackerGUI::plotAzElChart()
{
if ((m_targetSatState == nullptr) || !m_satellites.contains(m_settings.m_target) || (m_targetSatState->m_passes.size() == 0))
{
ui->passChart->setChart(&m_emptyChart);
return;
}
QChart *oldChart = m_lineChart;
if (m_plotPass >= m_targetSatState->m_passes.size() - 1)
{
m_plotPass = m_targetSatState->m_passes.size() - 1;
ui->passLabel->setText(QString("%1").arg(m_plotPass));
}
const SatellitePass &pass = m_targetSatState->m_passes[m_plotPass];
// Always create a new chart, otherwise sometimes they aren't drawn properly
m_lineChart = new QChart();
m_lineChart->setTheme(m_settings.m_chartsDarkTheme ? QChart::ChartThemeDark : QChart::ChartThemeLight);
QDateTimeAxis *xAxis = new QDateTimeAxis();
QValueAxis *yLeftAxis = new QValueAxis();
QValueAxis *yRightAxis = new QValueAxis();
QString title;
if (m_settings.m_utc) {
title = pass.m_aos.date().toString(m_settings.m_dateFormat);
} else {
title = pass.m_aos.toLocalTime().date().toString(m_settings.m_dateFormat);
}
m_lineChart->setTitle(QString("%1").arg(title));
m_lineChart->legend()->hide();
m_lineChart->addAxis(xAxis, Qt::AlignBottom);
m_lineChart->addAxis(yLeftAxis, Qt::AlignLeft);
m_lineChart->addAxis(yRightAxis, Qt::AlignRight);
m_lineChart->layout()->setContentsMargins(0, 0, 0, 0);
m_lineChart->setMargins(QMargins(1, 1, 1, 1));
SatNogsSatellite *sat = m_satellites.value(m_settings.m_target);
QLineSeries *azSeries = new QLineSeries();
QLineSeries *elSeries = new QLineSeries();
getPassAzEl(
azSeries,
elSeries,
nullptr,
sat->m_tle->m_tle0,
sat->m_tle->m_tle1,
sat->m_tle->m_tle2,
m_settings.m_latitude,
m_settings.m_longitude,
m_settings.m_heightAboveSeaLevel/1000.0,
pass.m_aos,
pass.m_los
);
// Split crossing of 360/0 degrees in to multiple series in the same colour
QList<QLineSeries *> azSeriesList;
QPen pen(QColor(153, 202, 83), 2, Qt::SolidLine);
QLineSeries *s = new QLineSeries();
azSeriesList.append(s);
s->setPen(pen);
qreal prevAz = azSeries->at(0).y();
for (int i = 0; i < azSeries->count(); i++)
{
qreal az = azSeries->at(i).y();
if (((prevAz >= 270) && (az < 90)) || ((prevAz < 90) && (az >= 270)))
{
s = new QLineSeries();
azSeriesList.append(s);
s->setPen(pen);
}
s->append(azSeries->at(i).x(), az);
prevAz = az;
}
m_lineChart->addSeries(elSeries);
elSeries->attachAxis(xAxis);
elSeries->attachAxis(yLeftAxis);
for (int i = 0; i < azSeriesList.size(); i++)
{
m_lineChart->addSeries(azSeriesList[i]);
azSeriesList[i]->attachAxis(xAxis);
azSeriesList[i]->attachAxis(yRightAxis);
}
// Plot current target on elevation series
if (m_targetSatState && (m_targetSatState->m_elevation > 0.0))
{
QDateTime currentTime;
if (m_settings.m_dateTime == "") {
currentTime = m_satelliteTracker->currentDateTimeUtc();
} else if (m_settings.m_utc) {
currentTime = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs);
} else {
currentTime = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs).toUTC();
}
QScatterSeries *posSeries = new QScatterSeries();
posSeries->setMarkerSize(3);
posSeries->append(currentTime.toMSecsSinceEpoch(), m_targetSatState->m_elevation);
posSeries->setPointLabelsVisible(true);
posSeries->setPointLabelsFormat(m_settings.m_target);
posSeries->setPointLabelsClipping(false);
m_lineChart->addSeries(posSeries);
posSeries->attachAxis(xAxis);
posSeries->attachAxis(yLeftAxis);
}
xAxis->setRange(pass.m_aos, pass.m_los);
xAxis->setFormat("hh:mm");
yLeftAxis->setRange(0.0, 90.0);
yLeftAxis->setTickCount(7);
yLeftAxis->setLabelFormat("%d");
yLeftAxis->setTitleText(QString("Elevation (%1)").arg(QChar(0xb0)));
yRightAxis->setRange(0.0, 360.0);
yRightAxis->setTickCount(7);
yRightAxis->setLabelFormat("%d");
yRightAxis->setTitleText(QString("Azimuth (%1)").arg(QChar(0xb0)));
ui->passChart->setChart(m_lineChart);
delete azSeries;
delete oldChart;
}
void SatelliteTrackerGUI::resizeTable()
{
// Fill table with a row of dummy data that will size the columns nicely
int row = ui->satTable->rowCount();
ui->satTable->setRowCount(row + 1);
ui->satTable->setItem(row, SAT_COL_NAME, new QTableWidgetItem("Satellite123"));
ui->satTable->setItem(row, SAT_COL_AZ, new QTableWidgetItem("360"));
ui->satTable->setItem(row, SAT_COL_EL, new QTableWidgetItem("-90"));
ui->satTable->setItem(row, SAT_COL_TNE, new QTableWidgetItem("99:99:99 AOS"));
ui->satTable->setItem(row, SAT_COL_DUR, new QTableWidgetItem("9:99:99"));
ui->satTable->setItem(row, SAT_COL_AOS, new QTableWidgetItem("+1 10:17"));
ui->satTable->setItem(row, SAT_COL_LOS, new QTableWidgetItem("+1 10:17"));
ui->satTable->setItem(row, SAT_COL_MAX_EL, new QTableWidgetItem("90"));
ui->satTable->setItem(row, SAT_COL_DIR, new QTableWidgetItem("^"));
ui->satTable->setItem(row, SAT_COL_LATITUDE, new QTableWidgetItem("-90.0"));
ui->satTable->setItem(row, SAT_COL_LONGITUDE, new QTableWidgetItem("-180.0"));
ui->satTable->setItem(row, SAT_COL_ALT, new QTableWidgetItem("50000"));
ui->satTable->setItem(row, SAT_COL_RANGE, new QTableWidgetItem("50000"));
ui->satTable->setItem(row, SAT_COL_RANGE_RATE, new QTableWidgetItem("10.0"));
ui->satTable->setItem(row, SAT_COL_DOPPLER, new QTableWidgetItem("10000"));
ui->satTable->setItem(row, SAT_COL_PATH_LOSS, new QTableWidgetItem("100"));
ui->satTable->setItem(row, SAT_COL_NORAD_ID, new QTableWidgetItem("123456"));
ui->satTable->resizeColumnsToContents();
ui->satTable->setRowCount(row);
}
// As we only have limited space in table, display time plus number of days to AOS/LOS
// unless it's greater than 10 days, in which case just display the date
QString SatelliteTrackerGUI::formatDaysTime(qint64 days, QDateTime dateTime)
{
QDateTime dt;
if (m_settings.m_utc) {
dt = dateTime.toUTC();
} else {
dt = dateTime.toLocalTime();
}
if (abs(days) > 10) {
return dt.date().toString(m_settings.m_dateFormat);
} else if (days == 0) {
return dt.time().toString("hh:mm");
} else if (days > 0) {
return dt.time().toString(QString("hh:mm +%1").arg(days));
} else {
return dt.time().toString(QString("hh:mm %1").arg(days));
}
}
QString SatelliteTrackerGUI::formatSecondsAsHHMMSS(qint64 seconds)
{
char const* sign = "";
if (seconds < 0)
{
sign = "-";
seconds = -seconds;
}
int minutes = seconds / 60;
seconds = seconds % 60;
int hours = minutes / 60;
minutes = minutes % 60;
if (hours > 0) {
return QString("%1%2:%3:%4").arg(sign).arg(hours).arg(minutes, 2, 10, QChar('0')).arg(seconds, 2, 10, QChar('0'));
} else {
return QString("%1%2:%3").arg(sign).arg(minutes).arg(seconds, 2, 10, QChar('0'));
}
}
// Table item showing some text, but sorted by datetime set as user data
class DateTimeSortedTableWidgetItem : public QTableWidgetItem {
public:
bool operator<(const QTableWidgetItem& other) const
{
QVariant v1 = data(Qt::UserRole);
QVariant v2 = other.data(Qt::UserRole);
if (v1.isValid() && v2.isValid()) {
return v1.toDateTime() < v2.toDateTime();
} else {
return false;
}
}
};
// Handle sorting for next column, which can have times as HH:MM:SS or MM:SS
class NextEventTableWidgetItem : public QTableWidgetItem
{
public:
bool operator<(const QTableWidgetItem &other) const override
{
QString t1 = text();
QString t2 = other.text();
int t1Colons = t1.count(":");
int t2Colons = t2.count(":");
if (t1Colons == t2Colons)
{
QCollator coll;
coll.setNumericMode(true);
return coll.compare(t1, t2) < 0;
}
else
{
return t1Colons < t2Colons;
}
}
};
class NaturallySortedTableWidgetItem : public QTableWidgetItem
{
public:
bool operator<(const QTableWidgetItem &other) const override
{
QCollator coll;
coll.setNumericMode(true);
return coll.compare(text() , other.text()) < 0;
}
};
#define SPEED_OF_LIGHT 299792458.0
// Frequency in Hz, speed in m/s
static double doppler(double frequency, double speed)
{
return frequency * speed / SPEED_OF_LIGHT;
}
// Frequency in Hz, speed in m/s
static double freeSpaceLoss(double frequency, double distance)
{
return 20.0 * log10(distance) + 20 * log10(frequency) + 20 * log10(4*M_PI/SPEED_OF_LIGHT);
}
// Distance in m, delay in s
static double propagationDelay(double distance)
{
return distance / SPEED_OF_LIGHT;
}
// Update satellite data table with latest data for the satellite
void SatelliteTrackerGUI::updateTable(SatelliteState *satState)
{
// Does the table already contain this satellite?
QList<QTableWidgetItem *> matches = ui->satTable->findItems(satState->m_name, Qt::MatchExactly);
QTableWidgetItem *items[SAT_COL_COLUMNS];
if (matches.size() == 0)
{
// Add a new row
ui->satTable->setSortingEnabled(false);
int row = ui->satTable->rowCount();
ui->satTable->setRowCount(row + 1);
for (int i = 0; i < SAT_COL_COLUMNS; i++)
{
if ((i == SAT_COL_AOS) || (i == SAT_COL_LOS)) {
items[i] = new DateTimeSortedTableWidgetItem();
} else if ((i == SAT_COL_NAME) || (i == SAT_COL_NORAD_ID)) {
items[i] = new QTableWidgetItem();
} else if (i == SAT_COL_TNE) {
items[i] = new NextEventTableWidgetItem();
} else {
items[i] = new NaturallySortedTableWidgetItem();
}
items[i]->setToolTip(ui->satTable->horizontalHeaderItem(i)->toolTip());
ui->satTable->setItem(row, i, items[i]);
}
ui->satTable->setSortingEnabled(true);
// Static columns
items[SAT_COL_NAME]->setText(satState->m_name);
if (m_satellites.contains(satState->m_name))
{
SatNogsSatellite *sat = m_satellites.value(satState->m_name);
items[SAT_COL_NORAD_ID]->setData(Qt::DisplayRole, sat->m_noradCatId);
}
// Text alignment
for (int col : {SAT_COL_AZ, SAT_COL_EL, SAT_COL_TNE, SAT_COL_DUR, SAT_COL_MAX_EL,
SAT_COL_LATITUDE, SAT_COL_LONGITUDE,
SAT_COL_ALT, SAT_COL_RANGE, SAT_COL_RANGE_RATE, SAT_COL_DOPPLER,
SAT_COL_PATH_LOSS, SAT_COL_DELAY})
{
items[col]->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
}
}
else
{
// Update existing row
int row = ui->satTable->row(matches[0]);
for (int i = 0; i < SAT_COL_COLUMNS; i++) {
items[i] = ui->satTable->item(row, i);
}
}
items[SAT_COL_AZ]->setData(Qt::DisplayRole, (int)round(satState->m_azimuth));
items[SAT_COL_EL]->setData(Qt::DisplayRole, (int)round(satState->m_elevation));
if (satState->m_passes.size() > 0)
{
// Get number of days to AOS/LOS
QDateTime currentDateTime = m_satelliteTracker->currentDateTime();
int daysToAOS = currentDateTime.daysTo(satState->m_passes[0].m_aos);
int daysToLOS = currentDateTime.daysTo(satState->m_passes[0].m_los);
if (satState->m_passes[0].m_aos > currentDateTime) {
items[SAT_COL_TNE]->setText(formatSecondsAsHHMMSS(currentDateTime.secsTo(satState->m_passes[0].m_aos))+" AOS");
} else {
items[SAT_COL_TNE]->setText(formatSecondsAsHHMMSS(currentDateTime.secsTo(satState->m_passes[0].m_los))+" LOS");
}
items[SAT_COL_DUR]->setText(formatSecondsAsHHMMSS(satState->m_passes[0].m_aos.secsTo(satState->m_passes[0].m_los)));
items[SAT_COL_AOS]->setText(formatDaysTime(daysToAOS, satState->m_passes[0].m_aos));
items[SAT_COL_AOS]->setData(Qt::UserRole, satState->m_passes[0].m_aos);
items[SAT_COL_LOS]->setText(formatDaysTime(daysToLOS, satState->m_passes[0].m_los));
items[SAT_COL_LOS]->setData(Qt::UserRole, satState->m_passes[0].m_los);
items[SAT_COL_MAX_EL]->setData(Qt::DisplayRole, (int)round(satState->m_passes[0].m_maxElevation));
if (satState->m_passes[0].m_northToSouth) {
items[SAT_COL_DIR]->setText(QString("%1").arg(QChar(0x2193))); // Down arrow
} else {
items[SAT_COL_DIR]->setText(QString("%1").arg(QChar(0x2191))); // Up arrow
}
}
else
{
items[SAT_COL_TNE]->setText("");
items[SAT_COL_DUR]->setText("");
items[SAT_COL_AOS]->setText("");
items[SAT_COL_LOS]->setText("");
items[SAT_COL_MAX_EL]->setData(Qt::DisplayRole, QVariant());
items[SAT_COL_DIR]->setText("");
}
items[SAT_COL_LATITUDE]->setData(Qt::DisplayRole, satState->m_latitude);
items[SAT_COL_LONGITUDE]->setData(Qt::DisplayRole, satState->m_longitude);
items[SAT_COL_ALT]->setData(Qt::DisplayRole, (int)round(satState->m_altitude));
items[SAT_COL_RANGE]->setData(Qt::DisplayRole, (int)round(satState->m_range));
items[SAT_COL_RANGE_RATE]->setData(Qt::DisplayRole, QString::number(satState->m_rangeRate, 'f', 3));
items[SAT_COL_DOPPLER]->setData(Qt::DisplayRole, (int)round(-doppler(m_settings.m_defaultFrequency, satState->m_rangeRate*1000.0)));
items[SAT_COL_PATH_LOSS]->setData(Qt::DisplayRole, QString::number(freeSpaceLoss(m_settings.m_defaultFrequency, satState->m_range*1000.0), 'f', 1));
items[SAT_COL_DELAY]->setData(Qt::DisplayRole, QString::number(propagationDelay(satState->m_range*1000.0)*1000.0, 'f', 1));
}
void SatelliteTrackerGUI::on_satTable_cellDoubleClicked(int row, int column)
{
(void) column;
QString sat = ui->satTable->item(row, SAT_COL_NAME)->text();
FeatureWebAPIUtils::mapFind(sat);
}
void SatelliteTrackerGUI::on_satTableHeader_sortIndicatorChanged(int logicalIndex, Qt::SortOrder order)
{
m_settings.m_columnSort = logicalIndex;
m_settings.m_columnSortOrder = order;
m_settingsKeys.append("columnSort");
m_settingsKeys.append("columnSortOrder");
applySettings();
}
// Columns in table reordered
void SatelliteTrackerGUI::satTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex)
{
(void) oldVisualIndex;
m_settings.m_columnIndexes[logicalIndex] = newVisualIndex;
}
// Column in table resized (when hidden size is 0)
void SatelliteTrackerGUI::satTable_sectionResized(int logicalIndex, int oldSize, int newSize)
{
(void) oldSize;
m_settings.m_columnSizes[logicalIndex] = newSize;
}
// Right click in table header - show column select menu
void SatelliteTrackerGUI::columnSelectMenu(QPoint pos)
{
menu->popup(ui->satTable->horizontalHeader()->viewport()->mapToGlobal(pos));
}
// Hide/show column when menu selected
void SatelliteTrackerGUI::columnSelectMenuChecked(bool checked)
{
(void) checked;
QAction* action = qobject_cast<QAction*>(sender());
if (action != nullptr)
{
int idx = action->data().toInt(nullptr);
ui->satTable->setColumnHidden(idx, !action->isChecked());
}
}
// Create column select menu item
QAction *SatelliteTrackerGUI::createCheckableItem(QString &text, int idx, bool checked)
{
QAction *action = new QAction(text, this);
action->setCheckable(true);
action->setChecked(checked);
action->setData(QVariant(idx));
connect(action, SIGNAL(triggered()), this, SLOT(columnSelectMenuChecked()));
return action;
}
void SatelliteTrackerGUI::updateDeviceFeatureCombo()
{
if (m_settings.m_dateTimeSelect == SatelliteTrackerSettings::FROM_MAP) {
updateMapList();
} else if (m_settings.m_dateTimeSelect == SatelliteTrackerSettings::FROM_FILE) {
updateFileInputList();
}
}
void SatelliteTrackerGUI::updateDeviceFeatureCombo(const QStringList &items, const QString &selected)
{
// Remove items no longer in list
int i = 0;
while (i < ui->deviceFeatureSelect->count())
{
if (!items.contains(ui->deviceFeatureSelect->itemText(i))) {
ui->deviceFeatureSelect->removeItem(i);
} else {
i++;
}
}
// Add new items to list
for (auto item : items)
{
int idx = ui->deviceFeatureSelect->findText(item);
if (idx == -1) {
ui->deviceFeatureSelect->addItem(item);
}
}
ui->deviceFeatureSelect->setCurrentIndex(ui->deviceFeatureSelect->findText(selected));
}
void SatelliteTrackerGUI::updateFileInputList()
{
// Create list of File Input devices
std::vector<DeviceSet*>& deviceSets = MainCore::instance()->getDeviceSets();
int deviceIndex = 0;
QStringList items;
for (std::vector<DeviceSet*>::const_iterator it = deviceSets.begin(); it != deviceSets.end(); ++it, deviceIndex++)
{
if ((*it)->m_deviceAPI && (*it)->m_deviceAPI->getHardwareId() == "FileInput") {
items.append(QString("R%1").arg(deviceIndex));
}
}
updateDeviceFeatureCombo(items, m_settings.m_fileInputDevice);
}
void SatelliteTrackerGUI::updateMapList()
{
// Create list of Map features
std::vector<FeatureSet*>& featureSets = MainCore::instance()->getFeatureeSets();
int featureIndex = 0;
QStringList items;
for (std::vector<FeatureSet*>::const_iterator it = featureSets.begin(); it != featureSets.end(); ++it, featureIndex++)
{
for (int fi = 0; fi < (*it)->getNumberOfFeatures(); fi++)
{
Feature *feature = (*it)->getFeatureAt(fi);
if (feature->getURI() == "sdrangel.feature.map") {
items.append(QString("F%1:%2").arg(featureIndex).arg(fi));
}
}
}
updateDeviceFeatureCombo(items, m_settings.m_mapFeature);
}
void SatelliteTrackerGUI::on_deviceFeatureSelect_currentIndexChanged(int index)
{
(void) index;
if (m_settings.m_dateTimeSelect == SatelliteTrackerSettings::FROM_MAP)
{
m_settings.m_mapFeature = ui->deviceFeatureSelect->currentText();
m_settingsKeys.append("mapFeature");
} else
{
m_settings.m_fileInputDevice = ui->deviceFeatureSelect->currentText();
m_settingsKeys.append("fileInputDevice");
}
applySettings();
}
void SatelliteTrackerGUI::makeUIConnections()
{
QObject::connect(ui->startStop, &ButtonSwitch::toggled, this, &SatelliteTrackerGUI::on_startStop_toggled);
QObject::connect(ui->useMyPosition, &QToolButton::clicked, this, &SatelliteTrackerGUI::on_useMyPosition_clicked);
QObject::connect(ui->latitude, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &SatelliteTrackerGUI::on_latitude_valueChanged);
QObject::connect(ui->longitude, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &SatelliteTrackerGUI::on_longitude_valueChanged);
QObject::connect(ui->target, &QComboBox::currentTextChanged, this, &SatelliteTrackerGUI::on_target_currentTextChanged);
QObject::connect(ui->displaySettings, &QToolButton::clicked, this, &SatelliteTrackerGUI::on_displaySettings_clicked);
QObject::connect(ui->radioControl, &QToolButton::clicked, this, &SatelliteTrackerGUI::on_radioControl_clicked);
QObject::connect(ui->dateTimeSelect, qOverload<int>(&QComboBox::currentIndexChanged), this, &SatelliteTrackerGUI::on_dateTimeSelect_currentIndexChanged);
QObject::connect(ui->dateTime, &WrappingDateTimeEdit::dateTimeChanged, this, &SatelliteTrackerGUI::on_dateTime_dateTimeChanged);
QObject::connect(ui->viewOnMap, &QToolButton::clicked, this, &SatelliteTrackerGUI::on_viewOnMap_clicked);
QObject::connect(ui->updateSatData, &QToolButton::clicked, this, &SatelliteTrackerGUI::on_updateSatData_clicked);
QObject::connect(ui->selectSats, &QToolButton::clicked, this, &SatelliteTrackerGUI::on_selectSats_clicked);
QObject::connect(ui->autoTarget, &ButtonSwitch::clicked, this, &SatelliteTrackerGUI::on_autoTarget_clicked);
QObject::connect(ui->chartSelect, qOverload<int>(&QComboBox::currentIndexChanged), this, &SatelliteTrackerGUI::on_chartSelect_currentIndexChanged);
QObject::connect(ui->nextPass, &QToolButton::clicked, this, &SatelliteTrackerGUI::on_nextPass_clicked);
QObject::connect(ui->prevPass, &QToolButton::clicked, this, &SatelliteTrackerGUI::on_prevPass_clicked);
QObject::connect(ui->darkTheme, &QToolButton::clicked, this, &SatelliteTrackerGUI::on_darkTheme_clicked);
QObject::connect(ui->satTable, &QTableWidget::cellDoubleClicked, this, &SatelliteTrackerGUI::on_satTable_cellDoubleClicked);
QObject::connect(ui->satTable->horizontalHeader(), &QHeaderView::sortIndicatorChanged, this, &SatelliteTrackerGUI::on_satTableHeader_sortIndicatorChanged);
QObject::connect(ui->deviceFeatureSelect, qOverload<int>(&QComboBox::currentIndexChanged), this, &SatelliteTrackerGUI::on_deviceFeatureSelect_currentIndexChanged);
}