OpenCPN Partial API docs
Loading...
Searching...
No Matches
data_monitor.cpp
Go to the documentation of this file.
1/***************************************************************************
2 * Copyright (C) 2025 Alec Leamas *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
8 * *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, see <https://www.gnu.org/licenses/>. *
16 **************************************************************************/
17
24#include <chrono>
25#include <fstream>
26#include <sstream>
27
28#include <wx/app.h>
29#include <wx/button.h>
30#include <wx/choice.h>
31#include <wx/filedlg.h>
32#include <wx/menu.h>
33#include <wx/panel.h>
34#include <wx/sizer.h>
35#include <wx/statline.h>
36#include <wx/stattext.h>
37#include <wx/translation.h>
38#include <wx/wrapsizer.h>
39
40#ifdef __ANDROID__
41#include "androidUTIL.h"
42#endif
43
44#include "libgui/svg_button.h"
45
46#include "model/base_platform.h"
47#include "model/config_vars.h"
50#include "model/gui.h"
51#include "model/navmsg_filter.h"
52#include "model/nmea_log.h"
53#include "model/svg_utils.h"
54
55#include "data_monitor.h"
56#include "std_filesystem.h"
57#include "svg_icons.h"
58#include "tty_scroll.h"
59#include "user_colors_dlg.h"
60#include "filter_dlg.h"
61
62#pragma clang diagnostic push
63#pragma ide diagnostic ignored "UnreachableCode"
64
65// Make _() return std::string instead of wxString;
66#undef _
67#if wxCHECK_VERSION(3, 2, 0)
68#define _(s) wxGetTranslation(wxASCII_STR(s)).ToStdString()
69#else
70#define _(s) wxGetTranslation((s)).ToStdString()
71#endif
72
73using SetFormatFunc = std::function<void(DataLogger::Format, std::string)>;
74
76template <typename T>
77T* GetWindowById(int id) {
78 return dynamic_cast<T*>(wxWindow::FindWindowById(id));
79};
80
81static const char* const kFilterChoiceName = "FilterChoiceWindow";
82
83// clang-format: off
84static const std::unordered_map<NavAddr::Bus, std::string> kSourceByBus = {
85 {NavAddr::Bus::N0183, "NMEA0183"},
86 {NavAddr::Bus::N2000, "NMEA2000"},
87 {NavAddr::Bus::Signalk, "SignalK"}}; // clang-format: on
88
90static bool IsUserFilter(const std::string& filter_name) {
91 std::vector<std::string> filters = filters_on_disk::List();
92 auto found = std::find(filters.begin(), filters.end(), filter_name);
93 if (found != filters.end()) return true;
94 return std::any_of(
95 filters.begin(), filters.end(),
96 [filter_name](const std::string& f) { return f == filter_name; });
97}
98
100static std::string TimeStamp(const NavmsgTimePoint& when,
101 const NavmsgTimePoint& since) {
102 using namespace std::chrono;
103 using namespace std;
104
105 auto duration = when - since;
106 std::stringstream ss;
107 auto hrs = duration_cast<hours>(duration) % 24;
108 duration -= duration_cast<hours>(duration) / 24;
109 auto mins = duration_cast<minutes>(duration) % 60;
110 duration -= duration_cast<minutes>(duration) / 60;
111 auto secs = duration_cast<seconds>(duration) % 60;
112 duration -= duration_cast<seconds>(duration) / 60;
113 const auto msecs = duration_cast<milliseconds>(duration);
114 ss << setw(2) << setfill('0') << hrs.count() << ":" << setw(2) << mins.count()
115 << ":" << setw(2) << secs.count() << "." << setw(3)
116 << msecs.count() % 1000;
117 return ss.str();
118}
119
120static fs::path NullLogfile() {
121 if (wxPlatformInfo::Get().GetOperatingSystemId() & wxOS_WINDOWS)
122 return "NUL:";
123 return "/dev/null";
124}
125
132static std::string VdrQuote(const std::string& arg) {
133 auto static constexpr npos = std::string::npos;
134 if (arg.find(',') == npos && arg.find('"') == npos) return arg;
135 std::string s;
136 for (const auto c : arg) {
137 if (c == '"')
138 s += "\"\"";
139 else
140 s += c;
141 }
142 return "\"" + s + "\"";
143}
144
149static void AddVdrLogline(const Logline& ll, std::ostream& stream) {
150 if (kSourceByBus.find(ll.navmsg->bus) == kSourceByBus.end()) return;
151
152 using namespace std::chrono;
153 const auto now = system_clock::now();
154 const auto ms = duration_cast<milliseconds>(now.time_since_epoch()).count();
155 stream << ms << ",";
156
157 stream << kSourceByBus.at(ll.navmsg->bus) << ",";
158 stream << ll.navmsg->source->iface << ",";
159 switch (ll.navmsg->bus) {
160 case NavAddr::Bus::N0183: {
161 auto msg0183 = std::dynamic_pointer_cast<const Nmea0183Msg>(ll.navmsg);
162 stream << msg0183->talker << msg0183->type << ",";
163 } break;
164 case NavAddr::Bus::N2000: {
165 auto msg2000 = std::dynamic_pointer_cast<const Nmea2000Msg>(ll.navmsg);
166 stream << msg2000->PGN.to_string() << ",";
167 } break;
168 case NavAddr::Bus::Signalk: {
169 auto msgSignalK = std::dynamic_pointer_cast<const SignalkMsg>(ll.navmsg);
170 stream << "\"" << msgSignalK->context_self << "\",";
171 } break;
172 default:
173 assert(false && "Illegal message type");
174 };
175 stream << VdrQuote(ll.navmsg->to_vdr()) << "\n";
176}
177
179static void AddStdLogline(const Logline& ll, std::ostream& stream, char fs,
180 const NavmsgTimePoint log_start) {
181 if (!ll.navmsg) return;
182 wxString ws;
183 ws << TimeStamp(ll.navmsg->created_at, log_start) << fs;
184 if (ll.state.direction == NavmsgStatus::Direction::kOutput)
185 ws << kUtfRightArrow << fs;
186 else if (ll.state.direction == NavmsgStatus::Direction::kInput)
187 ws << kUtfLeftwardsArrowToBar << fs;
188 else if (ll.state.direction == NavmsgStatus::Direction::kInternal)
189 ws << kUtfLeftRightArrow << fs;
190 else
191 ws << kUtfLeftArrow << fs;
192 if (ll.state.status != NavmsgStatus::State::kOk)
193 ws << kUtfMultiplicationX << fs;
194 else if (ll.state.accepted == NavmsgStatus::Accepted::kFilteredNoOutput)
195 ws << kUtfFallingDiagonal << fs;
196 else if (ll.state.accepted == NavmsgStatus::Accepted::kFilteredDropped)
197 ws << kUtfCircledDivisionSlash << fs;
198 else
199 ws << kUtfCheckMark << fs;
200
201 ws << ll.navmsg->source->iface << fs;
202 ws << NavAddr::BusToString(ll.navmsg->bus) << fs;
203 if (ll.state.status != NavmsgStatus::State::kOk)
204 ws << (!ll.error_msg.empty() ? ll.error_msg : "Unknown error");
205 else
206 ws << "ok";
207 ws << fs << ll.message << "\n";
208 stream << ws;
209}
210
212class CrossIconWindow : public wxWindow {
213public:
214 CrossIconWindow(wxWindow* parent, std::function<void()> on_click)
215 : wxWindow(parent, wxID_ANY), m_on_click(on_click) {
216 fs::path icon_path(g_BasePlatform->GetSharedDataDir().ToStdString());
217 icon_path /= fs::path("uidata") / "MUI_flat" / "cross-small-symbolic.svg";
218 int size = parent->GetTextExtent("X").y;
219 m_bitmap = LoadSVG(icon_path.string(), size, size);
220 assert(m_bitmap.IsOk());
221 SetInitialSize({size, size});
222
223 Bind(wxEVT_LEFT_DOWN, [&](wxMouseEvent&) { m_on_click(); });
224 Bind(wxEVT_PAINT, [&](wxPaintEvent& ev) { OnPaint(ev); });
225 }
226
227private:
228 void OnPaint(wxPaintEvent& event) {
229 wxPaintDC dc(this);
230 PrepareDC(dc);
231 dc.DrawBitmap(m_bitmap, 0, 0, true);
232 }
233
234 wxBitmap m_bitmap;
235 std::function<void()> m_on_click;
236};
237
239class TtyPanel : public wxPanel, public NmeaLog {
240public:
241 TtyPanel(wxWindow* parent, size_t lines)
242 : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
243 wxTAB_TRAVERSAL, "TtyPanel"),
244 m_tty_scroll(nullptr),
245 m_filter(this, wxID_ANY),
246 m_lines(lines),
247 m_on_right_click([] {}) {
248 const auto vbox = new wxBoxSizer(wxVERTICAL);
249 m_tty_scroll = new TtyScroll(this, static_cast<int>(m_lines));
250 m_tty_scroll->Bind(wxEVT_RIGHT_UP,
251 [&](wxMouseEvent&) { m_on_right_click(); });
252 vbox->Add(m_tty_scroll, wxSizerFlags(1).Expand().Border());
253 m_filter.Hide();
254 SetSizer(vbox);
255 wxWindow::Fit();
256 }
257
258 void Add(const Logline& ll) override { m_tty_scroll->Add(ll); }
259
260 bool IsVisible() const override { return IsShownOnScreen(); }
261
262 void OnStop(bool stop) const {
263 m_tty_scroll->Pause(stop);
264 if (stop)
265 m_tty_scroll->ShowScrollbars(wxSHOW_SB_DEFAULT, wxSHOW_SB_DEFAULT);
266 else
267 m_tty_scroll->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
268 }
269
270 void SetFilter(const NavmsgFilter& f) const { m_tty_scroll->SetFilter(f); };
271
272 void SetQuickFilter(const std::string& filter) const {
273 m_tty_scroll->SetQuickFilter(filter);
274 }
275
276 void SetOnRightClick(std::function<void()> f) {
277 m_on_right_click = std::move(f);
278 }
279
281 static void AddIfExists(const Logline& ll) {
282 auto window = wxWindow::FindWindowByName("TtyPanel");
283 if (!window) return;
284 auto tty_panel = dynamic_cast<TtyPanel*>(window);
285 if (tty_panel) tty_panel->Add(ll);
286 }
287
288protected:
289 wxSize DoGetBestClientSize() const override {
290 return {1, static_cast<int>(m_lines * GetCharHeight())};
291 }
292
293private:
294 TtyScroll* m_tty_scroll;
295 wxTextCtrl m_filter;
296 size_t m_lines;
297 std::function<void()> m_on_right_click;
298};
299
301class QuickFilterPanel : public wxPanel {
302public:
303 QuickFilterPanel(wxWindow* parent, std::function<void()> on_text_evt,
304 std::function<void()> on_close)
305 : wxPanel(parent),
306 m_text_ctrl(new wxTextCtrl(this, wxID_ANY)),
307 m_on_text_evt(std::move(on_text_evt)),
308 m_on_close(std::move(on_close)) {
309 auto hbox = new wxBoxSizer(wxHORIZONTAL);
310 auto flags = wxSizerFlags(0).Border();
311 auto label_box = new wxBoxSizer(wxVERTICAL);
312 label_box->Add(new wxStaticText(this, wxID_ANY, _("Quick filter:")));
313 hbox->Add(label_box, flags.Align(wxALIGN_CENTER_VERTICAL));
314 hbox->Add(m_text_ctrl, flags);
315 hbox->AddStretchSpacer();
316 hbox->Add(new CrossIconWindow(this, [&] { m_on_close(); }), flags);
317 SetSizer(hbox);
318 wxWindow::Fit();
319 wxWindow::Show();
320 m_text_ctrl->Bind(wxEVT_TEXT, [&](wxCommandEvent&) { m_on_text_evt(); });
321 }
322
323 bool Show(bool show) override {
324 if (!show) m_text_ctrl->SetValue("");
325 return wxWindow::Show(show);
326 }
327
328 [[nodiscard]] std::string GetValue() const {
329 return m_text_ctrl->GetValue().ToStdString();
330 }
331
332private:
333 wxTextCtrl* m_text_ctrl;
334 std::function<void()> m_on_text_evt;
335 std::function<void()> m_on_close;
336};
337
339class FilterChoice : public wxChoice {
340public:
341 FilterChoice(wxWindow* parent, TtyPanel* tty_panel)
342 : wxChoice(parent, wxID_ANY), m_tty_panel(tty_panel) {
343 wxWindow::SetName(kFilterChoiceName);
344 Bind(wxEVT_CHOICE, [&](wxCommandEvent&) { OnChoice(); });
345 OnFilterListChange();
346 const int ix = wxChoice::FindString(kLabels.at("default"));
347 if (ix != wxNOT_FOUND) wxChoice::SetSelection(ix);
348 NavmsgFilter filter = filters_on_disk::Read("default.filter");
349 m_tty_panel->SetFilter(filter);
350 }
351
352 void OnFilterListChange() {
353 m_filters = NavmsgFilter::GetAllFilters();
354 int select_ix = GetSelection();
355 std::string selected;
356 if (select_ix != wxNOT_FOUND) selected = GetString(select_ix).ToStdString();
357 Clear();
358 for (auto& filter : m_filters) {
359 try {
360 Append(kLabels.at(filter.m_name));
361 } catch (std::out_of_range&) {
362 if (filter.m_description.empty())
363 Append(filter.m_name);
364 else
365 Append(filter.m_description);
366 }
367 }
368 if (!selected.empty()) {
369 int ix = FindString(selected);
370 SetSelection(ix == wxNOT_FOUND ? 0 : ix);
371 }
372 }
373
374 void OnFilterUpdate(const std::string& name) {
375 m_filters = NavmsgFilter::GetAllFilters();
376 int select_ix = GetSelection();
377 if (select_ix == wxNOT_FOUND) return;
378
379 std::string selected = GetString(select_ix).ToStdString();
380 if (selected != name) return;
381
382 NavmsgFilter filter = filters_on_disk::Read(name);
383 m_tty_panel->SetFilter(filter);
384 }
385
386 void OnApply(const std::string& name) {
387 int found = FindString(name);
388 if (found == wxNOT_FOUND) {
389 for (auto& filter : m_filters) {
390 if (filter.m_name == name) {
391 found = FindString(filter.m_description);
392 break;
393 }
394 }
395 }
396 if (found == wxNOT_FOUND) return;
397
398 SetSelection(found);
399 OnFilterUpdate(name);
400 }
401
402private:
403 // Translated labels for system filters by filter name. If not
404 // found the untranslated json description is used.
405 const std::unordered_map<std::string, std::string> kLabels = {
406 {"all-data", _("All data")},
407 {"all-nmea", _("All NMEA data")},
408 {"default", _("Default settings")},
409 {"malformed", _("Malformed messages")},
410 {"nmea-input", _("NMEA input data")},
411 {"nmea-output", _("NMEA output data")},
412 {"plugins", _("Messages to plugins")},
413 };
414
415 std::vector<NavmsgFilter> m_filters;
416 TtyPanel* m_tty_panel;
417
418 void OnChoice() {
419 wxString label = GetString(GetSelection());
420 NavmsgFilter filter = FilterByLabel(label.ToStdString());
421 m_tty_panel->SetFilter(filter);
422 }
423
424 NavmsgFilter FilterByLabel(const std::string& label) {
425 std::string name = label;
426 for (const auto& kv : kLabels) {
427 if (kv.second == label) {
428 name = kv.first;
429 break;
430 }
431 }
432 if (!name.empty()) {
433 for (auto& f : m_filters)
434 if (f.m_name == name) return f;
435 } else {
436 for (auto& f : m_filters)
437 if (f.m_description == label) return f;
438 }
439 return {};
440 }
441};
442
444class PauseResumeButton : public wxButton {
445public:
446 PauseResumeButton(wxWindow* parent, std::function<void(bool)> on_stop)
447 : wxButton(parent, wxID_ANY),
448 is_paused(true),
449 m_on_stop(std::move(on_stop)) {
450 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
451 OnClick();
452 }
453
454private:
455 bool is_paused;
456 std::function<void(bool)> m_on_stop;
457
458 void OnClick() {
459 is_paused = !is_paused;
460 m_on_stop(is_paused);
461 SetLabel(is_paused ? _("Resume") : _("Pause"));
462 }
463};
464
466class CloseButton : public wxButton {
467public:
468 CloseButton(wxWindow* parent, std::function<void()> on_close)
469 : wxButton(parent, wxID_ANY), m_on_close(std::move(on_close)) {
470 wxButton::SetLabel(_("Close"));
471 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
472 OnClick();
473 }
474
475private:
476 std::function<void()> m_on_close;
477
478 void OnClick() const { m_on_close(); }
479};
480
482class LoggingSetup : public wxDialog {
483public:
485 class ThePanel : public wxPanel {
486 public:
487 ThePanel(wxWindow* parent, SetFormatFunc set_logtype, DataLogger& logger)
488 : wxPanel(parent),
489 m_overwrite(false),
490 m_set_logtype(std::move(set_logtype)),
491 m_logger(logger),
492 kFilenameLabelId(wxWindow::NewControlId()) {
493 auto flags = wxSizerFlags(0).Border();
494
495 /* left column: Select log format. */
496 auto vdr_btn = new wxRadioButton(this, wxID_ANY, "VDR");
497 vdr_btn->Bind(wxEVT_RADIOBUTTON, [&](const wxCommandEvent& e) {
498 m_set_logtype(DataLogger::Format::kVdr, "VDR");
499 });
500 auto default_btn = new wxRadioButton(this, wxID_ANY, "Default");
501 default_btn->Bind(wxEVT_RADIOBUTTON, [&](const wxCommandEvent& e) {
502 m_set_logtype(DataLogger::Format::kDefault, _("Default"));
503 });
504 default_btn->SetValue(true);
505 auto csv_btn = new wxRadioButton(this, wxID_ANY, "CSV");
506 csv_btn->Bind(wxEVT_RADIOBUTTON, [&](const wxCommandEvent& e) {
507 m_set_logtype(DataLogger::Format::kCsv, "CSV");
508 });
509 auto left_vbox = new wxStaticBoxSizer(wxVERTICAL, this, _("Log format"));
510 left_vbox->Add(default_btn, flags.DoubleBorder());
511 left_vbox->Add(vdr_btn, flags);
512 left_vbox->Add(csv_btn, flags);
513
514 /* Right column: log file */
515 m_logger.SetLogfile(m_logger.GetDefaultLogfile());
516 auto label = new wxStaticText(this, kFilenameLabelId,
517 m_logger.GetDefaultLogfile().string());
518 auto path_btn = new wxButton(this, wxID_ANY, _("Change..."));
519 path_btn->Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnFileDialog(); });
520 auto force_box =
521 new wxCheckBox(this, wxID_ANY, _("Overwrite existing file"));
522 force_box->Bind(wxEVT_CHECKBOX, [&](const wxCommandEvent& e) {
523 m_overwrite = e.IsChecked();
524 });
525 auto right_vbox = new wxStaticBoxSizer(wxVERTICAL, this, _("Log file"));
526 right_vbox->Add(label, flags);
527 right_vbox->Add(path_btn, flags);
528 right_vbox->Add(force_box, flags);
529
530 /* Top part above buttons */
531 auto hbox = new wxBoxSizer(wxHORIZONTAL);
532 hbox->Add(left_vbox, flags);
533 hbox->Add(wxWindow::GetCharWidth() * 10, 0, 1);
534 hbox->Add(right_vbox, flags);
535 SetSizer(hbox);
536 wxWindow::Layout();
537 wxWindow::Show();
538
539 FilenameLstnr.Init(logger.OnNewLogfile, [&](const ObservedEvt& ev) {
540 GetWindowById<wxStaticText>(kFilenameLabelId)->SetLabel(ev.GetString());
541 g_dm_logfile = ev.GetString();
542 });
543 }
544
545 void OnFileDialog() const {
546 long options = wxFD_SAVE;
547 if (!m_overwrite) options |= wxFD_OVERWRITE_PROMPT;
548 wxFileDialog dlg(m_parent, _("Select logfile"),
549 m_logger.GetDefaultLogfile().parent_path().string(),
550 m_logger.GetDefaultLogfile().stem().string(),
551 m_logger.GetFileDlgTypes(), options);
552 if (dlg.ShowModal() == wxID_CANCEL) return;
553 m_logger.SetLogfile(fs::path(dlg.GetPath().ToStdString()));
554 auto file_label = GetWindowById<wxStaticText>(kFilenameLabelId);
555 file_label->SetLabel(dlg.GetPath());
556 }
557
558 bool m_overwrite;
559 SetFormatFunc m_set_logtype;
560 DataLogger& m_logger;
561 const int kFilenameLabelId;
562 obs::Listener FilenameLstnr;
563 }; // ThePanel
564
565 LoggingSetup(wxWindow* parent, SetFormatFunc set_logtype, DataLogger& logger)
566 : wxDialog(parent, wxID_ANY, _("Logging setup"), wxDefaultPosition,
567 wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) {
568 auto flags = wxSizerFlags(0).Border();
569
570 /* Buttons at bottom */
571 auto buttons = new wxStdDialogButtonSizer();
572 auto close_btn = new wxButton(this, wxID_CLOSE);
573 close_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED,
574 [&](wxCommandEvent& ev) { EndModal(0); });
575 buttons->AddButton(close_btn);
576 buttons->Realize();
577 buttons->Fit(parent);
578
579 /* Overall vbox setup */
580 auto panel = new ThePanel(this, std::move(set_logtype), logger);
581 auto vbox = new wxBoxSizer(wxVERTICAL);
582 vbox->Add(panel, flags.Expand());
583 vbox->Add(new wxStaticLine(this, wxID_ANY), flags.Expand());
584 vbox->Add(buttons, flags.Expand());
585 SetSizer(vbox);
586 wxWindow::Fit();
587 wxDialog::Show();
588 }
589 obs::Listener FilenameLstnr;
590};
591
593class TheMenu : public wxMenu {
594public:
595 enum class Id : char {
596 kNewFilter = 1, // MacOS does not want ids to be 0.
597 kEditFilter,
598 kDeleteFilter,
599 kRenameFilter,
600 kEditActiveFilter,
601 kLogSetup,
602 kViewStdColors,
603 kUserColors,
604 kClear
605 };
606
607 TheMenu(wxWindow* parent, DataLogger& logger)
608 : m_parent(parent), m_logger(logger), m_is_logging_configured(false) {
609 AppendCheckItem(static_cast<int>(Id::kViewStdColors), _("Use colors"));
610 Append(static_cast<int>(Id::kUserColors), _("Colors..."));
611 Append(static_cast<int>(Id::kClear), _("Clear..."));
612 Append(static_cast<int>(Id::kLogSetup), _("Logging..."));
613 auto filters = new wxMenu("");
614 AppendId(filters, Id::kNewFilter, _("Create new..."));
615 AppendId(filters, Id::kEditFilter, _("Edit..."));
616 AppendId(filters, Id::kDeleteFilter, _("Delete..."));
617 AppendId(filters, Id::kRenameFilter, _("Rename..."));
618 AppendSubMenu(filters, _("Filters..."));
619 if (IsUserFilter(m_filter))
620 Append(static_cast<int>(Id::kEditActiveFilter), _("Edit active filter"));
621
622 Bind(wxEVT_MENU, [&](const wxCommandEvent& ev) {
623 switch (static_cast<Id>(ev.GetId())) {
624 case Id::kLogSetup:
625 ConfigureLogging();
626 break;
627
628 case Id::kViewStdColors:
629 SetColor(static_cast<int>(Id::kViewStdColors));
630 break;
631
632 case Id::kNewFilter:
633 CreateFilterDlg(parent);
634 break;
635
636 case Id::kEditFilter:
637 EditFilterDlg(wxTheApp->GetTopWindow());
638 break;
639
640 case Id::kRenameFilter:
641 RenameFilterDlg(wxTheApp->GetTopWindow());
642 break;
643
644 case Id::kEditActiveFilter:
645 EditOneFilterDlg(wxTheApp->GetTopWindow(), m_filter);
646 break;
647
648 case Id::kDeleteFilter:
649 RemoveFilterDlg(parent);
650 break;
651
652 case Id::kUserColors:
653 UserColorsDlg(wxTheApp->GetTopWindow());
654 break;
655
656 case Id::kClear:
657 ClearLogWindow();
658 break;
659 }
660 });
661 Check(static_cast<int>(Id::kViewStdColors), true);
662 }
663
664 void ClearLogWindow() {
665 auto* w = wxWindow::FindWindowByName("TtyScroll");
666 auto tty_scroll = dynamic_cast<TtyScroll*>(w);
667 if (tty_scroll) tty_scroll->Clear();
668 }
669
670 void SetFilterName(const std::string& filter) {
671 int id = static_cast<int>(Id::kEditActiveFilter);
672 if (FindItem(id)) Delete(id);
673 if (IsUserFilter(filter)) Append(id, _("Edit active filter"));
674 m_filter = filter;
675 }
676
677 void ConfigureLogging() {
678 LoggingSetup dlg(
679 m_parent,
680 [&](DataLogger::Format f, const std::string& s) { SetLogFormat(f, s); },
681 m_logger);
682 dlg.ShowModal();
683 m_is_logging_configured = true;
684 auto monitor = wxWindow::FindWindowByName(kDataMonitorWindowName);
685 assert(monitor);
686 monitor->Layout();
687 }
688
689 bool IsLoggingConfigured() const { return m_is_logging_configured; }
690
691private:
692 static wxMenuItem* AppendId(wxMenu* root, Id id, const wxString& label) {
693 return root->Append(static_cast<int>(id), label);
694 }
695
696 void SetLogFormat(DataLogger::Format format, const std::string& label) const {
697 m_logger.SetFormat(format);
698 std::string extension =
699 format == DataLogger::Format::kDefault ? ".log" : ".csv";
700 fs::path path = m_logger.GetLogfile();
701 path = path.parent_path() / (path.stem().string() + extension);
702 m_logger.SetLogfile(path);
703 }
704
705 void SetColor(int id) const {
706 auto* w = wxWindow::FindWindowByName("TtyScroll");
707 auto tty_scroll = dynamic_cast<TtyScroll*>(w);
708 if (!tty_scroll) return;
709
710 wxMenuItem* item = FindItem(id);
711 if (!item) return;
712 if (item->IsCheck() && item->IsChecked())
713 tty_scroll->SetColors(std::make_unique<StdColorsByState>());
714 else
715 tty_scroll->SetColors(
716 std::make_unique<NoColorsByState>(tty_scroll->GetForegroundColour()));
717 }
718
719 wxWindow* m_parent;
720 DataLogger& m_logger;
721 std::string m_filter;
722 bool m_is_logging_configured;
723};
724
726class LogButton : public wxButton {
727public:
728 LogButton(wxWindow* parent, DataLogger& logger, TheMenu& menu)
729 : wxButton(parent, wxID_ANY),
730 is_logging(true),
731 m_is_inited(false),
732 m_logger(logger),
733 m_menu(menu) {
734 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
735 OnClick(true);
736 UpdateTooltip();
737 }
738
739 void UpdateTooltip() {
740 if (is_logging)
741 SetToolTip(_("Click to stop logging"));
742 else
743 SetToolTip(_("Click to start logging"));
744 }
745
746private:
747 bool is_logging;
748 bool m_is_inited;
749 DataLogger& m_logger;
750 TheMenu& m_menu;
751
752 void OnClick(bool ctor = false) {
753 if (!m_is_inited && !ctor && !m_menu.IsLoggingConfigured()) {
754 m_menu.ConfigureLogging();
755 m_is_inited = true;
756 }
757 is_logging = !is_logging;
758 SetLabel(is_logging ? _("Stop logging") : _("Start logging"));
759 UpdateTooltip();
760 m_logger.SetLogging(is_logging);
761 }
762};
763
766public:
767 explicit CopyClipboardButton(wxWindow* parent) : SvgButton(parent) {
768 LoadIcon(kCopyIconSvg);
769 SetToolTip(_("Copy to clipboard"));
770 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) {
771 auto* tty_scroll =
772 dynamic_cast<TtyScroll*>(wxWindow::FindWindowByName("TtyScroll"));
773 if (tty_scroll) tty_scroll->CopyToClipboard();
774 });
775 }
776};
777
779class FilterButton : public SvgButton {
780public:
781 FilterButton(wxWindow* parent, wxWindow* quick_filter)
782 : SvgButton(parent), m_quick_filter(quick_filter), m_show_filter(true) {
783 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
784 LoadIcon(kFunnelSvg);
785 OnClick();
786 }
787
788private:
789 wxWindow* m_quick_filter;
790 bool m_show_filter;
791
792 void OnClick() {
793 m_quick_filter->Show(m_show_filter);
794 SetToolTip(_("Open quick filter"));
795 GetGrandParent()->Layout();
796 m_quick_filter->SetFocus();
797 }
798};
799
801class MenuButton : public SvgButton {
802public:
803 MenuButton(wxWindow* parent, TheMenu& menu,
804 std::function<std::string()> get_current_filter)
805 : SvgButton(parent),
806 m_menu(menu),
807 m_get_current_filter(std::move(get_current_filter)) {
808 LoadIcon(kMenuSvg);
809 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
810 SetToolTip(_("Open menu"));
811 }
812
813private:
814 TheMenu& m_menu;
815 std::function<std::string()> m_get_current_filter;
816
817 void OnClick() {
818 m_menu.SetFilterName(m_get_current_filter());
819 PopupMenu(&m_menu);
820 }
821};
822
824class StatusLine : public wxPanel {
825public:
826 StatusLine(wxWindow* parent, wxWindow* quick_filter, TtyPanel* tty_panel,
827 std::function<void(bool)> on_stop,
828 const std::function<void()>& on_hide, DataLogger& logger)
829 : wxPanel(parent),
830 m_is_resized(false),
831 m_filter_choice(new FilterChoice(this, tty_panel)),
832 m_menu(this, logger),
833 m_log_button(new LogButton(this, logger, m_menu)) {
834 // Add a containing sizer for labels, so they can be aligned vertically
835 auto filter_label_box = new wxBoxSizer(wxVERTICAL);
836 filter_label_box->Add(new wxStaticText(this, wxID_ANY, _("Filter")));
837
838 auto flags = wxSizerFlags(0).Border();
839 auto wbox = new wxWrapSizer(wxHORIZONTAL);
840 wbox->Add(m_log_button, flags);
841 // Stretching horizontal space. Does not work with a WrapSizer, known
842 // wx bug. Left in place if it becomes fixed.
843 wbox->Add(wxWindow::GetCharWidth() * 5, 0, 1);
844 wbox->Add(filter_label_box, flags.Align(wxALIGN_CENTER_VERTICAL));
845 wbox->Add(m_filter_choice, flags);
846 wbox->Add(new PauseResumeButton(this, std::move(on_stop)), flags);
847 wbox->Add(new FilterButton(this, quick_filter), flags);
848 auto get_current_filter = [&] {
849 return m_filter_choice->GetStringSelection().ToStdString();
850 };
851 wbox->Add(new CopyClipboardButton(this), flags);
852 wbox->Add(new MenuButton(this, m_menu, get_current_filter), flags);
853#ifdef ANDROID
854 wbox->Add(new CloseButton(this, std::move(on_hide)), flags);
855#endif
856 SetSizer(wbox);
857 wxWindow::Layout();
858 wxWindow::Show();
859
860 Bind(wxEVT_SIZE, [&](wxSizeEvent& ev) {
861 m_is_resized = true;
862 ev.Skip();
863 });
864 Bind(wxEVT_RIGHT_UP, [&](wxMouseEvent& ev) {
865 m_menu.SetFilterName(m_filter_choice->GetStringSelection().ToStdString());
866 PopupMenu(&m_menu);
867 });
868 }
869
870 void OnContextClick() {
871 m_menu.SetFilterName(m_filter_choice->GetStringSelection().ToStdString());
872 PopupMenu(&m_menu);
873 }
874
875protected:
876 // Make sure the initial size is sane, don't meddle when user resizes
877 // dialog
878 [[nodiscard]] wxSize DoGetBestClientSize() const override {
879 if (m_is_resized)
880 return {-1, -1};
881 else
882 return {85 * GetCharWidth(), 5 * GetCharHeight() / 2};
883 }
884
885private:
886 bool m_is_resized;
887 wxChoice* m_filter_choice;
888 TheMenu m_menu;
889 wxButton* m_log_button;
890};
891
892DataLogger::DataLogger(wxWindow* parent, const fs::path& path)
893 : m_parent(parent),
894 m_path(path),
895 m_stream(path, std::ios_base::app),
896 m_is_logging(false),
897 m_format(Format::kDefault),
898 m_log_start(NavmsgClock::now()) {}
899
900DataLogger::DataLogger(wxWindow* parent) : DataLogger(parent, NullLogfile()) {}
901
902void DataLogger::SetLogging(bool logging) { m_is_logging = logging; }
903
904void DataLogger::SetLogfile(const fs::path& path) {
905 const auto now = std::chrono::system_clock::now();
906 const std::time_t t_c = std::chrono::system_clock::to_time_t(now);
907 m_path = path;
908 std::stringstream ss;
909 ss << "# timestamp_format: EPOCH_MILLIS\n";
910 ss << "# Created at: " << std::ctime(&t_c) << " \n";
911 ss << "received_at,protocol,source,msg_type,raw_data\n";
912 m_header = ss.str();
913 OnNewLogfile.Notify(path.string());
914}
915
916void DataLogger::SetFormat(DataLogger::Format format) { m_format = format; }
917
918fs::path DataLogger::GetDefaultLogfile() {
919 if (!g_dm_logfile.empty()) return g_dm_logfile.ToStdString();
920 if (m_path.stem() != NullLogfile().stem()) return m_path;
921 fs::path path(g_BasePlatform->GetHomeDir().ToStdString());
922 path /= "monitor";
923 path += (m_format == Format::kDefault ? ".log" : ".csv");
924 return path;
925}
926
927std::string DataLogger::GetFileDlgTypes() const {
928 if (m_format == Format::kDefault)
929 return _("Log file (*.log)|*.log");
930 else
931 return _("Spreadsheet csv file(*.csv)|*.csv");
932}
933
934void DataLogger::Add(const Logline& ll) {
935 if (!m_is_logging || !ll.navmsg) return;
936 if (!m_header.empty()) {
937 m_stream = std::ofstream(m_path);
938 m_stream << m_header;
939 m_header.clear();
940 }
941 if (m_format == Format::kVdr && ll.navmsg->to_vdr().empty()) return;
942 if (m_format == DataLogger::Format::kVdr)
943 AddVdrLogline(ll, m_stream);
944 else
945 AddStdLogline(ll, m_stream,
946 m_format == DataLogger::Format::kCsv ? '|' : ' ',
947 m_log_start);
948}
949
950DataMonitor::DataMonitor(wxWindow* parent)
951 : wxFrame(parent, wxID_ANY, _("Data Monitor"), wxPoint(0, 0), wxDefaultSize,
952 wxDEFAULT_FRAME_STYLE | wxFRAME_FLOAT_ON_PARENT,
953 kDataMonitorWindowName),
954 m_monitor_src([&](const std::shared_ptr<const NavMsg>& navmsg) {
956 }),
957 m_quick_filter(nullptr),
958 m_logger(parent) {
959 auto vbox = new wxBoxSizer(wxVERTICAL);
960 auto tty_panel = new TtyPanel(this, 12);
961 vbox->Add(tty_panel, wxSizerFlags(1).Expand().Border());
962 vbox->Add(new wxStaticLine(this), wxSizerFlags().Expand().Border());
963
964 auto on_quick_filter_evt = [&, tty_panel] {
965 auto* quick_filter = dynamic_cast<QuickFilterPanel*>(m_quick_filter);
966 assert(quick_filter);
967 std::string value = quick_filter->GetValue();
968 tty_panel->SetQuickFilter(value);
969 };
970 auto on_dismiss = [&] {
971 m_quick_filter->Hide();
972 Layout();
973 };
974 m_quick_filter = new QuickFilterPanel(this, on_quick_filter_evt, on_dismiss);
975 vbox->Add(m_quick_filter, wxSizerFlags().Expand());
976
977 auto on_stop = [&, tty_panel](bool stop) { tty_panel->OnStop(stop); };
978 auto on_close = [&, this]() { this->OnHide(); };
979 auto status_line = new StatusLine(this, m_quick_filter, tty_panel, on_stop,
980 on_close, m_logger);
981 vbox->Add(status_line, wxSizerFlags().Expand());
982 SetSizer(vbox);
983 wxWindow::Fit();
984 wxWindow::Hide();
985
986 m_quick_filter->Bind(wxEVT_TEXT, [&, tty_panel](wxCommandEvent&) {
987 tty_panel->SetQuickFilter(GetLabel().ToStdString());
988 });
989 m_quick_filter->Hide();
990 tty_panel->SetOnRightClick(
991 [&, status_line] { status_line->OnContextClick(); });
992
993 Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& ev) { Hide(); });
994 Bind(wxEVT_RIGHT_UP, [status_line](wxMouseEvent& ev) {
995 status_line->OnContextClick();
996 ev.Skip();
997 });
998 m_filter_list_lstnr.Init(FilterEvents::GetInstance().filter_list_change,
999 [&](ObservedEvt&) { OnFilterListChange(); });
1000 m_filter_update_lstnr.Init(FilterEvents::GetInstance().filter_update,
1001 [&](const ObservedEvt& ev) {
1002 OnFilterUpdate(ev.GetString().ToStdString());
1003 });
1004
1005 m_filter_apply_lstnr.Init(FilterEvents::GetInstance().filter_apply,
1006 [&](const ObservedEvt& ev) {
1007 OnFilterApply(ev.GetString().ToStdString());
1008 });
1009}
1010
1011void DataMonitor::Add(const Logline& ll) {
1013 m_logger.Add(ll);
1014}
1015
1017 wxWindow* w = wxWindow::FindWindowByName("TtyPanel");
1018 assert(w && "No TtyPanel found");
1019 return w->IsShownOnScreen();
1020}
1021
1022void DataMonitor::OnFilterListChange() {
1023 wxWindow* w = wxWindow::FindWindowByName(kFilterChoiceName);
1024 if (!w) return;
1025 auto filter_choice = dynamic_cast<FilterChoice*>(w);
1026 assert(filter_choice && "Wrong FilterChoice type (!)");
1027 filter_choice->OnFilterListChange();
1028}
1029
1030void DataMonitor::OnFilterUpdate(const std::string& name) const {
1031 if (name != m_current_filter) return;
1032 wxWindow* w = wxWindow::FindWindowByName("TtyScroll");
1033 if (!w) return;
1034 auto tty_scroll = dynamic_cast<TtyScroll*>(w);
1035 assert(tty_scroll && "Wrong TtyScroll type (!)");
1036 tty_scroll->SetFilter(filters_on_disk::Read(name));
1037}
1038
1039void DataMonitor::OnFilterApply(const std::string& name) {
1040 wxWindow* w = wxWindow::FindWindowByName(kFilterChoiceName);
1041 if (!w) return;
1042 auto filter_choice = dynamic_cast<FilterChoice*>(w);
1043 assert(filter_choice && "Wrong FilterChoice type (!)");
1044 m_current_filter = name;
1045 filter_choice->OnApply(name);
1046}
1047
1048void DataMonitor::OnHide() { Hide(); }
1049
1050#pragma clang diagnostic pop
BasePlatform * g_BasePlatform
points to g_platform, handles brain-dead MS linker.
Basic platform specific support utilities without GUI deps.
Button to hide data monitor, used only on Android.
Copy to clipboard button.
Clickable crossmark icon window.
Internal helper class.
obs::EventVar OnNewLogfile
Notified with new path on filename change.
void Add(const Logline &ll) override
Add an input line to log output.
bool IsVisible() const override
Return true if log is visible i.e., if it's any point using Add().
Button opening the filter dialog.
Offer user to select current filter.
Button to start/stop logging.
Top part above buttons.
Log setup window invoked from menu "Logging" item.
Button invoking the popup menu.
Actual data sent between application and transport layer.
static std::vector< NavmsgFilter > GetAllFilters()
Return list of all filters, system + user defined.
NMEA Log Interface.
Definition nmea_log.h:70
Custom event class for OpenCPN's notification system.
Button to stop/resume messages in main log window.
The quick filter above the status line, invoked by funnel button.
Overall bottom status line.
A button capable of loading an svg image.
Definition svg_button.h:46
void LoadIcon(const char *svg)
Load an svg icon available in memory.
The monitor popup menu.
Main window, a rolling log of messages.
void Add(const Logline &ll) override
Add a formatted string to log output.
static void AddIfExists(const Logline &ll)
Invoke Add(s) for possibly existing instance.
bool IsVisible() const override
Return true if log is visible i.e., if it's any point using Add().
Scrolled TTY-like window for logging, etc.
Definition tty_scroll.h:79
void SetQuickFilter(const std::string s)
Apply a quick filter directly matched against lines.
Definition tty_scroll.h:113
virtual void Clear()
Clear the log window.
void Pause(bool pause)
Set the window to ignore Add() or not depending on pause.
Definition tty_scroll.h:101
void SetFilter(const NavmsgFilter &filter)
Apply a display filter.
Definition tty_scroll.h:107
virtual void Add(const Logline &line)
Add a line to bottom of window, typically discarding top-most line.
void CopyToClipboard() const
Copy message contents to clipboard.
void Notify() override
Notify all listeners, no data supplied.
Definition evtvar.h:83
wxString g_dm_logfile
Last Data Monitor log file.
Global variables stored in configuration file.
T * GetWindowById(int id)
Return window with given id (which must exist) cast to T*.
New NMEA Debugger successor main window.
Provide a data stream of input messages for the Data Monitor.
Dialogs handing user defined filters.
std::vector< std::string > List(bool include_system)
Return list of filters, possibly including also the system ones.
Data Monitor filter storage routines.
Hooks into gui available in model.
Data monitor filter definitions.
Basic DataMonitor logging interface: LogLine (reflects a line in the log) and NmeaLog,...
Item in the log window.
Definition nmea_log.h:32
wxBitmap LoadSVG(const wxString filename, const unsigned int width, const unsigned int height, wxBitmap *default_bitmap, bool use_cache)
Load SVG file and return it's bitmap representation of requested size In case file can't be loaded an...
Definition svg_utils.cpp:59
SVG utilities.
Scrolled tty like window for logging.