/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2021 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 . // /////////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include "device/deviceapi.h" #include "device/deviceset.h" #include "feature/featureset.h" #include "feature/featureuiset.h" #include "feature/featurewebapiutils.h" #include "gui/basicfeaturesettingsdialog.h" #include "mainwindow.h" #include "device/deviceuiset.h" #include "util/units.h" #include "util/astronomy.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)) { updateSelectedSats(); displaySettings(); qDebug() << " deserialize " << m_settings.m_satellites; 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; m_settings = 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)); if (satState->m_passes.size() > 0) { 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; plotChart(); 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.getName(), aosReport.getDuration(), aosReport.getMaxElevation()); 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.getName()); 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 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; } 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 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; getRollupContents()->saveState(m_rollupState); applySettings(); } 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) { ui->setupUi(getRollupContents()); getRollupContents()->arrangeRollups(); m_helpURL = "plugins/feature/satellitetracker/readme.md"; setAttribute(Qt::WA_DeleteOnClose, true); connect(getRollupContents(), SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); m_satelliteTracker = reinterpret_cast(feature); m_satelliteTracker->setMessageQueueToGUI(&m_inputMessageQueue); m_featureUISet->addRollupWidget(this); 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); // Intialise 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); // Use My Position from preferences, if none set if ((m_settings.m_latitude == 0.0) && (m_settings.m_longitude == 0.0)) { on_useMyPosition_clicked(); } 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))); m_speech = new QTextToSpeech(this); displaySettings(); applySettings(true); makeUIConnections(); // Get initial list of satellites on_updateSatData_clicked(); } SatelliteTrackerGUI::~SatelliteTrackerGUI() { delete ui; } 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->clear(); for (const QString& s : m_settings.m_satellites) { ui->target->addItem(s); } ui->target->setCurrentIndex(ui->target->findText(m_settings.m_target)); ui->dateTimeSelect->setCurrentIndex((int)m_settings.m_dateTimeSelect); ui->dateTime->setVisible(m_settings.m_dateTimeSelect == SatelliteTrackerSettings::CUSTOM); ui->dateTime->setDateTime(QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs)); ui->autoTarget->setChecked(m_settings.m_autoTarget); ui->darkTheme->setChecked(m_settings.m_chartsDarkTheme); getRollupContents()->restoreState(m_rollupState); plotChart(); blockApplySettings(false); } void SatelliteTrackerGUI::leaveEvent(QEvent*) { } void SatelliteTrackerGUI::enterEvent(QEvent*) { } void SatelliteTrackerGUI::onMenuDialogCalled(const QPoint &p) { if (m_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.move(p); 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(); setWindowTitle(m_settings.m_title); setTitleColor(m_settings.m_rgbColor); applySettings(); } resetContextMenuType(); } void SatelliteTrackerGUI::aos(const QString& name, int duration, int maxElevation) { // Give speech notification of pass QString speech = m_settings.m_aosSpeech.trimmed(); if (!speech.isEmpty()) { speech = speech.replace("${name}", name); speech = speech.replace("${duration}", QString::number(duration)); speech = speech.replace("${elevation}", QString::number(maxElevation)); m_speech->say(speech); } } void SatelliteTrackerGUI::los(const QString& name) { // Give speech notification of end of pass QString speech = m_settings.m_losSpeech.trimmed(); if (!speech.isEmpty()) { speech = speech.replace("${name}", name); m_speech->say(speech); } } 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; applySettings(); plotChart(); } void SatelliteTrackerGUI::on_longitude_valueChanged(double value) { m_settings.m_longitude = value; applySettings(); plotChart(); } void SatelliteTrackerGUI::setTarget(const QString& target) { if (target != m_settings.m_target) { m_settings.m_target = 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; applySettings(); plotChart(); } // Show settings dialog void SatelliteTrackerGUI::on_displaySettings_clicked() { SatelliteTrackerSettingsDialog dialog(&m_settings); if (dialog.exec() == QDialog::Accepted) { 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(); 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); 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); if (dialog.exec() == QDialog::Accepted) { updateSelectedSats(); applySettings(); } } void SatelliteTrackerGUI::on_radioControl_clicked() { SatelliteRadioControlDialog dialog(&m_settings, m_satellites); if (dialog.exec() == QDialog::Accepted) applySettings(); } void SatelliteTrackerGUI::on_autoTarget_clicked(bool checked) { m_settings.m_autoTarget = checked; 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, force); m_satelliteTracker->getInputMessageQueue()->push(message); } } 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(); 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); } // 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)); } 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 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); } // 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 QLineSeries *nowSeries = new QLineSeries(); // 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); // Redraw in 5 seconds (call plotChart, incase user selects a different chart) QTimer::singleShot(5000, this, &SatelliteTrackerGUI::plotChart); } 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)); } 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 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); } 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("9999:99 AOS")); ui->satTable->setItem(row, SAT_COL_DUR, new QTableWidgetItem("999: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::formatSecondsHHMM(qint64 seconds) { char const* sign = ""; if(seconds < 0) { sign = "-"; seconds = -seconds; } return QString("%1%2:%3").arg(sign).arg(seconds/60).arg(seconds%60,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; } }; 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 matches = ui->satTable->findItems(satState->m_name, Qt::MatchExactly); QTableWidgetItem *items[SAT_COL_COLUMNS]; if (matches.size() == 0) { // Add a new row 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 items[i] = new NaturallySortedTableWidgetItem(); items[i]->setToolTip(ui->satTable->horizontalHeaderItem(i)->toolTip()); ui->satTable->setItem(row, i, items[i]); } // 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(formatSecondsHHMM(currentDateTime.secsTo(satState->m_passes[0]->m_aos))+" AOS"); else items[SAT_COL_TNE]->setText(formatSecondsHHMM(currentDateTime.secsTo(satState->m_passes[0]->m_los))+" LOS"); items[SAT_COL_DUR]->setText(formatSecondsHHMM(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); } // 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(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& deviceSets = MainCore::instance()->getDeviceSets(); int deviceIndex = 0; QStringList items; for (std::vector::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& featureSets = MainCore::instance()->getFeatureeSets(); int featureIndex = 0; QStringList items; for (std::vector::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(); } else { m_settings.m_fileInputDevice = ui->deviceFeatureSelect->currentText(); } 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(&QDoubleSpinBox::valueChanged), this, &SatelliteTrackerGUI::on_latitude_valueChanged); QObject::connect(ui->longitude, qOverload(&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(&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(&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->deviceFeatureSelect, qOverload(&QComboBox::currentIndexChanged), this, &SatelliteTrackerGUI::on_deviceFeatureSelect_currentIndexChanged); }