/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2016 Edouard Griffiths, F4EXB // // Copyright (C) 2020 Jon Beniston, M7RCE // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // // the Free Software Foundation as version 3 of the License, or // // (at your option) any later version. // // // // This program is distributed in the hope that it will be useful, // // but WITHOUT ANY WARRANTY; without even the implied warranty of // // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // // GNU General Public License V3 for more details. // // // // You should have received a copy of the GNU General Public License // // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include #include #include "vordemodgui.h" #include "device/deviceuiset.h" #include "dsp/dspengine.h" #include "dsp/dspcommands.h" #include "ui_vordemodgui.h" #include "plugin/pluginapi.h" #include "util/simpleserializer.h" #include "util/db.h" #include "util/morse.h" #include "util/units.h" #include "gui/basicchannelsettingsdialog.h" #include "gui/devicestreamselectiondialog.h" #include "dsp/dspengine.h" #include "gui/crightclickenabler.h" #include "gui/audioselectdialog.h" #include "channel/channelwebapiutils.h" #include "maincore.h" #include "vordemod.h" #include "vordemodreport.h" #include "vordemodsink.h" #define VOR_COL_NAME 0 #define VOR_COL_FREQUENCY 1 #define VOR_COL_OFFSET 2 #define VOR_COL_IDENT 3 #define VOR_COL_MORSE 4 #define VOR_COL_RX_IDENT 5 #define VOR_COL_RX_MORSE 6 #define VOR_COL_RADIAL 7 #define VOR_COL_REF_MAG 8 #define VOR_COL_VAR_MAG 9 #define VOR_COL_MUTE 10 static const char *countryCodes[] = { "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au", "aw", "ax", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", "bl", "bm", "bn", "bo", "bq", "br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", "cu", "cv", "cw", "cx", "cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", "et", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gb", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "me", "mf", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "rs", "ru", "rw", "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "ss", "st", "sv", "sx", "sy", "sz", "tc", "td", "tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", "to", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "um", "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu", "wf", "ws", "ye", "yt", "za", "zm", "zw", nullptr }; // Lats and longs in decimal degrees. Distance in metres. Bearing in degrees. // https://www.movable-type.co.uk/scripts/latlong.html static void calcRadialEndPoint(float startLatitude, float startLongitude, float distance, float bearing, float &endLatitude, float &endLongitude) { double startLatRad = startLatitude*M_PI/180.0; double startLongRad = startLongitude*M_PI/180.0; double theta = bearing*M_PI/180.0; double earthRadius = 6378137.0; // At equator double delta = distance/earthRadius; double endLatRad = std::asin(sin(startLatRad)*cos(delta) + cos(startLatRad)*sin(delta)*cos(theta)); double endLongRad = startLongRad + std::atan2(sin(theta)*sin(delta)*cos(startLatRad), cos(delta) - sin(startLatRad)*sin(endLatRad)); endLatitude = endLatRad*180.0/M_PI; endLongitude = endLongRad*180.0/M_PI; } // Calculate intersection point along two radials // https://www.movable-type.co.uk/scripts/latlong.html static bool calcIntersectionPoint(float lat1, float lon1, float bearing1, float lat2, float lon2, float bearing2, float &intersectLat, float &intersectLon) { double lat1Rad = Units::degreesToRadians(lat1); double lon1Rad = Units::degreesToRadians(lon1); double lat2Rad = Units::degreesToRadians(lat2); double lon2Rad = Units::degreesToRadians(lon2); double theta13 = Units::degreesToRadians(bearing1); double theta23 = Units::degreesToRadians(bearing2); double deltaLat = lat1Rad - lat2Rad; double deltaLon = lon1Rad - lon2Rad; double sindlat = sin(deltaLat/2.0); double sindlon = sin(deltaLon/2.0); double cosLat1 = cos(lat1Rad); double cosLat2 = cos(lat2Rad); double delta12 = 2.0 * asin(sqrt(sindlat*sindlat+cosLat1*cosLat2*sindlon*sindlon)); if (abs(delta12) < std::numeric_limits::epsilon()) return false; double sinLat1 = sin(lat1Rad); double sinLat2 = sin(lat2Rad); double sinDelta12 = sin(delta12); double cosDelta12 = cos(delta12); double thetaA = acos((sinLat2-sinLat1*cosDelta12)/(sinDelta12*cosLat1)); double thetaB = acos((sinLat1-sinLat2*cosDelta12)/(sinDelta12*cosLat2)); double theta12, theta21; if (sin(lon2Rad-lon1Rad) > 0.0) { theta12 = thetaA; theta21 = 2.0*M_PI-thetaB; } else { theta12 = 2.0*M_PI-thetaA; theta21 = thetaB; } double alpha1 = theta13 - theta12; double alpha2 = theta21 - theta23; double sinAlpha1 = sin(alpha1); double sinAlpha2 = sin(alpha2); if ((sinAlpha1 == 0.0) && (sinAlpha2 == 0.0)) return false; if (sinAlpha1*sinAlpha2 < 0.0) return false; double cosAlpha1 = cos(alpha1); double cosAlpha2 = cos(alpha2); double cosAlpha3 = -cosAlpha1*cosAlpha2+sinAlpha1*sinAlpha2*cos(delta12); double delta13 = atan2(sin(delta12)*sinAlpha1*sinAlpha2, cosAlpha2+cosAlpha1*cosAlpha3); double lat3Rad = asin(sinLat1*cos(delta13)+cosLat1*sin(delta13)*cos(theta13)); double lon3Rad = lon1Rad + atan2(sin(theta13)*sin(delta13)*cosLat1, cos(delta13)-sinLat1*sin(lat3Rad)); intersectLat = Units::radiansToDegrees(lat3Rad); intersectLon = Units::radiansToDegrees(lon3Rad); return true; } VORGUI::VORGUI(NavAid *navAid, VORDemodGUI *gui) : m_navAid(navAid), m_gui(gui) { // These are deleted by QTableWidget m_nameItem = new QTableWidgetItem(); m_frequencyItem = new QTableWidgetItem(); m_offsetItem = new QTableWidgetItem(); m_radialItem = new QTableWidgetItem(); m_identItem = new QTableWidgetItem(); m_morseItem = new QTableWidgetItem(); m_rxIdentItem = new QTableWidgetItem(); m_rxMorseItem = new QTableWidgetItem(); m_varMagItem = new QTableWidgetItem(); m_refMagItem = new QTableWidgetItem(); m_muteItem = new QWidget(); m_muteButton = new QToolButton(); m_muteButton->setCheckable(true); m_muteButton->setChecked(false); m_muteButton->setToolTip("Mute/unmute audio from this VOR"); m_muteButton->setIcon(m_gui->m_muteIcon); QHBoxLayout* pLayout = new QHBoxLayout(m_muteItem); pLayout->addWidget(m_muteButton); pLayout->setAlignment(Qt::AlignCenter); pLayout->setContentsMargins(0, 0, 0, 0); m_muteItem->setLayout(pLayout); connect(m_muteButton, &QPushButton::toggled, this, &VORGUI::on_audioMute_toggled); m_coordinates.push_back(QVariant::fromValue(*new QGeoCoordinate(m_navAid->m_latitude, m_navAid->m_longitude, Units::feetToMetres(m_navAid->m_elevation)))); } void VORGUI::on_audioMute_toggled(bool checked) { m_gui->m_settings.m_subChannelSettings.value(m_navAid->m_id)->m_audioMute = checked; m_gui->applySettings(); } QVariant VORModel::data(const QModelIndex &index, int role) const { int row = index.row(); if ((row < 0) || (row >= m_vors.count())) return QVariant(); if (role == VORModel::positionRole) { // Coordinates to display the VOR icon at QGeoCoordinate coords; coords.setLatitude(m_vors[row]->m_latitude); coords.setLongitude(m_vors[row]->m_longitude); coords.setAltitude(Units::feetToMetres(m_vors[row]->m_elevation)); return QVariant::fromValue(coords); } else if (role == VORModel::vorDataRole) { // Create the text to go in the bubble next to the VOR QStringList list; list.append(QString("Name: %1").arg(m_vors[row]->m_name)); list.append(QString("Frequency: %1 MHz").arg(m_vors[row]->m_frequencykHz / 1000.0f, 0, 'f', 1)); if (m_vors[row]->m_channel != "") list.append(QString("Channel: %1").arg(m_vors[row]->m_channel)); list.append(QString("Ident: %1 %2").arg(m_vors[row]->m_ident).arg(Morse::toSpacedUnicodeMorse(m_vors[row]->m_ident))); list.append(QString("Range: %1 nm").arg(m_vors[row]->m_range)); if (m_vors[row]->m_alignedTrueNorth) list.append(QString("Magnetic declination: Aligned to true North")); else if (m_vors[row]->m_magneticDeclination != 0.0f) list.append(QString("Magnetic declination: %1%2").arg(std::round(m_vors[row]->m_magneticDeclination)).arg(QChar(0x00b0))); QString data = list.join("\n"); return QVariant::fromValue(data); } else if (role == VORModel::vorImageRole) { // Select an image to use for the VOR return QVariant::fromValue(QString("/demodvor/map/%1.png").arg(m_vors[row]->m_type)); } else if (role == VORModel::bubbleColourRole) { // Select a background colour for the text bubble next to the VOR if (m_selected[row]) return QVariant::fromValue(QColor("lightgreen")); else return QVariant::fromValue(QColor("lightblue")); } else if (role == VORModel::vorRadialRole) { // Draw a radial line from centre of VOR outwards at the demodulated angle if (m_radialsVisible && m_selected[row] && (m_vorGUIs[row] != nullptr) && (m_radials[row] != -1.0f)) { QVariantList list; list.push_back(m_vorGUIs[row]->m_coordinates[0]); // Centre of VOR float endLat, endLong; float bearing; if (m_gui->m_settings.m_magDecAdjust && !m_vors[row]->m_alignedTrueNorth) bearing = m_radials[row] - m_vors[row]->m_magneticDeclination; else bearing = m_radials[row]; calcRadialEndPoint(m_vors[row]->m_latitude, m_vors[row]->m_longitude, m_vors[row]->getRangeMetres(), bearing, endLat, endLong); list.push_back(QVariant::fromValue(*new QGeoCoordinate(endLat, endLong, Units::feetToMetres(m_vors[row]->m_elevation)))); return list; } else return QVariantList(); } else if (role == VORModel::selectedRole) return QVariant::fromValue(m_selected[row]); return QVariant(); } bool VORModel::setData(const QModelIndex &index, const QVariant& value, int role) { int row = index.row(); if ((row < 0) || (row >= m_vors.count())) return false; if (role == VORModel::selectedRole) { bool selected = value.toBool(); VORGUI *vorGUI; if (selected == true) { vorGUI = new VORGUI(m_vors[row], m_gui); m_vorGUIs[row] = vorGUI; } else vorGUI = m_vorGUIs[row]; m_gui->selectVOR(vorGUI, selected); m_selected[row] = selected; emit dataChanged(index, index); if (!selected) { delete vorGUI; m_vorGUIs[row] = nullptr; } return true; } return true; } // Find intersection between first two selected radials bool VORModel::findIntersection(float &lat, float &lon) { if (m_vors.count() > 2) { float lat1, lon1, bearing1, valid1 = false; float lat2, lon2, bearing2, valid2 = false; for (int i = 0; i < m_vors.count(); i++) { if (m_selected[i] && (m_radials[i] >= 0.0)) { if (!valid1) { lat1 = m_vors[i]->m_latitude; lon1 = m_vors[i]->m_longitude; if (m_gui->m_settings.m_magDecAdjust && !m_vors[i]->m_alignedTrueNorth) bearing1 = m_radials[i] - m_vors[i]->m_magneticDeclination; else bearing1 = m_radials[i]; valid1 = true; } else { lat2 = m_vors[i]->m_latitude; lon2 = m_vors[i]->m_longitude; if (m_gui->m_settings.m_magDecAdjust && !m_vors[i]->m_alignedTrueNorth) bearing2 = m_radials[i] - m_vors[i]->m_magneticDeclination; else bearing2 = m_radials[i]; valid2 = true; break; } } } if (valid1 && valid2) { return calcIntersectionPoint(lat1, lon1, bearing1, lat2, lon2, bearing2, lat, lon); } } return false; } void VORDemodGUI::resizeTable() { // Fill table with a row of dummy data that will size the columns nicely // Trailing spaces are for sort arrow QString morse("---- ---- ----"); int row = ui->vorData->rowCount(); ui->vorData->setRowCount(row + 1); ui->vorData->setItem(row, VOR_COL_NAME, new QTableWidgetItem("White Sulphur Springs")); ui->vorData->setItem(row, VOR_COL_FREQUENCY, new QTableWidgetItem("Freq (MHz) ")); ui->vorData->setItem(row, VOR_COL_OFFSET, new QTableWidgetItem("Offset (kHz) ")); ui->vorData->setItem(row, VOR_COL_IDENT, new QTableWidgetItem("Ident ")); ui->vorData->setItem(row, VOR_COL_MORSE, new QTableWidgetItem(Morse::toSpacedUnicode(morse))); ui->vorData->setItem(row, VOR_COL_RADIAL, new QTableWidgetItem("Radial (o) ")); ui->vorData->setItem(row, VOR_COL_RX_IDENT, new QTableWidgetItem("RX Ident ")); ui->vorData->setItem(row, VOR_COL_RX_MORSE, new QTableWidgetItem(Morse::toSpacedUnicode(morse))); ui->vorData->setItem(row, VOR_COL_VAR_MAG, new QTableWidgetItem("Var (dB) ")); ui->vorData->setItem(row, VOR_COL_REF_MAG, new QTableWidgetItem("Ref (dB) ")); ui->vorData->setItem(row, VOR_COL_MUTE, new QTableWidgetItem("Mute")); ui->vorData->resizeColumnsToContents(); ui->vorData->removeRow(row); } // Columns in table reordered void VORDemodGUI::vorData_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 VORDemodGUI::vorData_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 VORDemodGUI::columnSelectMenu(QPoint pos) { menu->popup(ui->vorData->horizontalHeader()->viewport()->mapToGlobal(pos)); } // Hide/show column when menu selected void VORDemodGUI::columnSelectMenuChecked(bool checked) { (void) checked; QAction* action = qobject_cast(sender()); if (action != nullptr) { int idx = action->data().toInt(nullptr); ui->vorData->setColumnHidden(idx, !action->isChecked()); } } // Create column select menu item QAction *VORDemodGUI::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; } // Called when a VOR is selected on the map void VORDemodGUI::selectVOR(VORGUI *vorGUI, bool selected) { if (selected) { m_selectedVORs.insert(vorGUI->m_navAid->m_id, vorGUI); ui->vorData->setSortingEnabled(false); int row = ui->vorData->rowCount(); ui->vorData->setRowCount(row + 1); ui->vorData->setItem(row, VOR_COL_NAME, vorGUI->m_nameItem); ui->vorData->setItem(row, VOR_COL_FREQUENCY, vorGUI->m_frequencyItem); ui->vorData->setItem(row, VOR_COL_OFFSET, vorGUI->m_offsetItem); ui->vorData->setItem(row, VOR_COL_IDENT, vorGUI->m_identItem); ui->vorData->setItem(row, VOR_COL_MORSE, vorGUI->m_morseItem); ui->vorData->setItem(row, VOR_COL_RADIAL, vorGUI->m_radialItem); ui->vorData->setItem(row, VOR_COL_RX_IDENT, vorGUI->m_rxIdentItem); ui->vorData->setItem(row, VOR_COL_RX_MORSE, vorGUI->m_rxMorseItem); ui->vorData->setItem(row, VOR_COL_VAR_MAG, vorGUI->m_varMagItem); ui->vorData->setItem(row, VOR_COL_REF_MAG, vorGUI->m_refMagItem); ui->vorData->setCellWidget(row, VOR_COL_MUTE, vorGUI->m_muteItem); vorGUI->m_nameItem->setText(vorGUI->m_navAid->m_name); vorGUI->m_identItem->setText(vorGUI->m_navAid->m_ident); vorGUI->m_morseItem->setText(Morse::toSpacedUnicodeMorse(vorGUI->m_navAid->m_ident)); vorGUI->m_frequencyItem->setData(Qt::DisplayRole, vorGUI->m_navAid->m_frequencykHz / 1000.0); ui->vorData->setSortingEnabled(true); // Add to settings to create corresponding demodulator VORDemodSubChannelSettings *subChannelSettings = new VORDemodSubChannelSettings(); subChannelSettings->m_id = vorGUI->m_navAid->m_id; subChannelSettings->m_frequency = vorGUI->m_navAid->m_frequencykHz * 1000; subChannelSettings->m_audioMute = false; m_settings.m_subChannelSettings.insert(vorGUI->m_navAid->m_id, subChannelSettings); applySettings(); } else { m_selectedVORs.remove(vorGUI->m_navAid->m_id); ui->vorData->removeRow(vorGUI->m_nameItem->row()); // Remove from settings to remove corresponding demodulator VORDemodSubChannelSettings *subChannelSettings = m_settings.m_subChannelSettings.value(vorGUI->m_navAid->m_id); m_settings.m_subChannelSettings.remove(vorGUI->m_navAid->m_id); delete subChannelSettings; applySettings(); } } void VORDemodGUI::updateVORs() { m_vorModel.removeAllVORs(); QHash::iterator i = m_vors->begin(); AzEl azEl = m_azEl; while (i != m_vors->end()) { NavAid *vor = i.value(); // Calculate distance to VOR from My Position azEl.setTarget(vor->m_latitude, vor->m_longitude, Units::feetToMetres(vor->m_elevation)); azEl.calculate(); // Only display VOR if in range if (azEl.getDistance() <= 200000) { m_vorModel.addVOR(vor); } ++i; } } VORDemodGUI* VORDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) { VORDemodGUI* gui = new VORDemodGUI(pluginAPI, deviceUISet, rxChannel); return gui; } void VORDemodGUI::destroy() { delete this; } void VORDemodGUI::resetToDefaults() { m_settings.resetToDefaults(); displaySettings(); applySettings(true); } QByteArray VORDemodGUI::serialize() const { return m_settings.serialize(); } bool VORDemodGUI::deserialize(const QByteArray& data) { if(m_settings.deserialize(data)) { displaySettings(); applySettings(true); return true; } else { resetToDefaults(); return false; } } bool VORDemodGUI::handleMessage(const Message& message) { if (VORDemod::MsgConfigureVORDemod::match(message)) { qDebug("VORDemodGUI::handleMessage: VORDemod::MsgConfigureVORDemod"); const VORDemod::MsgConfigureVORDemod& cfg = (VORDemod::MsgConfigureVORDemod&) message; m_settings = cfg.getSettings(); blockApplySettings(true); displaySettings(); blockApplySettings(false); return true; } else if (DSPSignalNotification::match(message)) { DSPSignalNotification& notif = (DSPSignalNotification&) message; m_basebandSampleRate = notif.getSampleRate(); return true; } else if (VORDemodReport::MsgReportRadial::match(message)) { VORDemodReport::MsgReportRadial& report = (VORDemodReport::MsgReportRadial&) message; int subChannelId = report.getSubChannelId(); VORGUI *vorGUI = m_selectedVORs.value(subChannelId); // Display radial and signal magnitudes in table Real varMagDB = std::round(20.0*std::log10(report.getVarMag())); Real refMagDB = std::round(20.0*std::log10(report.getRefMag())); bool validRadial = (refMagDB > m_settings.m_refThresholdDB) && (varMagDB > m_settings.m_varThresholdDB); vorGUI->m_radialItem->setData(Qt::DisplayRole, std::round(report.getRadial())); if (validRadial) vorGUI->m_radialItem->setForeground(QBrush(Qt::white)); else vorGUI->m_radialItem->setForeground(QBrush(Qt::red)); vorGUI->m_refMagItem->setData(Qt::DisplayRole, refMagDB); if (refMagDB > m_settings.m_refThresholdDB) vorGUI->m_refMagItem->setForeground(QBrush(Qt::white)); else vorGUI->m_refMagItem->setForeground(QBrush(Qt::red)); vorGUI->m_varMagItem->setData(Qt::DisplayRole, varMagDB); if (varMagDB > m_settings.m_varThresholdDB) vorGUI->m_varMagItem->setForeground(QBrush(Qt::white)); else vorGUI->m_varMagItem->setForeground(QBrush(Qt::red)); // Update radial on map m_vorModel.setRadial(subChannelId, validRadial, report.getRadial()); return true; } else if (VORDemodReport::MsgReportFreqOffset::match(message)) { VORDemodReport::MsgReportFreqOffset& report = (VORDemodReport::MsgReportFreqOffset&) message; int subChannelId = report.getSubChannelId(); VORGUI *vorGUI = m_selectedVORs.value(subChannelId); vorGUI->m_offsetItem->setData(Qt::DisplayRole, report.getFreqOffset() / 1000.0); if (report.getOutOfBand()) { vorGUI->m_offsetItem->setForeground(QBrush(Qt::red)); // Clear other fields as data is now invalid vorGUI->m_radialItem->setText(""); vorGUI->m_refMagItem->setText(""); vorGUI->m_varMagItem->setText(""); m_vorModel.setRadial(subChannelId, false, -1.0f); } else vorGUI->m_offsetItem->setForeground(QBrush(Qt::white)); } else if (VORDemodReport::MsgReportIdent::match(message)) { VORDemodReport::MsgReportIdent& report = (VORDemodReport::MsgReportIdent&) message; int subChannelId = report.getSubChannelId(); VORGUI *vorGUI = m_selectedVORs.value(subChannelId); QString ident = report.getIdent(); // Convert Morse to a string QString identString = Morse::toString(ident); // Idents should only be two or three characters, so filter anything else // other than TEST which indicates a VOR is under maintainance (may also be TST) if (((identString.size() >= 2) && (identString.size() <= 3)) || (identString == "TEST")) { vorGUI->m_rxIdentItem->setText(identString); vorGUI->m_rxMorseItem->setText(Morse::toSpacedUnicode(ident)); if (vorGUI->m_navAid->m_ident == identString) { // Set colour to green if matching expected ident vorGUI->m_rxIdentItem->setForeground(QBrush(Qt::green)); vorGUI->m_rxMorseItem->setForeground(QBrush(Qt::green)); } else { // Set colour to green if not matching expected ident vorGUI->m_rxIdentItem->setForeground(QBrush(Qt::red)); vorGUI->m_rxMorseItem->setForeground(QBrush(Qt::red)); } } else { // Set yellow to indicate we've filtered something (unless red) if (vorGUI->m_rxIdentItem->foreground().color() != Qt::red) { vorGUI->m_rxIdentItem->setForeground(QBrush(Qt::yellow)); vorGUI->m_rxMorseItem->setForeground(QBrush(Qt::yellow)); } } return true; } return false; } void VORDemodGUI::handleInputMessages() { Message* message; while ((message = getInputMessageQueue()->pop()) != 0) { if (handleMessage(*message)) { delete message; } } } void VORDemodGUI::channelMarkerChangedByCursor() { } void VORDemodGUI::channelMarkerHighlightedByCursor() { setHighlighted(m_channelMarker.getHighlighted()); } void VORDemodGUI::on_thresh_valueChanged(int value) { ui->threshText->setText(QString("%1").arg(value / 10.0, 0, 'f', 1)); m_settings.m_identThreshold = value / 10.0; applySettings(); } void VORDemodGUI::on_volume_valueChanged(int value) { ui->volumeText->setText(QString("%1").arg(value / 10.0, 0, 'f', 1)); m_settings.m_volume = value / 10.0; applySettings(); } void VORDemodGUI::on_squelch_valueChanged(int value) { ui->squelchText->setText(QString("%1 dB").arg(value)); m_settings.m_squelch = value; applySettings(); } void VORDemodGUI::on_audioMute_toggled(bool checked) { m_settings.m_audioMute = checked; applySettings(); } qint64 VORDemodGUI::fileAgeInDays(QString filename) { QFile file(filename); if (file.exists()) { QDateTime modified = file.fileTime(QFileDevice::FileModificationTime); if (modified.isValid()) return modified.daysTo(QDateTime::currentDateTime()); else return -1; } return -1; } bool VORDemodGUI::confirmDownload(QString filename) { qint64 age = fileAgeInDays(filename); if ((age == -1) || (age > 100)) return true; else { QMessageBox::StandardButton reply; if (age == 0) reply = QMessageBox::question(this, "Confirm download", "This file was last downloaded today. Are you sure you wish to redownload it?", QMessageBox::Yes|QMessageBox::No); else if (age == 1) reply = QMessageBox::question(this, "Confirm download", "This file was last downloaded yesterday. Are you sure you wish to redownload it?", QMessageBox::Yes|QMessageBox::No); else reply = QMessageBox::question(this, "Confirm download", QString("This file was last downloaded %1 days ago. Are you sure you wish to redownload this file?").arg(age), QMessageBox::Yes|QMessageBox::No); return reply == QMessageBox::Yes; } } QString VORDemodGUI::getDataDir() { // Get directory to store app data in QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); // First dir is writable return locations[0]; } QString VORDemodGUI::getOpenAIPVORDBFilename(int i) { if (countryCodes[i] != nullptr) return getDataDir() + "/" + countryCodes[i] + "_nav.aip"; else return ""; } QString VORDemodGUI::getOpenAIPVORDBURL(int i) { if (countryCodes[i] != nullptr) return QString(OPENAIP_NAVAIDS_URL).arg(countryCodes[i]); else return ""; } QString VORDemodGUI::getVORDBFilename() { return getDataDir() + "/vorDatabase.csv"; } void VORDemodGUI::updateDownloadProgress(qint64 bytesRead, qint64 totalBytes) { if (m_progressDialog) { m_progressDialog->setMaximum(totalBytes); m_progressDialog->setValue(bytesRead); } } void VORDemodGUI::downloadFinished(const QString& filename, bool success) { bool closeDialog = true; if (success) { if (filename == getVORDBFilename()) { m_vors = NavAid::readNavAidsDB(filename); if (m_vors != nullptr) updateVORs(); } else if (filename == getOpenAIPVORDBFilename(m_countryIndex)) { m_countryIndex++; if (countryCodes[m_countryIndex] != nullptr) { QString vorDBFile = getOpenAIPVORDBFilename(m_countryIndex); QString urlString = getOpenAIPVORDBURL(m_countryIndex); QUrl dbURL(urlString); m_progressDialog->setLabelText(QString("Downloading %1.").arg(urlString)); m_progressDialog->setValue(m_countryIndex); m_dlm.download(dbURL, vorDBFile); closeDialog = false; } else { readNavAids(); if (m_vors != nullptr) updateVORs(); } } else { qDebug() << "VORDemodGUI::downloadFinished: Unexpected filename: " << filename; } } else { qDebug() << "VORDemodGUI::downloadFinished: Failed: " << filename; QMessageBox::warning(this, "Download failed", QString("Failed to download %1").arg(filename)); } if (closeDialog && m_progressDialog) { m_progressDialog->close(); delete m_progressDialog; m_progressDialog = nullptr; } } void VORDemodGUI::on_getOurAirportsVORDB_clicked(bool checked) { (void) checked; // Don't try to download while already in progress if (m_progressDialog == nullptr) { QString vorDBFile = getVORDBFilename(); if (confirmDownload(vorDBFile)) { // Download OurAirports navaid database to disk QUrl dbURL(QString(OURAIRPORTS_NAVAIDS_URL)); m_progressDialog = new QProgressDialog(this); m_progressDialog->setCancelButton(nullptr); m_progressDialog->setMinimumDuration(500); m_progressDialog->setLabelText(QString("Downloading %1.").arg(OURAIRPORTS_NAVAIDS_URL)); QNetworkReply *reply = m_dlm.download(dbURL, vorDBFile); connect(reply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(updateDownloadProgress(qint64,qint64))); } } } void VORDemodGUI::on_getOpenAIPVORDB_clicked(bool checked) { (void) checked; // Don't try to download while already in progress if (m_progressDialog == nullptr) { m_countryIndex = 0; QString vorDBFile = getOpenAIPVORDBFilename(m_countryIndex); if (confirmDownload(vorDBFile)) { // Download OpenAIP XML to disk QString urlString = getOpenAIPVORDBURL(m_countryIndex); QUrl dbURL(urlString); m_progressDialog = new QProgressDialog(this); m_progressDialog->setCancelButton(nullptr); m_progressDialog->setMinimumDuration(500); m_progressDialog->setMaximum(sizeof(countryCodes)/sizeof(countryCodes[0])); m_progressDialog->setValue(0); m_progressDialog->setLabelText(QString("Downloading %1.").arg(urlString)); m_dlm.download(dbURL, vorDBFile); } } } void VORDemodGUI::readNavAids() { m_vors = new QHash(); for (int countryIndex = 0; countryCodes[countryIndex] != nullptr; countryIndex++) { QString vorDBFile = getOpenAIPVORDBFilename(countryIndex); NavAid::readNavAidsXML(m_vors, vorDBFile); } } void VORDemodGUI::on_magDecAdjust_clicked(bool checked) { m_settings.m_magDecAdjust = checked; m_vorModel.allVORUpdated(); applySettings(); } void VORDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) { (void) widget; (void) rollDown; } void VORDemodGUI::onMenuDialogCalled(const QPoint &p) { if (m_contextMenuType == ContextMenuChannelSettings) { BasicChannelSettingsDialog dialog(&m_channelMarker, this); dialog.setUseReverseAPI(m_settings.m_useReverseAPI); dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex); dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex); dialog.move(p); dialog.exec(); m_settings.m_rgbColor = m_channelMarker.getColor().rgb(); m_settings.m_title = m_channelMarker.getTitle(); m_settings.m_useReverseAPI = dialog.useReverseAPI(); m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex(); setWindowTitle(m_settings.m_title); setTitleColor(m_settings.m_rgbColor); applySettings(); } else if ((m_contextMenuType == ContextMenuStreamSettings) && (m_deviceUISet->m_deviceMIMOEngine)) { DeviceStreamSelectionDialog dialog(this); dialog.setNumberOfStreams(m_vorDemod->getNumberOfDeviceStreams()); dialog.setStreamIndex(m_settings.m_streamIndex); dialog.move(p); dialog.exec(); m_settings.m_streamIndex = dialog.getSelectedStreamIndex(); m_channelMarker.clearStreamIndexes(); m_channelMarker.addStreamIndex(m_settings.m_streamIndex); displayStreamIndex(); applySettings(); } resetContextMenuType(); } VORDemodGUI::VORDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : ChannelGUI(parent), ui(new Ui::VORDemodGUI), m_pluginAPI(pluginAPI), m_deviceUISet(deviceUISet), m_channelMarker(this), m_doApplySettings(true), m_squelchOpen(false), m_tickCount(0), m_progressDialog(nullptr), m_vorModel(this), m_vors(nullptr) { ui->setupUi(this); ui->map->rootContext()->setContextProperty("vorModel", &m_vorModel); ui->map->setSource(QUrl(QStringLiteral("qrc:/demodvor/map/map.qml"))); m_muteIcon.addPixmap(QPixmap("://sound_off.png"), QIcon::Normal, QIcon::On); m_muteIcon.addPixmap(QPixmap("://sound_on.png"), QIcon::Normal, QIcon::Off); setAttribute(Qt::WA_DeleteOnClose, true); connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &VORDemodGUI::downloadFinished); m_vorDemod = reinterpret_cast(rxChannel); m_vorDemod->setMessageQueueToGUI(getInputMessageQueue()); connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms CRightClickEnabler *audioMuteRightClickEnabler = new CRightClickEnabler(ui->audioMute); connect(audioMuteRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(audioSelect())); ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue); m_channelMarker.blockSignals(true); m_channelMarker.setColor(Qt::yellow); m_channelMarker.setBandwidth(2*48000); m_channelMarker.setCenterFrequency(0); m_channelMarker.setTitle("VOR Demodulator"); m_channelMarker.blockSignals(false); m_channelMarker.setVisible(true); // activate signal on the last setting only setTitleColor(m_channelMarker.getColor()); m_settings.setChannelMarker(&m_channelMarker); m_deviceUISet->addChannelMarker(&m_channelMarker); m_deviceUISet->addRollupWidget(this); connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); // Get station position Real stationLatitude = MainCore::instance()->getSettings().getLatitude(); Real stationLongitude = MainCore::instance()->getSettings().getLongitude(); Real stationAltitude = MainCore::instance()->getSettings().getAltitude(); m_azEl.setLocation(stationLatitude, stationLongitude, stationAltitude); // Centre map at My Position QQuickItem *item = ui->map->rootObject(); QObject *object = item->findChild("map"); if(object != NULL) { QGeoCoordinate coords = object->property("center").value(); coords.setLatitude(stationLatitude); coords.setLongitude(stationLongitude); object->setProperty("center", QVariant::fromValue(coords)); } // Move antenna icon to My Position to start with QObject *stationObject = item->findChild("station"); if(stationObject != NULL) { QGeoCoordinate coords = stationObject->property("coordinate").value(); coords.setLatitude(stationLatitude); coords.setLongitude(stationLongitude); coords.setAltitude(stationAltitude); stationObject->setProperty("coordinate", QVariant::fromValue(coords)); stationObject->setProperty("stationName", QVariant::fromValue(MainCore::instance()->getSettings().getStationName())); } // Read in VOR information if it exists bool useOurAirports = false; if (useOurAirports) { m_vors = NavAid::readNavAidsDB(getVORDBFilename()); ui->getOpenAIPVORDB->setVisible(false); } else { readNavAids(); ui->getOurAirportsVORDB->setVisible(false); } if (m_vors != nullptr) updateVORs(); // Resize the table using dummy data resizeTable(); // Allow user to reorder columns ui->vorData->horizontalHeader()->setSectionsMovable(true); // Allow user to sort table by clicking on headers ui->vorData->setSortingEnabled(true); // Add context menu to allow hiding/showing of columns menu = new QMenu(ui->vorData); for (int i = 0; i < ui->vorData->horizontalHeader()->count(); i++) { QString text = ui->vorData->horizontalHeaderItem(i)->text(); menu->addAction(createCheckableItem(text, i, true)); } ui->vorData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->vorData->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(columnSelectMenu(QPoint))); // Get signals when columns change connect(ui->vorData->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(vorData_sectionMoved(int, int, int))); connect(ui->vorData->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(vorData_sectionResized(int, int, int))); displaySettings(); applySettings(true); } VORDemodGUI::~VORDemodGUI() { delete ui; } void VORDemodGUI::blockApplySettings(bool block) { m_doApplySettings = !block; } void VORDemodGUI::applySettings(bool force) { if (m_doApplySettings) { VORDemod::MsgConfigureVORDemod* message = VORDemod::MsgConfigureVORDemod::create( m_settings, force); m_vorDemod->getInputMessageQueue()->push(message); } } void VORDemodGUI::displaySettings() { m_channelMarker.blockSignals(true); m_channelMarker.setCenterFrequency(0); m_channelMarker.setBandwidth(m_basebandSampleRate > 0 ? m_basebandSampleRate : 2*48000); m_channelMarker.setTitle(m_settings.m_title); m_channelMarker.blockSignals(false); m_channelMarker.setColor(m_settings.m_rgbColor); // activate signal on the last setting only setTitleColor(m_settings.m_rgbColor); setWindowTitle(m_channelMarker.getTitle()); blockApplySettings(true); ui->thresh->setValue(m_settings.m_identThreshold * 10.0); ui->threshText->setText(QString("%1").arg(m_settings.m_identThreshold, 0, 'f', 1)); ui->volume->setValue(m_settings.m_volume * 10.0); ui->volumeText->setText(QString("%1").arg(m_settings.m_volume, 0, 'f', 1)); ui->squelch->setValue(m_settings.m_squelch); ui->squelchText->setText(QString("%1 dB").arg(m_settings.m_squelch)); ui->audioMute->setChecked(m_settings.m_audioMute); displayStreamIndex(); // Order and size columns QHeaderView *header = ui->vorData->horizontalHeader(); for (int i = 0; i < VORDEMOD_COLUMNS; i++) { bool hidden = m_settings.m_columnSizes[i] == 0; header->setSectionHidden(i, hidden); menu->actions().at(i)->setChecked(!hidden); if (m_settings.m_columnSizes[i] > 0) ui->vorData->setColumnWidth(i, m_settings.m_columnSizes[i]); header->moveSection(header->visualIndex(i), m_settings.m_columnIndexes[i]); } blockApplySettings(false); } void VORDemodGUI::displayStreamIndex() { if (m_deviceUISet->m_deviceMIMOEngine) { setStreamIndicator(tr("%1").arg(m_settings.m_streamIndex)); } else { setStreamIndicator("S"); // single channel indicator } } void VORDemodGUI::leaveEvent(QEvent*) { m_channelMarker.setHighlighted(false); } void VORDemodGUI::enterEvent(QEvent*) { m_channelMarker.setHighlighted(true); } void VORDemodGUI::audioSelect() { qDebug("VORDemodGUI::audioSelect"); AudioSelectDialog audioSelect(DSPEngine::instance()->getAudioDeviceManager(), m_settings.m_audioDeviceName); audioSelect.exec(); if (audioSelect.m_selected) { m_settings.m_audioDeviceName = audioSelect.m_audioDeviceName; applySettings(); } } void VORDemodGUI::tick() { double magsqAvg, magsqPeak; int nbMagsqSamples; m_vorDemod->getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples); double powDbAvg = CalcDb::dbPower(magsqAvg); double powDbPeak = CalcDb::dbPower(magsqPeak); ui->channelPowerMeter->levelChanged( (100.0f + powDbAvg) / 100.0f, (100.0f + powDbPeak) / 100.0f, nbMagsqSamples); if (m_tickCount % 4 == 0) { ui->channelPower->setText(QString::number(powDbAvg, 'f', 1)); } int audioSampleRate = m_vorDemod->getAudioSampleRate(); bool squelchOpen = m_vorDemod->getSquelchOpen(); if (squelchOpen != m_squelchOpen) { if (audioSampleRate < 0) { ui->audioMute->setStyleSheet("QToolButton { background-color : red; }"); } else if (squelchOpen) { ui->audioMute->setStyleSheet("QToolButton { background-color : green; }"); } else { ui->audioMute->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); } m_squelchOpen = squelchOpen; } // Try to determine position, based on intersection of two radials if (m_tickCount % 50) { float lat, lon; if (m_vorModel.findIntersection(lat, lon)) { // Move antenna icon to estimated position QQuickItem *item = ui->map->rootObject(); QObject *stationObject = item->findChild("station"); if(stationObject != NULL) { QGeoCoordinate coords = stationObject->property("coordinate").value(); coords.setLatitude(lat); coords.setLongitude(lon); stationObject->setProperty("coordinate", QVariant::fromValue(coords)); stationObject->setProperty("stationName", QVariant::fromValue(MainCore::instance()->getSettings().getStationName())); } } } m_tickCount++; }