diff --git a/USER_MANUAL.md b/USER_MANUAL.md index ccd649383..59c353906 100644 --- a/USER_MANUAL.md +++ b/USER_MANUAL.md @@ -916,6 +916,9 @@ LDPC | Low Density Parity Check Codes - a family of powerful FEC codes * Adjust coloring of text and ticks on spectrum plot to improve visibility when in dark mode. (PR #518) 2. Enhancements: * Add tooltip to Record button to claify its behavior. (PR #511) + * Add highlighting for RX rows in FreeDV Reporter (to match web version). (PR #519) + * Add Distance column in FreeDV Reporter window. (PR #519) + * Add support for sorting columns in FreeDV Reporter window. (PR #519) 3. Cleanup: * Remove unneeded 2400B and 2020B sample files. (PR #521) diff --git a/src/config/FreeDVConfiguration.cpp b/src/config/FreeDVConfiguration.cpp index d6c83ab47..c8f0674a6 100644 --- a/src/config/FreeDVConfiguration.cpp +++ b/src/config/FreeDVConfiguration.cpp @@ -47,6 +47,8 @@ FreeDVConfiguration::FreeDVConfiguration() , reporterWindowWidth("/Windows/FreeDVReporter/width", -1) , reporterWindowHeight("/Windows/FreeDVReporter/height", -1) , reporterWindowVisible("/Windows/FreeDVReporter/visible", false) + , reporterWindowCurrentSort("/Windows/FreeDVReporter/currentSort", -1) + , reporterWindowCurrentSortDirection("/Windows/FreeDVReporter/currentSortDirection", true) /* Current tab view */ , currentNotebookTab("/MainFrame/rxNbookCtrl", 0) @@ -130,6 +132,8 @@ void FreeDVConfiguration::load(wxConfigBase* config) load_(config, reporterWindowWidth); load_(config, reporterWindowHeight); load_(config, reporterWindowVisible); + load_(config, reporterWindowCurrentSort); + load_(config, reporterWindowCurrentSortDirection); load_(config, currentNotebookTab); @@ -210,6 +214,8 @@ void FreeDVConfiguration::save(wxConfigBase* config) save_(config, reporterWindowWidth); save_(config, reporterWindowHeight); save_(config, reporterWindowVisible); + save_(config, reporterWindowCurrentSort); + save_(config, reporterWindowCurrentSortDirection); save_(config, currentNotebookTab); diff --git a/src/config/FreeDVConfiguration.h b/src/config/FreeDVConfiguration.h index 52fee06a7..5d090c302 100644 --- a/src/config/FreeDVConfiguration.h +++ b/src/config/FreeDVConfiguration.h @@ -59,6 +59,8 @@ class FreeDVConfiguration : public WxWidgetsConfigStore ConfigurationDataElement reporterWindowWidth; ConfigurationDataElement reporterWindowHeight; ConfigurationDataElement reporterWindowVisible; + ConfigurationDataElement reporterWindowCurrentSort; + ConfigurationDataElement reporterWindowCurrentSortDirection; ConfigurationDataElement currentNotebookTab; diff --git a/src/config/ReportingConfiguration.cpp b/src/config/ReportingConfiguration.cpp index 877f46892..7332fa5e4 100644 --- a/src/config/ReportingConfiguration.cpp +++ b/src/config/ReportingConfiguration.cpp @@ -40,6 +40,7 @@ ReportingConfiguration::ReportingConfiguration() , freedvReporterEnabled("/Reporting/FreeDV/Enable", true) , freedvReporterHostname("/Reporting/FreeDV/Hostname", wxT(FREEDV_REPORTER_DEFAULT_HOSTNAME)) , freedvReporterBandFilter("/Reporting/FreeDV/CurrentBandFilter", 0) + , useMetricDistances("/Reporting/FreeDV/UseMetricDistances", true) , useUTCForReporting("/CallsignList/UseUTCTime", false) @@ -86,6 +87,7 @@ void ReportingConfiguration::load(wxConfigBase* config) load_(config, freedvReporterEnabled); load_(config, freedvReporterHostname); load_(config, freedvReporterBandFilter); + load_(config, useMetricDistances); load_(config, useUTCForReporting); @@ -111,6 +113,7 @@ void ReportingConfiguration::save(wxConfigBase* config) save_(config, freedvReporterEnabled); save_(config, freedvReporterHostname); save_(config, freedvReporterBandFilter); + save_(config, useMetricDistances); save_(config, useUTCForReporting); @@ -121,4 +124,4 @@ void ReportingConfiguration::save(wxConfigBase* config) // Special save handling for reporting below. wxString tempFreqStr = wxString::Format(wxT("%" PRIu64), reportingFrequency.getWithoutProcessing()); config->Write(reportingFrequency.getElementName(), tempFreqStr); -} \ No newline at end of file +} diff --git a/src/config/ReportingConfiguration.h b/src/config/ReportingConfiguration.h index 64dba389d..c6b665b23 100644 --- a/src/config/ReportingConfiguration.h +++ b/src/config/ReportingConfiguration.h @@ -49,6 +49,7 @@ class ReportingConfiguration : public WxWidgetsConfigStore ConfigurationDataElement freedvReporterEnabled; ConfigurationDataElement freedvReporterHostname; ConfigurationDataElement freedvReporterBandFilter; + ConfigurationDataElement useMetricDistances; ConfigurationDataElement useUTCForReporting; diff --git a/src/dlg_options.cpp b/src/dlg_options.cpp index 95fefaed1..98e324567 100644 --- a/src/dlg_options.cpp +++ b/src/dlg_options.cpp @@ -46,7 +46,6 @@ OptionsDlg::OptionsDlg(wxWindow* parent, wxWindowID id, const wxString& title, c sessionActive_ = false; wxPanel* panel = new wxPanel(this); - panel->SetMinSize(wxSize(700, -1)); wxBoxSizer* bSizer30; bSizer30 = new wxBoxSizer(wxVERTICAL); @@ -136,6 +135,9 @@ OptionsDlg::OptionsDlg(wxWindow* parent, wxWindowID id, const wxString& title, c sbSizerReportingFreeDV->Add(labelFreeDVHostName, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); sbSizerReportingFreeDV->Add(m_freedvReporterHostname, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + m_useMetricDistances = new wxCheckBox(m_reportingTab, wxID_ANY, _("Distances in km"), wxDefaultPosition, wxDefaultSize, wxCHK_2STATE); + sbSizerReportingFreeDV->Add(m_useMetricDistances, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + sbSizerReportingRows->Add(sbSizerReportingFreeDV, 0, wxALL | wxEXPAND, 5); sizerReporting->Add(sbSizerReportingRows, 0, wxALL | wxEXPAND, 5); @@ -550,6 +552,7 @@ OptionsDlg::OptionsDlg(wxWindow* parent, wxWindowID id, const wxString& title, c m_ckboxManualFrequencyReporting->MoveBeforeInTabOrder(m_ckboxPskReporterEnable); m_ckboxPskReporterEnable->MoveBeforeInTabOrder(m_ckboxFreeDVReporterEnable); m_ckboxFreeDVReporterEnable->MoveBeforeInTabOrder(m_freedvReporterHostname); + m_freedvReporterHostname->MoveBeforeInTabOrder(m_useMetricDistances); m_waterfallColorScheme1->MoveBeforeInTabOrder(m_waterfallColorScheme2); m_waterfallColorScheme2->MoveBeforeInTabOrder(m_waterfallColorScheme3); @@ -763,6 +766,7 @@ void OptionsDlg::ExchangeData(int inout, bool storePersistent) // FreeDV Reporter options m_ckboxFreeDVReporterEnable->SetValue(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterEnabled); m_freedvReporterHostname->SetValue(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterHostname); + m_useMetricDistances->SetValue(wxGetApp().appConfiguration.reportingConfiguration.useMetricDistances); // Callsign list config m_ckbox_use_utc_time->SetValue(wxGetApp().appConfiguration.reportingConfiguration.useUTCForReporting); @@ -896,6 +900,7 @@ void OptionsDlg::ExchangeData(int inout, bool storePersistent) // FreeDV Reporter options wxGetApp().appConfiguration.reportingConfiguration.freedvReporterEnabled = m_ckboxFreeDVReporterEnable->GetValue(); wxGetApp().appConfiguration.reportingConfiguration.freedvReporterHostname = m_freedvReporterHostname->GetValue(); + wxGetApp().appConfiguration.reportingConfiguration.useMetricDistances = m_useMetricDistances->GetValue(); // Callsign list config wxGetApp().appConfiguration.reportingConfiguration.useUTCForReporting = m_ckbox_use_utc_time->GetValue(); @@ -1095,10 +1100,12 @@ void OptionsDlg::updateReportingState() if (m_ckboxFreeDVReporterEnable->GetValue()) { m_freedvReporterHostname->Enable(true); + m_useMetricDistances->Enable(true); } else { m_freedvReporterHostname->Enable(false); + m_useMetricDistances->Enable(false); } } else @@ -1110,6 +1117,7 @@ void OptionsDlg::updateReportingState() m_ckboxFreeDVReporterEnable->Enable(false); m_freedvReporterHostname->Enable(false); m_ckboxManualFrequencyReporting->Enable(false); + m_useMetricDistances->Enable(false); } } else @@ -1123,6 +1131,7 @@ void OptionsDlg::updateReportingState() m_ckboxPskReporterEnable->Enable(false); m_ckboxFreeDVReporterEnable->Enable(false); m_freedvReporterHostname->Enable(false); + m_useMetricDistances->Enable(false); m_ckbox_use_utc_time->Enable(false); } diff --git a/src/dlg_options.h b/src/dlg_options.h index e52d52c80..5a0a49b9d 100644 --- a/src/dlg_options.h +++ b/src/dlg_options.h @@ -136,6 +136,7 @@ class OptionsDlg : public wxDialog wxCheckBox *m_ckboxFreeDVReporterEnable; wxTextCtrl *m_freedvReporterHostname; + wxCheckBox *m_useMetricDistances; wxButton* m_BtnFifoReset; wxStaticText *m_textFifos; diff --git a/src/freedv_reporter.cpp b/src/freedv_reporter.cpp index 0eec5e746..21399117a 100644 --- a/src/freedv_reporter.cpp +++ b/src/freedv_reporter.cpp @@ -19,11 +19,12 @@ // //========================================================================== +#include #include #include "freedv_reporter.h" #define UNKNOWN_STR "--" -#define NUM_COLS (11) +#define NUM_COLS (12) using namespace std::placeholders; @@ -31,6 +32,8 @@ FreeDVReporterDialog::FreeDVReporterDialog(wxWindow* parent, wxWindowID id, cons : wxDialog(parent, id, title, pos, size, style) , reporter_(nullptr) , currentBandFilter_(FreeDVReporterDialog::BAND_ALL) + , currentSortColumn_(-1) + , sortAscending_(false) { for (int col = 0; col < NUM_COLS; col++) { @@ -47,21 +50,37 @@ FreeDVReporterDialog::FreeDVReporterDialog(wxWindow* parent, wxWindowID id, cons m_listSpots = new wxListView(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_SINGLE_SEL | wxLC_REPORT | wxLC_HRULES); m_listSpots->InsertColumn(0, wxT("Callsign"), wxLIST_FORMAT_CENTER, 80); m_listSpots->InsertColumn(1, wxT("Locator"), wxLIST_FORMAT_CENTER, 80); - m_listSpots->InsertColumn(2, wxT("Version"), wxLIST_FORMAT_CENTER, 80); - m_listSpots->InsertColumn(3, wxT("Frequency"), wxLIST_FORMAT_CENTER, 80); - m_listSpots->InsertColumn(4, wxT("Status"), wxLIST_FORMAT_CENTER, 80); - m_listSpots->InsertColumn(5, wxT("TX Mode"), wxLIST_FORMAT_CENTER, 80); - m_listSpots->InsertColumn(6, wxT("Last TX"), wxLIST_FORMAT_CENTER, 80); - m_listSpots->InsertColumn(7, wxT("Last RX Callsign"), wxLIST_FORMAT_CENTER, 120); - m_listSpots->InsertColumn(8, wxT("Last RX Mode"), wxLIST_FORMAT_CENTER, 120); - m_listSpots->InsertColumn(9, wxT("SNR"), wxLIST_FORMAT_CENTER, 40); - m_listSpots->InsertColumn(10, wxT("Last Update"), wxLIST_FORMAT_CENTER, 120); + m_listSpots->InsertColumn(2, wxT("Dist (km)"), wxLIST_FORMAT_CENTER, 80); + m_listSpots->InsertColumn(3, wxT("Version"), wxLIST_FORMAT_CENTER, 80); + m_listSpots->InsertColumn(4, wxT("Frequency"), wxLIST_FORMAT_CENTER, 80); + m_listSpots->InsertColumn(5, wxT("Status"), wxLIST_FORMAT_CENTER, 80); + m_listSpots->InsertColumn(6, wxT("TX Mode"), wxLIST_FORMAT_CENTER, 80); + m_listSpots->InsertColumn(7, wxT("Last TX"), wxLIST_FORMAT_CENTER, 80); + m_listSpots->InsertColumn(8, wxT("RX Call"), wxLIST_FORMAT_CENTER, 120); + m_listSpots->InsertColumn(9, wxT("RX Mode"), wxLIST_FORMAT_CENTER, 120); + m_listSpots->InsertColumn(10, wxT("SNR"), wxLIST_FORMAT_CENTER, 40); + m_listSpots->InsertColumn(11, wxT("Last Update"), wxLIST_FORMAT_CENTER, 120); // On Windows, the last column will end up taking a lot more space than desired regardless // of the space we actually need. Create a "dummy" column to take that space instead. - m_listSpots->InsertColumn(11, wxT(""), wxLIST_FORMAT_CENTER, 1); + m_listSpots->InsertColumn(12, wxT(""), wxLIST_FORMAT_CENTER, 1); sectionSizer->Add(m_listSpots, 0, wxALL | wxEXPAND, 2); + + // Add sorting up/down arrows. + wxArtProvider provider; + m_sortIcons = new wxImageList(16, 16, true, 2); + auto upIcon = provider.GetBitmap("wxART_GO_UP"); + assert(upIcon.IsOk()); + upIconIndex_ = m_sortIcons->Add(upIcon); + assert(upIconIndex_ >= 0); + + auto downIcon = provider.GetBitmap("wxART_GO_DOWN"); + assert(downIcon.IsOk()); + downIconIndex_ = m_sortIcons->Add(downIcon); + assert(downIconIndex_ >= 0); + + m_listSpots->AssignImageList(m_sortIcons, wxIMAGE_LIST_SMALL); // Bottom buttons // ============================= @@ -123,7 +142,12 @@ FreeDVReporterDialog::FreeDVReporterDialog(wxWindow* parent, wxWindowID id, cons this->Layout(); + // Set up highlight clear timer + m_highlightClearTimer = new wxTimer(this); + m_highlightClearTimer->Start(1000); + // Hook in events + this->Connect(wxEVT_TIMER, wxTimerEventHandler(FreeDVReporterDialog::OnTimer), NULL, this); this->Connect(wxEVT_INIT_DIALOG, wxInitDialogEventHandler(FreeDVReporterDialog::OnInitDialog)); this->Connect(wxEVT_CLOSE_WINDOW, wxCloseEventHandler(FreeDVReporterDialog::OnClose)); this->Connect(wxEVT_SIZE, wxSizeEventHandler(FreeDVReporterDialog::OnSize)); @@ -132,16 +156,23 @@ FreeDVReporterDialog::FreeDVReporterDialog(wxWindow* parent, wxWindowID id, cons m_listSpots->Connect(wxEVT_LIST_ITEM_SELECTED, wxListEventHandler(FreeDVReporterDialog::OnItemSelected), NULL, this); m_listSpots->Connect(wxEVT_LIST_ITEM_DESELECTED, wxListEventHandler(FreeDVReporterDialog::OnItemDeselected), NULL, this); + m_listSpots->Connect(wxEVT_LIST_COL_CLICK, wxListEventHandler(FreeDVReporterDialog::OnSortColumn), NULL, this); m_buttonOK->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(FreeDVReporterDialog::OnOK), NULL, this); m_buttonSendQSY->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(FreeDVReporterDialog::OnSendQSY), NULL, this); m_buttonDisplayWebpage->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(FreeDVReporterDialog::OnOpenWebsite), NULL, this); m_bandFilter->Connect(wxEVT_TEXT, wxCommandEventHandler(FreeDVReporterDialog::OnBandFilterChange), NULL, this); + + // Trigger sorting on last sorted column + sortColumn_(wxGetApp().appConfiguration.reporterWindowCurrentSort, wxGetApp().appConfiguration.reporterWindowCurrentSortDirection); } FreeDVReporterDialog::~FreeDVReporterDialog() { + m_highlightClearTimer->Stop(); + + this->Disconnect(wxEVT_TIMER, wxTimerEventHandler(FreeDVReporterDialog::OnTimer), NULL, this); this->Disconnect(wxEVT_SIZE, wxSizeEventHandler(FreeDVReporterDialog::OnSize)); this->Disconnect(wxEVT_INIT_DIALOG, wxInitDialogEventHandler(FreeDVReporterDialog::OnInitDialog)); this->Disconnect(wxEVT_CLOSE_WINDOW, wxCloseEventHandler(FreeDVReporterDialog::OnClose)); @@ -150,6 +181,7 @@ FreeDVReporterDialog::~FreeDVReporterDialog() m_listSpots->Disconnect(wxEVT_LIST_ITEM_SELECTED, wxListEventHandler(FreeDVReporterDialog::OnItemSelected), NULL, this); m_listSpots->Disconnect(wxEVT_LIST_ITEM_DESELECTED, wxListEventHandler(FreeDVReporterDialog::OnItemDeselected), NULL, this); + m_listSpots->Disconnect(wxEVT_LIST_COL_CLICK, wxListEventHandler(FreeDVReporterDialog::OnSortColumn), NULL, this); m_buttonOK->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(FreeDVReporterDialog::OnOK), NULL, this); m_buttonSendQSY->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(FreeDVReporterDialog::OnSendQSY), NULL, this); @@ -158,6 +190,23 @@ FreeDVReporterDialog::~FreeDVReporterDialog() m_bandFilter->Disconnect(wxEVT_TEXT, wxCommandEventHandler(FreeDVReporterDialog::OnBandFilterChange), NULL, this); } +void FreeDVReporterDialog::refreshDistanceColumn() +{ + wxListItem item; + m_listSpots->GetColumn(2, item); + + if (wxGetApp().appConfiguration.reportingConfiguration.useMetricDistances) + { + item.SetText("Dist (km)"); + } + else + { + item.SetText("Dist (mi)"); + } + + m_listSpots->SetColumn(2, item); +} + void FreeDVReporterDialog::setReporter(FreeDVReporter* reporter) { if (reporter_ != nullptr) @@ -265,6 +314,19 @@ void FreeDVReporterDialog::OnItemDeselected(wxListEvent& event) m_buttonSendQSY->Enable(false); } +void FreeDVReporterDialog::OnSortColumn(wxListEvent& event) +{ + int col = event.GetColumn(); + + if (col > 11) + { + // Don't allow sorting by "fake" columns. + col = -1; + } + + sortColumn_(col); +} + void FreeDVReporterDialog::OnBandFilterChange(wxCommandEvent& event) { wxGetApp().appConfiguration.reportingConfiguration.freedvReporterBandFilter = @@ -275,6 +337,25 @@ void FreeDVReporterDialog::OnBandFilterChange(wxCommandEvent& event) setBandFilter(freq); } +void FreeDVReporterDialog::OnTimer(wxTimerEvent& event) +{ + // Iterate across all visible rows. If a row is currently highlighted + // green and it's been more than >10 seconds, clear coloring. + auto curDate = wxDateTime::Now(); + for (auto index = 0; index < m_listSpots->GetItemCount(); index++) + { + std::string* sidPtr = (std::string*)m_listSpots->GetItemData(index); + auto reportData = allReporterData_[*sidPtr]; + + if (!reportData->transmitting && + (!reportData->lastRxDate.IsValid() || !reportData->lastRxDate.IsEqualUpTo(curDate, wxTimeSpan(0, 0, 10)))) + { + m_listSpots->SetItemBackgroundColour(index, wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOX)); + m_listSpots->SetItemTextColour(index, wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOXTEXT)); + } + } +} + void FreeDVReporterDialog::refreshQSYButtonState() { bool enabled = false; @@ -303,16 +384,50 @@ void FreeDVReporterDialog::setBandFilter(FilterFrequency freq) currentBandFilter_ = freq; // Update displayed list based on new filter criteria. - m_listSpots->Freeze(); - clearAllEntries_(false); - for (auto& kvp : allReporterData_) { addOrUpdateListIfNotFiltered_(kvp.second); } +} + +void FreeDVReporterDialog::sortColumn_(int col) +{ + bool direction = true; + + if (currentSortColumn_ != col) + { + direction = true; + } + else if (sortAscending_) + { + direction = false; + } + else + { + col = -1; + } + + sortColumn_(col, direction); + wxGetApp().appConfiguration.reporterWindowCurrentSort = col; + wxGetApp().appConfiguration.reporterWindowCurrentSortDirection = direction; +} + +void FreeDVReporterDialog::sortColumn_(int col, bool direction) +{ + if (currentSortColumn_ != -1) + { + m_listSpots->ClearColumnImage(currentSortColumn_); + } + + sortAscending_ = direction; + currentSortColumn_ = col; - m_listSpots->Thaw(); + if (currentSortColumn_ != -1) + { + m_listSpots->SetColumnImage(currentSortColumn_, direction ? upIconIndex_ : downIconIndex_); + m_listSpots->SortItems(&FreeDVReporterDialog::ListCompareFn_, (wxIntPtr)this); + } } void FreeDVReporterDialog::clearAllEntries_(bool clearForAllBands) @@ -339,6 +454,189 @@ void FreeDVReporterDialog::clearAllEntries_(bool clearForAllBands) } } +double FreeDVReporterDialog::calculateDistance_(wxString gridSquare1, wxString gridSquare2) +{ + double lat1 = 0; + double lon1 = 0; + double lat2 = 0; + double lon2 = 0 ; + + // Grab latitudes and longitudes for the two locations. + calculateLatLonFromGridSquare_(gridSquare1, lat1, lon1); + calculateLatLonFromGridSquare_(gridSquare2, lat2, lon2); + + // Use Haversine formula to calculate distance. See + // https://stackoverflow.com/questions/27928/calculate-distance-between-two-latitude-longitude-points-haversine-formula. + const double EARTH_RADIUS = 6371; + double dLat = DegreesToRadians_(lat2-lat1); + double dLon = DegreesToRadians_(lon2-lon1); + double a = + sin(dLat/2) * sin(dLat/2) + + cos(DegreesToRadians_(lat1)) * cos(DegreesToRadians_(lat2)) * + sin(dLon/2) * sin(dLon/2); + double c = 2 * atan2(sqrt(a), sqrt(1-a)); + return EARTH_RADIUS * c; +} + +void FreeDVReporterDialog::calculateLatLonFromGridSquare_(wxString gridSquare, double& lat, double& lon) +{ + char charA = 'A'; + char char0 = '0'; + + // Uppercase grid square for easier processing + gridSquare.MakeUpper(); + + // Start from antimeridian South Pole (e.g. over the Pacific, not over the UK) + lon = -180.0; + lat = -90.0; + + // Process first two characters + lon += ((char)gridSquare.GetChar(0) - charA) * 20; + lat += ((char)gridSquare.GetChar(1) - charA) * 10; + + // Then next two + lon += ((char)gridSquare.GetChar(2) - char0) * 2; + lat += ((char)gridSquare.GetChar(3) - char0) * 1; + + // If grid square is 6 or more letters, THEN use the next two. + // Otherwise, optional. + if (gridSquare.Length() >= 6) + { + lon += ((char)gridSquare.GetChar(4) - charA) * 5.0 / 60; + lat += ((char)gridSquare.GetChar(5) - charA) * 2.5 / 60; + } + + // Center in middle of grid square + if (gridSquare.Length() >= 6) + { + lon += 5.0 / 60 / 2; + lat += 2.5 / 60 / 2; + } + else + { + lon += 2 / 2; + lat += 1.0 / 2; + } +} + +double FreeDVReporterDialog::DegreesToRadians_(double degrees) +{ + return degrees * (M_PI / 180.0); +} + +int FreeDVReporterDialog::ListCompareFn_(wxIntPtr item1, wxIntPtr item2, wxIntPtr sortData) +{ + FreeDVReporterDialog* thisPtr = (FreeDVReporterDialog*)sortData; + std::string* leftSid = (std::string*)item1; + std::string* rightSid = (std::string*)item2; + auto leftData = thisPtr->allReporterData_[*leftSid]; + auto rightData = thisPtr->allReporterData_[*rightSid]; + + int result = 0; + + switch(thisPtr->currentSortColumn_) + { + case 0: + result = leftData->callsign.CmpNoCase(rightData->callsign); + break; + case 1: + result = leftData->gridSquare.CmpNoCase(rightData->gridSquare); + break; + case 2: + result = leftData->distanceVal - rightData->distanceVal; + break; + case 3: + result = leftData->version.CmpNoCase(rightData->version); + break; + case 4: + result = leftData->frequency - rightData->frequency; + break; + case 5: + result = leftData->status.CmpNoCase(rightData->status); + break; + case 6: + result = leftData->txMode.CmpNoCase(rightData->txMode); + break; + case 7: + if (leftData->lastTxDate.IsValid() && rightData->lastTxDate.IsValid()) + { + if (leftData->lastTxDate.IsEarlierThan(rightData->lastTxDate)) + { + result = -1; + } + else if (leftData->lastTxDate.IsLaterThan(rightData->lastTxDate)) + { + result = 1; + } + else + { + result = 0; + } + } + else if (!leftData->lastTxDate.IsValid() && rightData->lastTxDate.IsValid()) + { + result = -1; + } + else if (leftData->lastTxDate.IsValid() && !rightData->lastTxDate.IsValid()) + { + result = 1; + } + else + { + result = 0; + } + break; + case 8: + result = leftData->lastRxCallsign.CmpNoCase(rightData->lastRxCallsign); + break; + case 9: + result = leftData->lastRxMode.CmpNoCase(rightData->lastRxMode); + break; + case 10: + result = leftData->snr.CmpNoCase(rightData->snr); + break; + case 11: + if (leftData->lastUpdateDate.IsValid() && rightData->lastUpdateDate.IsValid()) + { + if (leftData->lastUpdateDate.IsEarlierThan(rightData->lastUpdateDate)) + { + result = -1; + } + else if (leftData->lastUpdateDate.IsLaterThan(rightData->lastUpdateDate)) + { + result = 1; + } + else + { + result = 0; + } + } + else if (!leftData->lastUpdateDate.IsValid() && rightData->lastUpdateDate.IsValid()) + { + result = -1; + } + else if (leftData->lastUpdateDate.IsValid() && !rightData->lastUpdateDate.IsValid()) + { + result = 1; + } + else + { + result = 0; + } + break; + default: + assert(false); + break; + } + + if (!thisPtr->sortAscending_) + { + result = -result; + } + + return result; +} + // ================================================================================= // Note: these methods below do not run under the UI thread, so we need to make sure // UI actions happen there. @@ -347,18 +645,14 @@ void FreeDVReporterDialog::clearAllEntries_(bool clearForAllBands) void FreeDVReporterDialog::onReporterConnect_() { CallAfter([&]() { - m_listSpots->Freeze(); clearAllEntries_(true); - m_listSpots->Thaw(); }); } void FreeDVReporterDialog::onReporterDisconnect_() { CallAfter([&]() { - m_listSpots->Freeze(); clearAllEntries_(true); - m_listSpots->Thaw(); }); } @@ -375,15 +669,32 @@ void FreeDVReporterDialog::onUserConnectFn_(std::string sid, std::string lastUpd temp->sid = sid; temp->callsign = wxString(callsign).Upper(); temp->gridSquare = gridSquareWxString.Left(2).Upper() + gridSquareWxString.Mid(2); + temp->distanceVal = calculateDistance_(wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare, gridSquareWxString); + + if (!wxGetApp().appConfiguration.reportingConfiguration.useMetricDistances) + { + // Convert to miles for those who prefer it + // (calculateDistance_() returns distance in km). + temp->distanceVal *= 0.621371; + } + + if (temp->distanceVal < 10.0) + { + temp->distance = wxString::Format("%.01f", temp->distanceVal); + } + else + { + temp->distance = wxString::Format("%.0f", temp->distanceVal); + } temp->version = version; temp->freqString = UNKNOWN_STR; temp->transmitting = false; if (rxOnly) { - temp->status = "Receive Only"; - temp->txMode = "N/A"; - temp->lastTx = "N/A"; + temp->status = "RX Only"; + temp->txMode = UNKNOWN_STR; + temp->lastTx = UNKNOWN_STR; } else { @@ -396,7 +707,7 @@ void FreeDVReporterDialog::onUserConnectFn_(std::string sid, std::string lastUpd temp->lastRxMode = UNKNOWN_STR; temp->snr = UNKNOWN_STR; - auto lastUpdateTime = makeValidTime_(lastUpdate); + auto lastUpdateTime = makeValidTime_(lastUpdate, temp->lastUpdateDate); temp->lastUpdate = lastUpdateTime; allReporterData_[sid] = temp; @@ -432,16 +743,14 @@ void FreeDVReporterDialog::onFrequencyChangeFn_(std::string sid, std::string las double frequencyMHz = frequencyHz / 1000000.0; wxString frequencyMHzString = wxString::Format(_("%.04f MHz"), frequencyMHz); - auto lastUpdateTime = makeValidTime_(lastUpdate); + auto lastUpdateTime = makeValidTime_(lastUpdate, iter->second->lastUpdateDate); iter->second->frequency = frequencyHz; iter->second->freqString = frequencyMHzString; iter->second->lastUpdate = lastUpdateTime; iter->second->frequency = frequencyHz; - m_listSpots->Freeze(); addOrUpdateListIfNotFiltered_(iter->second); - m_listSpots->Thaw(); } }); } @@ -465,16 +774,14 @@ void FreeDVReporterDialog::onTransmitUpdateFn_(std::string sid, std::string last iter->second->status = txStatus; iter->second->txMode = txMode; - auto lastTxTime = makeValidTime_(lastTxDate); + auto lastTxTime = makeValidTime_(lastTxDate, iter->second->lastTxDate); iter->second->lastTx = lastTxTime; } - auto lastUpdateTime = makeValidTime_(lastUpdate); + auto lastUpdateTime = makeValidTime_(lastUpdate, iter->second->lastUpdateDate); iter->second->lastUpdate = lastUpdateTime; - m_listSpots->Freeze(); addOrUpdateListIfNotFiltered_(iter->second); - m_listSpots->Thaw(); } }); } @@ -488,6 +795,9 @@ void FreeDVReporterDialog::onReceiveUpdateFn_(std::string sid, std::string lastU iter->second->lastRxCallsign = receivedCallsign; iter->second->lastRxMode = rxMode; + auto lastUpdateTime = makeValidTime_(lastUpdate, iter->second->lastUpdateDate); + iter->second->lastUpdate = lastUpdateTime; + wxString snrString = wxString::Format(_("%.01f"), snr); if (receivedCallsign == "" && rxMode == "") { @@ -495,23 +805,20 @@ void FreeDVReporterDialog::onReceiveUpdateFn_(std::string sid, std::string lastU iter->second->lastRxCallsign = UNKNOWN_STR; iter->second->lastRxMode = UNKNOWN_STR; iter->second->snr = UNKNOWN_STR; + iter->second->lastRxDate = wxDateTime(); } else { iter->second->snr = snrString; + iter->second->lastRxDate = iter->second->lastUpdateDate; } - - auto lastUpdateTime = makeValidTime_(lastUpdate); - iter->second->lastUpdate = lastUpdateTime; - m_listSpots->Freeze(); addOrUpdateListIfNotFiltered_(iter->second); - m_listSpots->Thaw(); } }); } -wxString FreeDVReporterDialog::makeValidTime_(std::string timeStr) +wxString FreeDVReporterDialog::makeValidTime_(std::string timeStr, wxDateTime& timeObj) { wxRegEx millisecondsRemoval(_("\\.[^+-]+")); wxString tmp = timeStr; @@ -542,6 +849,7 @@ wxString FreeDVReporterDialog::makeValidTime_(std::string timeStr) if (tmpDate.ParseISOCombined(tmp)) { tmpDate.MakeFromTimezone(timeZone); + timeObj = tmpDate; if (wxGetApp().appConfiguration.reportingConfiguration.useUTCForReporting) { timeZone = wxDateTime::TimeZone(wxDateTime::TZ::UTC); @@ -554,6 +862,7 @@ wxString FreeDVReporterDialog::makeValidTime_(std::string timeStr) } else { + timeObj = wxDateTime(); return _(UNKNOWN_STR); } } @@ -573,18 +882,20 @@ void FreeDVReporterDialog::addOrUpdateListIfNotFiltered_(ReporterData* data) } } + bool needResort = false; + if (itemIndex >= 0 && filtered) { // Remove as it has been filtered out. delete (std::string*)m_listSpots->GetItemData(itemIndex); - m_listSpots->DeleteItem(itemIndex); - + m_listSpots->DeleteItem(itemIndex); return; } else if (itemIndex == -1 && !filtered) { itemIndex = m_listSpots->InsertItem(m_listSpots->GetItemCount(), data->callsign); m_listSpots->SetItemPtrData(itemIndex, (wxUIntPtr)new std::string(data->sid)); + needResort = currentSortColumn_ == 0; } else if (filtered) { @@ -592,36 +903,61 @@ void FreeDVReporterDialog::addOrUpdateListIfNotFiltered_(ReporterData* data) return; } - setColumnForRow_(itemIndex, 1, data->gridSquare); - setColumnForRow_(itemIndex, 2, data->version); - setColumnForRow_(itemIndex, 3, data->freqString); - setColumnForRow_(itemIndex, 4, data->status); - setColumnForRow_(itemIndex, 5, data->txMode); - setColumnForRow_(itemIndex, 6, data->lastTx); - setColumnForRow_(itemIndex, 7, data->lastRxCallsign); - setColumnForRow_(itemIndex, 8, data->lastRxMode); - setColumnForRow_(itemIndex, 9, data->snr); - setColumnForRow_(itemIndex, 10, data->lastUpdate); + bool changed = setColumnForRow_(itemIndex, 1, data->gridSquare); + needResort |= changed && currentSortColumn_ == 1; + changed = setColumnForRow_(itemIndex, 2, data->distance); + needResort |= changed && currentSortColumn_ == 2; + changed = setColumnForRow_(itemIndex, 3, data->version); + needResort |= changed && currentSortColumn_ == 3; + changed = setColumnForRow_(itemIndex, 4, data->freqString); + needResort |= changed && currentSortColumn_ == 4; + changed = setColumnForRow_(itemIndex, 5, data->status); + needResort |= changed && currentSortColumn_ == 5; + changed = setColumnForRow_(itemIndex, 6, data->txMode); + needResort |= changed && currentSortColumn_ == 6; + changed = setColumnForRow_(itemIndex, 7, data->lastTx); + needResort |= changed && currentSortColumn_ == 7; + changed = setColumnForRow_(itemIndex, 8, data->lastRxCallsign); + needResort |= changed && currentSortColumn_ == 8; + changed = setColumnForRow_(itemIndex, 9, data->lastRxMode); + needResort |= changed && currentSortColumn_ == 9; + changed = setColumnForRow_(itemIndex, 10, data->snr); + needResort |= changed && currentSortColumn_ == 10; + changed = setColumnForRow_(itemIndex, 11, data->lastUpdate); + needResort |= changed && currentSortColumn_ == 11; if (data->transmitting) { wxColour lightRed(0xfc, 0x45, 0x00); m_listSpots->SetItemBackgroundColour(itemIndex, lightRed); - m_listSpots->SetItemTextColour(itemIndex, *wxWHITE); + m_listSpots->SetItemTextColour(itemIndex, *wxBLACK); + } + else if (data->lastRxDate.IsValid() && data->lastRxDate.IsEqualUpTo(wxDateTime::Now(), wxTimeSpan(0, 0, 10))) + { + wxColour rxForegroundColor(55, 155, 175); + m_listSpots->SetItemBackgroundColour(itemIndex, rxForegroundColor); + m_listSpots->SetItemTextColour(itemIndex, *wxBLACK); } else { m_listSpots->SetItemBackgroundColour(itemIndex, wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOX)); m_listSpots->SetItemTextColour(itemIndex, wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOXTEXT)); } + + if (needResort) + { + m_listSpots->SortItems(&FreeDVReporterDialog::ListCompareFn_, (wxIntPtr)this); + } } -void FreeDVReporterDialog::setColumnForRow_(int row, int col, wxString val) +bool FreeDVReporterDialog::setColumnForRow_(int row, int col, wxString val) { + bool result = false; auto oldText = m_listSpots->GetItemText(row, col); if (oldText != val) { + result = true; m_listSpots->SetItem(row, col, val); auto itemFont = m_listSpots->GetItemFont(row); @@ -644,6 +980,8 @@ void FreeDVReporterDialog::setColumnForRow_(int row, int col, wxString val) m_listSpots->SetColumnWidth(col, wxLIST_AUTOSIZE_USEHEADER); } } + + return result; } bool FreeDVReporterDialog::isFiltered_(uint64_t freq) diff --git a/src/freedv_reporter.h b/src/freedv_reporter.h index 05796ee89..e447706af 100644 --- a/src/freedv_reporter.h +++ b/src/freedv_reporter.h @@ -25,6 +25,8 @@ #include #include +#include + #include "main.h" #include "defines.h" #include "reporting/FreeDVReporter.h" @@ -61,6 +63,7 @@ class FreeDVReporterDialog : public wxDialog void setReporter(FreeDVReporter* reporter); void refreshQSYButtonState(); + void refreshDistanceColumn(); void setBandFilter(FilterFrequency freq); @@ -79,10 +82,15 @@ class FreeDVReporterDialog : public wxDialog void OnItemSelected(wxListEvent& event); void OnItemDeselected(wxListEvent& event); + void OnSortColumn(wxListEvent& event); + void OnTimer(wxTimerEvent& event); // Main list box that shows spots wxListView* m_listSpots; - + wxImageList* m_sortIcons; + int upIconIndex_; + int downIconIndex_; + // QSY text wxTextCtrl* m_qsyText; @@ -93,6 +101,9 @@ class FreeDVReporterDialog : public wxDialog wxButton* m_buttonOK; wxButton* m_buttonSendQSY; wxButton* m_buttonDisplayWebpage; + + // Timer to unhighlight RX rows after 10s (like with web-based Reporter) + wxTimer* m_highlightClearTimer; private: struct ReporterData @@ -100,6 +111,8 @@ class FreeDVReporterDialog : public wxDialog std::string sid; wxString callsign; wxString gridSquare; + double distanceVal; + wxString distance; wxString version; uint64_t frequency; wxString freqString; @@ -107,16 +120,21 @@ class FreeDVReporterDialog : public wxDialog wxString txMode; bool transmitting; wxString lastTx; + wxDateTime lastTxDate; + wxDateTime lastRxDate; wxString lastRxCallsign; wxString lastRxMode; wxString snr; wxString lastUpdate; + wxDateTime lastUpdateDate; }; FreeDVReporter* reporter_; std::map columnLengths_; std::map allReporterData_; FilterFrequency currentBandFilter_; + int currentSortColumn_; + bool sortAscending_; void clearAllEntries_(bool clearForAllBands); void onReporterConnect_(); @@ -127,12 +145,21 @@ class FreeDVReporterDialog : public wxDialog void onTransmitUpdateFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string txMode, bool transmitting, std::string lastTxDate); void onReceiveUpdateFn_(std::string sid, std::string lastUpdate, std::string callsign, std::string gridSquare, std::string receivedCallsign, float snr, std::string rxMode); - wxString makeValidTime_(std::string timeStr); + wxString makeValidTime_(std::string timeStr, wxDateTime& timeObj); void addOrUpdateListIfNotFiltered_(ReporterData* data); bool isFiltered_(uint64_t freq); - void setColumnForRow_(int row, int col, wxString val); + bool setColumnForRow_(int row, int col, wxString val); + + void sortColumn_(int col); + void sortColumn_(int col, bool direction); + + double calculateDistance_(wxString gridSquare1, wxString gridSquare2); + void calculateLatLonFromGridSquare_(wxString gridSquare, double& lat, double& lon); + + static int ListCompareFn_(wxIntPtr item1, wxIntPtr item2, wxIntPtr sortData); + static double DegreesToRadians_(double degrees); }; #endif // __FREEDV_REPORTER_DIALOG__ diff --git a/src/main.cpp b/src/main.cpp index 386dbac2c..19e392071 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2033,6 +2033,7 @@ void MainFrame::performFreeDVOn_() } m_reporterDialog->setReporter(freedvReporter); + m_reporterDialog->refreshDistanceColumn(); }); // Set up QSY request handler diff --git a/src/ongui.cpp b/src/ongui.cpp index 9447014b9..f755c4de6 100644 --- a/src/ongui.cpp +++ b/src/ongui.cpp @@ -97,6 +97,7 @@ void MainFrame::OnToolsFreeDVReporter(wxCommandEvent& event) m_reporterDialog = new FreeDVReporterDialog(this); } + m_reporterDialog->refreshDistanceColumn(); m_reporterDialog->Show(); m_reporterDialog->Iconize(false); // undo minimize if required m_reporterDialog->Raise(); // brings from background to foreground if required @@ -181,13 +182,19 @@ void MainFrame::OnToolsOptions(wxCommandEvent& event) m_txtCtrlCallSign->Show(); } - // + // Update voice keyer file if different auto newVkFile = wxGetApp().appConfiguration.voiceKeyerWaveFile->mb_str(); if (vkFileName_ != newVkFile) { vkFileName_ = newVkFile; } + // Refresh distance column label in case setting was changed. + if (m_reporterDialog != nullptr) + { + m_reporterDialog->refreshDistanceColumn(); + } + // Relayout window so that the changes can take effect. m_panel->Layout(); }