OpenCPN Partial API docs
Loading...
Searching...
No Matches
data_monitor.cpp
1#include <chrono>
2#include <fstream>
3#include <sstream>
4
5#include <wx/app.h>
6#include <wx/button.h>
7#include <wx/choice.h>
8#include <wx/filedlg.h>
9#include <wx/menu.h>
10#include <wx/panel.h>
11#include <wx/platinfo.h>
12#include <wx/sizer.h>
13#include <wx/statline.h>
14#include <wx/stattext.h>
15#include <wx/translation.h>
16#include <wx/wrapsizer.h>
17
18#ifdef __ANDROID__
19#include "androidUTIL.h"
20#endif
21
22#include "model/base_platform.h"
25#include "model/navmsg_filter.h"
26#include "model/nmea_log.h"
27#include "model/gui.h"
28
29#include "data_monitor.h"
30#include "std_filesystem.h"
31#include "svg_button.h"
32#include "svg_icons.h"
33#include "tty_scroll.h"
34#include "filter_dlg.h"
35
36#pragma clang diagnostic push
37#pragma ide diagnostic ignored "UnreachableCode"
38
39// Make _() return std::string instead of wxString;
40#undef _
41#if wxCHECK_VERSION(3, 2, 0)
42#define _(s) wxGetTranslation(wxASCII_STR(s)).ToStdString()
43#else
44#define _(s) wxGetTranslation((s)).ToStdString()
45#endif
46
47using SetFormatFunc = std::function<void(DataLogger::Format, std::string)>;
48
49extern BasePlatform* g_BasePlatform;
50
52template <typename T>
53T* GetWindowById(int id) {
54 return dynamic_cast<T*>(wxWindow::FindWindowById(id));
55};
56
57static const char* const kFilterChoiceName = "FilterChoiceWindow";
58
59// clang-format: off
60static const std::unordered_map<NavAddr::Bus, std::string> kSourceByBus = {
61 {NavAddr::Bus::N0183, "NMEA0183"},
62 {NavAddr::Bus::N2000, "NMEA2000"},
63 {NavAddr::Bus::Signalk, "SignalK"}}; // clang-format: on
64
66static bool IsUserFilter(const std::string& filter_name) {
67 std::vector<std::string> filters = filters_on_disk::List();
68 auto found = std::find(filters.begin(), filters.end(), filter_name);
69 if (found != filters.end()) return true;
70 for (auto& f : filters) {
72 if (nf.m_description == filter_name) return true;
73 }
74 return false;
75};
76
78static std::string TimeStamp(const NavmsgTimePoint& when,
79 const NavmsgTimePoint& since) {
80 using namespace std::chrono;
81 using namespace std;
82
83 auto duration = when - since;
84 std::stringstream ss;
85 auto hrs = duration_cast<hours>(duration) % 24;
86 duration -= duration_cast<hours>(duration) / 24;
87 auto mins = duration_cast<minutes>(duration) % 60;
88 duration -= duration_cast<minutes>(duration) / 60;
89 auto secs = duration_cast<seconds>(duration) % 60;
90 duration -= duration_cast<seconds>(duration) / 60;
91 auto msecs = duration_cast<milliseconds>(duration);
92 ss << setw(2) << setfill('0') << hrs.count() << ":" << setw(2) << mins.count()
93 << ":" << setw(2) << secs.count() << "." << setw(3)
94 << msecs.count() % 1000;
95 return ss.str();
96}
97
104static std::string VdrQuote(const std::string& arg) {
105 auto static const npos = std::string::npos;
106 if (arg.find(',') == npos && arg.find('"') == npos) return arg;
107 std::string s;
108 for (auto c : arg) {
109 if (c == '"')
110 s += "\"\"";
111 else
112 s += c;
113 }
114 return "\"" + s + "\"";
115}
116
121static void AddVdrLogline(const Logline& ll, std::ostream& stream) {
122 if (kSourceByBus.find(ll.navmsg->bus) == kSourceByBus.end()) return;
123
124 using namespace std::chrono;
125 auto now = steady_clock::now();
126 auto ms = duration_cast<milliseconds>(now.time_since_epoch()).count();
127 stream << ms << ",";
128
129 stream << kSourceByBus.at(ll.navmsg->bus) << ",";
130 stream << ll.navmsg->source->iface << ",";
131 switch (ll.navmsg->bus) {
132 case NavAddr::Bus::N0183: {
133 auto msg0183 = std::dynamic_pointer_cast<const Nmea0183Msg>(ll.navmsg);
134 stream << msg0183->talker << msg0183->type << ",";
135 } break;
136 case NavAddr::Bus::N2000: {
137 auto msg2000 = std::dynamic_pointer_cast<const Nmea2000Msg>(ll.navmsg);
138 stream << msg2000->PGN.to_string() << ",";
139 } break;
140 case NavAddr::Bus::Signalk: {
141 auto msgSignalK = std::dynamic_pointer_cast<const SignalkMsg>(ll.navmsg);
142 stream << "\"" << msgSignalK->context_self << "\",";
143 } break;
144 default:
145 assert(false && "Illegal message type");
146 };
147 stream << VdrQuote(ll.navmsg->to_vdr()) << "\n";
148}
149
151static void AddStdLogline(const Logline& ll, std::ostream& stream, char fs,
152 const NavmsgTimePoint log_start) {
153 if (!ll.navmsg) return;
154 wxString ws;
155 ws << TimeStamp(ll.navmsg->created_at, log_start) << fs;
156 if (ll.state.direction == NavmsgStatus::Direction::kOutput)
157 ws << kUtfRightArrow << fs;
158 else if (ll.state.direction == NavmsgStatus::Direction::kInput)
159 ws << kUtfLeftwardsArrowToBar << fs;
160 else if (ll.state.direction == NavmsgStatus::Direction::kInternal)
161 ws << kUtfLeftRightArrow << fs;
162 else
163 ws << kUtfLeftArrow << fs;
164 if (ll.state.status != NavmsgStatus::State::kOk)
165 ws << kUtfMultiplicationX << fs;
166 else if (ll.state.accepted == NavmsgStatus::Accepted::kFilteredNoOutput)
167 ws << kUtfFallingDiagonal << fs;
168 else if (ll.state.accepted == NavmsgStatus::Accepted::kFilteredDropped)
169 ws << kUtfCircledDivisionSlash << fs;
170 else
171 ws << kUtfCheckMark << fs;
172
173 ws << ll.navmsg->source->iface << fs;
174 ws << NavAddr::BusToString(ll.navmsg->bus) << fs;
175 if (ll.state.status != NavmsgStatus::State::kOk)
176 ws << (!ll.error_msg.empty() ? ll.error_msg : "Unknown errror");
177 else
178 ws << "ok";
179 ws << fs << ll.message << "\n";
180 stream << ws;
181}
182
184class TtyPanel : public wxPanel, public NmeaLog {
185public:
186 TtyPanel(wxWindow* parent, size_t lines)
187 : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
188 wxTAB_TRAVERSAL, "TtyPanel"),
189 m_tty_scroll(nullptr),
190 m_filter(this, wxID_ANY),
191 m_lines(lines),
192 m_on_right_click([] {}) {
193 auto vbox = new wxBoxSizer(wxVERTICAL);
194 m_tty_scroll = new TtyScroll(this, m_lines);
195 m_tty_scroll->Bind(wxEVT_RIGHT_UP,
196 [&](wxMouseEvent&) { m_on_right_click(); });
197 vbox->Add(m_tty_scroll, wxSizerFlags(1).Expand().Border());
198 m_filter.Hide();
199 SetSizer(vbox);
200 Fit();
201 }
202
203 void Add(const Logline& ll) override { m_tty_scroll->Add(ll); }
204
205 bool IsVisible() const override { return IsShownOnScreen(); }
206
207 void OnStop(bool stop) {
208 m_tty_scroll->Pause(stop);
209 if (stop)
210 m_tty_scroll->ShowScrollbars(wxSHOW_SB_DEFAULT, wxSHOW_SB_DEFAULT);
211 else
212 m_tty_scroll->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
213 }
214
215 void SetFilter(const NavmsgFilter& f) { m_tty_scroll->SetFilter(f); };
216
217 void SetQuickFilter(const std::string& filter) {
218 m_tty_scroll->SetQuickFilter(filter);
219 }
220
221 void SetOnRightClick(std::function<void()> f) {
222 m_on_right_click = std::move(f);
223 }
224
226 static void AddIfExists(const Logline& ll) {
227 auto window = wxWindow::FindWindowByName("TtyPanel");
228 if (!window) return;
229 auto tty_panel = dynamic_cast<TtyPanel*>(window);
230 if (tty_panel) tty_panel->Add(ll);
231 }
232
233protected:
234 wxSize DoGetBestClientSize() const override {
235 return wxSize(-1, m_lines * GetCharHeight());
236 }
237
238private:
239 TtyScroll* m_tty_scroll;
240 wxTextCtrl m_filter;
241 size_t m_lines;
242 std::function<void()> m_on_right_click;
243};
244
246class QuickFilterPanel : public wxPanel {
247public:
248 QuickFilterPanel(wxWindow* parent, std::function<void()> on_text_evt)
249 : wxPanel(parent),
250 m_text_ctrl(new wxTextCtrl(this, wxID_ANY)),
251 m_on_text_evt(std::move(on_text_evt)) {
252 auto hbox = new wxBoxSizer(wxHORIZONTAL);
253 auto flags = wxSizerFlags(0).Border();
254 auto label_box = new wxBoxSizer(wxVERTICAL);
255 label_box->Add(new wxStaticText(this, wxID_ANY, _("Quick filter:")));
256 hbox->Add(label_box, flags.Align(wxALIGN_CENTER_VERTICAL));
257 hbox->Add(m_text_ctrl, flags);
258 SetSizer(hbox);
259 Fit();
260 Show();
261 m_text_ctrl->Bind(wxEVT_TEXT, [&](wxCommandEvent&) { m_on_text_evt(); });
262 }
263
264 bool Show(bool show = true) override {
265 if (!show) m_text_ctrl->SetValue("");
266 return wxWindow::Show(show);
267 }
268
269 std::string GetValue() { return m_text_ctrl->GetValue().ToStdString(); }
270
271private:
272 wxTextCtrl* m_text_ctrl;
273 std::function<void()> m_on_text_evt;
274};
275
277class LogButton : public wxButton {
278public:
279 LogButton(wxWindow* parent, DataLogger& logger)
280 : wxButton(parent, wxID_ANY), is_logging(true), m_logger(logger) {
281 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
282 OnClick();
283 Disable();
284 Enable(false);
285 UpdateTooltip();
286 }
287
288 void UpdateTooltip() {
289 if (!IsThisEnabled())
290 SetToolTip(_("Set log file using menu to enable"));
291 else if (is_logging)
292 SetToolTip(_("Click to stop logging"));
293 else
294 SetToolTip(_("Click to start logging"));
295 }
296
297 bool Enable(bool enable) override {
298 bool result = wxWindow::Enable(enable);
299 UpdateTooltip();
300 return result;
301 }
302
303private:
304 bool is_logging;
305 DataLogger& m_logger;
306
307 void OnClick() {
308 is_logging = !is_logging;
309 SetLabel(is_logging ? _("Stop") : _("Start"));
310 UpdateTooltip();
311 m_logger.SetLogging(is_logging);
312 }
313};
314
316class FilterChoice : public wxChoice {
317public:
318 FilterChoice(wxWindow* parent, TtyPanel* tty_panel)
319 : wxChoice(parent, wxID_ANY), m_tty_panel(tty_panel) {
320 SetName(kFilterChoiceName);
321 Bind(wxEVT_CHOICE, [&](wxCommandEvent&) { OnChoice(); });
322 OnFilterListChange();
323 int ix = FindString("Default settings");
324 if (ix != wxNOT_FOUND) SetSelection(ix);
325 NavmsgFilter filter = filters_on_disk::Read("default.filter");
326 m_tty_panel->SetFilter(filter);
327 }
328
329 void OnFilterListChange() {
330 m_filters = NavmsgFilter::GetAllFilters();
331 int select_ix = GetSelection();
332 std::string selected;
333 if (select_ix != wxNOT_FOUND) selected = GetString(select_ix).ToStdString();
334 Clear();
335 for (auto& filter : m_filters) {
336 try {
337 Append(kLabels.at(filter.m_name));
338 } catch (std::out_of_range&) {
339 if (filter.m_description.empty())
340 Append(filter.m_name);
341 else
342 Append(filter.m_description);
343 }
344 }
345 if (!selected.empty()) {
346 int ix = FindString(selected);
347 SetSelection(ix == wxNOT_FOUND ? 0 : ix);
348 }
349 }
350
351 void OnFilterUpdate(const std::string& name) {
352 m_filters = NavmsgFilter::GetAllFilters();
353 int select_ix = GetSelection();
354 if (select_ix == wxNOT_FOUND) return;
355
356 std::string selected = GetString(select_ix).ToStdString();
357 if (selected != name) return;
358
359 NavmsgFilter filter = filters_on_disk::Read(name);
360 m_tty_panel->SetFilter(filter);
361 }
362
363 void OnApply(const std::string& name) {
364 int found = FindString(name);
365 if (found == wxNOT_FOUND) {
366 for (auto& filter : m_filters) {
367 if (filter.m_name == name) {
368 found = FindString(filter.m_description);
369 break;
370 }
371 }
372 }
373 if (found == wxNOT_FOUND) return;
374
375 SetSelection(found);
376 OnFilterUpdate(name);
377 }
378
379private:
380 // Translated labels for system filters by filter name. If not
381 // found the untranslated json description is used.
382 const std::unordered_map<std::string, std::string> kLabels = {
383 {"all-data", _("All data")},
384 {"all-nmea", _("All NMEA data")},
385 {"malformed", _("Malformed messages")},
386 {"nmea-input", _("NMEA input data")},
387 {"nmea-output", _("NMEA output data")},
388 {"plugins", _("Messages to plugins")},
389 };
390
391 std::vector<NavmsgFilter> m_filters;
392 TtyPanel* m_tty_panel;
393
394 void OnChoice() {
395 wxString label = GetString(GetSelection());
396 NavmsgFilter filter = FilterByLabel(label.ToStdString());
397 m_tty_panel->SetFilter(filter);
398 }
399
400 NavmsgFilter FilterByLabel(const std::string& label) {
401 std::string name;
402 for (const auto& kv : kLabels) {
403 if (kv.second == label) {
404 name = kv.first;
405 break;
406 }
407 }
408 if (!name.empty()) {
409 for (auto& f : m_filters)
410 if (f.m_name == name) return f;
411 } else {
412 for (auto& f : m_filters)
413 if (f.m_description == label) return f;
414 }
415 return NavmsgFilter();
416 }
417};
418
420class PauseResumeButton : public wxButton {
421public:
422 PauseResumeButton(wxWindow* parent, std::function<void(bool)> on_stop)
423 : wxButton(parent, wxID_ANY),
424 is_paused(true),
425 m_on_stop(std::move(on_stop)) {
426 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
427 OnClick();
428 }
429
430private:
431 bool is_paused;
432 std::function<void(bool)> m_on_stop;
433
434 void OnClick() {
435 is_paused = !is_paused;
436 m_on_stop(is_paused);
437 SetLabel(is_paused ? _("Resume") : _("Pause"));
438 }
439};
440
442class LoggingSetup : public wxDialog {
443public:
445 class ThePanel : public wxPanel {
446 public:
447 ThePanel(wxWindow* parent, SetFormatFunc set_logtype, DataLogger& logger)
448 : wxPanel(parent),
449 m_overwrite(false),
450 m_set_logtype(set_logtype),
451 m_logger(logger),
452 kFilenameLabelId(wxWindow::NewControlId()) {
453 auto flags = wxSizerFlags(0).Border();
454
455 /* left column: Select log format. */
456 auto vdr_btn = new wxRadioButton(this, wxID_ANY, "VDR");
457 vdr_btn->Bind(wxEVT_RADIOBUTTON, [&](wxCommandEvent e) {
458 m_set_logtype(DataLogger::Format::kVdr, "VDR");
459 });
460 auto default_btn = new wxRadioButton(this, wxID_ANY, "Default");
461 default_btn->Bind(wxEVT_RADIOBUTTON, [&](wxCommandEvent e) {
462 m_set_logtype(DataLogger::Format::kDefault, _("Default"));
463 });
464 default_btn->SetValue(true);
465 auto csv_btn = new wxRadioButton(this, wxID_ANY, "CSV");
466 csv_btn->Bind(wxEVT_RADIOBUTTON, [&](wxCommandEvent e) {
467 m_set_logtype(DataLogger::Format::kCsv, "CSV");
468 });
469 auto left_vbox = new wxStaticBoxSizer(wxVERTICAL, this, _("Log format"));
470 left_vbox->Add(default_btn, flags.DoubleBorder());
471 left_vbox->Add(vdr_btn, flags);
472 left_vbox->Add(csv_btn, flags);
473
474 /* Right column: log file */
475 m_logger.SetLogfile(m_logger.GetDefaultLogfile());
476 auto label = new wxStaticText(this, kFilenameLabelId,
477 m_logger.GetDefaultLogfile().string());
478 auto path_btn = new wxButton(this, wxID_ANY, _("Change..."));
479 path_btn->Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnFileDialog(); });
480 auto force_box =
481 new wxCheckBox(this, wxID_ANY, _("Overwrite existing file"));
482 force_box->Bind(wxEVT_CHECKBOX,
483 [&](wxCommandEvent& e) { m_overwrite = e.IsChecked(); });
484 auto right_vbox = new wxStaticBoxSizer(wxVERTICAL, this, _("Log file"));
485 right_vbox->Add(label, flags);
486 right_vbox->Add(path_btn, flags);
487 right_vbox->Add(force_box, flags);
488
489 /* Top part above buttons */
490 auto hbox = new wxBoxSizer(wxHORIZONTAL);
491 hbox->Add(left_vbox, flags);
492 hbox->Add(GetCharWidth() * 10, 0, 1);
493 hbox->Add(right_vbox, flags);
494 SetSizer(hbox);
495 Layout();
496 Show();
497
498 m_set_logtype(DataLogger::Format::kDefault, _("Default"));
499 FilenameLstnr.Init(logger.OnNewLogfile, [&](ObservedEvt& ev) {
500 GetWindowById<wxStaticText>(kFilenameLabelId)->SetLabel(ev.GetString());
501 });
502 }
503
504 void OnFileDialog() {
505 long options = wxFD_SAVE;
506 if (!m_overwrite) options |= wxFD_OVERWRITE_PROMPT;
507 wxFileDialog dlg(m_parent, _("Select logfile"),
508 m_logger.GetDefaultLogfile().parent_path().string(),
509 m_logger.GetDefaultLogfile().stem().string(),
510 m_logger.GetFileDlgTypes(), options);
511 if (dlg.ShowModal() == wxID_CANCEL) return;
512 m_logger.SetLogfile(fs::path(dlg.GetPath().ToStdString()));
513 auto file_label = GetWindowById<wxStaticText>(kFilenameLabelId);
514 file_label->SetLabel(dlg.GetPath());
515 }
516
517 bool m_overwrite;
518 SetFormatFunc m_set_logtype;
519 DataLogger& m_logger;
520 const int kFilenameLabelId;
521 ObsListener FilenameLstnr;
522 }; // ThePanel
523
524 LoggingSetup(wxWindow* parent, SetFormatFunc set_logtype, DataLogger& logger)
525 : wxDialog(parent, wxID_ANY, _("Logging setup"), wxDefaultPosition,
526 wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) {
527 auto flags = wxSizerFlags(0).Border();
528
529 /* Buttons at bottom */
530 auto buttons = new wxStdDialogButtonSizer();
531 auto close_btn = new wxButton(this, wxID_CLOSE);
532 close_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED,
533 [&](wxCommandEvent& ev) { Destroy(); });
534 buttons->AddButton(close_btn);
535 buttons->Realize();
536 buttons->Fit(parent);
537
538 /* Overall vbox setup */
539 auto panel = new ThePanel(this, set_logtype, logger);
540 auto vbox = new wxBoxSizer(wxVERTICAL);
541 vbox->Add(panel, flags.Expand());
542 vbox->Add(new wxStaticLine(this, wxID_ANY), flags.Expand());
543 vbox->Add(buttons, flags.Expand());
544 SetSizer(vbox);
545 Fit();
546 Show();
547 }
548 ObsListener FilenameLstnr;
549};
550
552class TheMenu : public wxMenu {
553public:
554 enum class Id : char {
555 kNewFilter = 1, // MacOS does not want ids to be 0.
556 kEditFilter,
557 kDeleteFilter,
558 kEditActiveFilter,
559 kLogSetup,
560 kViewStdColors,
561 };
562
563 TheMenu(wxWindow* parent, wxStaticText* log_label, DataLogger& logger,
564 wxWindow* log_button)
565 : m_parent(parent),
566 m_log_button(log_button),
567 m_logger(logger),
568 m_log_label(log_label) {
569 AppendCheckItem(static_cast<int>(Id::kViewStdColors), _("Use colors"));
570 Append(static_cast<int>(Id::kLogSetup), _("Logging..."));
571 auto filters = new wxMenu("");
572 AppendId(filters, Id::kNewFilter, _("Create new..."));
573 AppendId(filters, Id::kEditFilter, _("Edit..."));
574 AppendId(filters, Id::kDeleteFilter, _("Delete..."));
575 AppendSubMenu(filters, _("Filters..."));
576 if (IsUserFilter(m_filter))
577 Append(static_cast<int>(Id::kEditActiveFilter), _("Edit active filter"));
578
579 Bind(wxEVT_MENU, [&](wxCommandEvent& ev) {
580 switch (static_cast<Id>(ev.GetId())) {
581 case Id::kLogSetup:
582 LogSetup();
583 break;
584
585 case Id::kViewStdColors:
586 SetColor(static_cast<int>(Id::kViewStdColors));
587 break;
588
589 case Id::kNewFilter:
590 CreateFilterDlg(parent);
591 break;
592
593 case Id::kEditFilter:
594 EditFilterDlg(wxTheApp->GetTopWindow());
595 break;
596
597 case Id::kEditActiveFilter:
598 EditOneFilterDlg(wxTheApp->GetTopWindow(), m_filter);
599 break;
600
601 case Id::kDeleteFilter:
602 RemoveFilterDlg(parent);
603 break;
604 }
605 });
606 Check(static_cast<int>(Id::kViewStdColors), true);
607 }
608
609 void SetFilterName(const std::string& filter) {
610 int id = static_cast<int>(Id::kEditActiveFilter);
611 if (FindItem(id)) Delete(id);
612 if (IsUserFilter(filter)) Append(id, _("Edit active filter"));
613 m_filter = filter;
614 }
615
616private:
617 wxMenuItem* AppendId(wxMenu* root, Id id, const wxString& label) {
618 return root->Append(static_cast<int>(id), label);
619 }
620
621 void SetLogFormat(DataLogger::Format format, const std::string& label) {
622 m_log_label->SetLabel(_("Log type: ") + label);
623 m_logger.SetFormat(format);
624 m_log_button->Enable();
625 std::string extension =
626 format == DataLogger::Format::kDefault ? ".log" : ".csv";
627 fs::path path = m_logger.GetLogfile();
628 path = path.parent_path() / (path.stem().string() + extension);
629 m_logger.SetLogfile(path);
630 }
631
632 void LogSetup() {
633 auto dlg = new LoggingSetup(
634 m_parent,
635 [&](DataLogger::Format f, std::string s) { SetLogFormat(f, s); },
636 m_logger);
637 dlg->ShowModal();
638 m_parent->GetParent()->Layout();
639 }
640
641 void SetColor(int id) {
642 auto* w = wxWindow::FindWindowByName("TtyScroll");
643 auto tty_scroll = dynamic_cast<TtyScroll*>(w);
644 if (!tty_scroll) return;
645
646 wxMenuItem* item = FindItem(id);
647 if (!item) return;
648 if (item->IsCheck() && item->IsChecked())
649 tty_scroll->SetColors(std::make_unique<StdColorsByState>());
650 else
651 tty_scroll->SetColors(
652 std::make_unique<NoColorsByState>(tty_scroll->GetForegroundColour()));
653 }
654
655 wxWindow* m_parent;
656 wxWindow* m_log_button;
657 DataLogger& m_logger;
658 wxStaticText* m_log_label;
659 std::string m_filter;
660};
661
664public:
665 CopyClipboardButton(wxWindow* parent) : SvgButton(parent) {
666 LoadIcon(kCopyIconSvg);
667 SetToolTip(_("Copy to clipboard"));
668 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) {
669 auto* tty_scroll =
670 dynamic_cast<TtyScroll*>(wxWindow::FindWindowByName("TtyScroll"));
671 if (tty_scroll) tty_scroll->CopyToClipboard();
672 });
673 }
674};
675
677class FilterButton : public SvgButton {
678public:
679 FilterButton(wxWindow* parent, wxWindow* quick_filter)
680 : SvgButton(parent), m_quick_filter(quick_filter), m_show_filter(true) {
681 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
682 OnClick();
683 }
684
685private:
686 wxWindow* m_quick_filter;
687 bool m_show_filter;
688
689 void OnClick() {
690 LoadIcon(m_show_filter ? kFunnelSvg : kNoFunnelSvg);
691 m_show_filter = !m_show_filter;
692 m_quick_filter->Show(m_show_filter);
693 SetToolTip(m_show_filter ? _("Close quick filter")
694 : _("Open quick filter"));
695 GetGrandParent()->Layout();
696 }
697};
698
700class MenuButton : public wxButton {
701public:
702 MenuButton(wxWindow* parent, TheMenu& menu,
703 std::function<std::string()> get_current_filter)
704 : wxButton(parent, wxID_ANY, wxEmptyString, wxDefaultPosition,
705 wxDefaultSize, wxBU_EXACTFIT),
706 m_menu(menu),
707 m_get_current_filter(std::move(get_current_filter)) {
708 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
709 SetLabel(kUtfIdenticalTo);
710 SetToolTip(_("Open menu"));
711 }
712
713private:
714 TheMenu& m_menu;
715 std::function<std::string()> m_get_current_filter;
716
717 void OnClick() {
718 m_menu.SetFilterName(m_get_current_filter());
719 PopupMenu(&m_menu);
720 }
721};
722
724class StatusLine : public wxPanel {
725public:
726 StatusLine(wxWindow* parent, wxWindow* quick_filter, TtyPanel* tty_panel,
727 std::function<void(bool)> on_stop, DataLogger& logger)
728 : wxPanel(parent),
729 m_is_resized(false),
730 m_log_button(new LogButton(this, logger)),
731 m_log_label(new wxStaticText(this, wxID_ANY, _("Logging: Default"))),
732 m_filter_choice(new FilterChoice(this, tty_panel)),
733 m_menu(this, m_log_label, logger, m_log_button) {
734 // Add a containing sizer for labels, so they can be aligned vertically
735 auto log_label_box = new wxBoxSizer(wxVERTICAL);
736 log_label_box->Add(m_log_label);
737 auto filter_label_box = new wxBoxSizer(wxVERTICAL);
738 filter_label_box->Add(new wxStaticText(this, wxID_ANY, _("View")));
739
740 auto flags = wxSizerFlags(0).Border();
741 auto wbox = new wxWrapSizer(wxHORIZONTAL);
742 wbox->Add(log_label_box, flags.Align(wxALIGN_CENTER_VERTICAL));
743 wbox->Add(m_log_button, flags);
744 // Stretching horizontal space. Does not work with a WrapSizer, known
745 // wx bug. Left in place if it becomes fixed.
746 wbox->Add(GetCharWidth() * 2, 0, 1);
747 wbox->Add(filter_label_box, flags.Align(wxALIGN_CENTER_VERTICAL));
748 wbox->Add(m_filter_choice, flags);
749 wbox->Add(new PauseResumeButton(this, std::move(on_stop)), flags);
750 wbox->Add(new FilterButton(this, quick_filter), flags);
751 auto get_current_filter = [&] {
752 return m_filter_choice->GetStringSelection().ToStdString();
753 };
754 wbox->Add(new CopyClipboardButton(this), flags);
755 wbox->Add(new MenuButton(this, m_menu, get_current_filter), flags);
756 SetSizer(wbox);
757 Layout();
758 Show();
759
760 Bind(wxEVT_SIZE, [&](wxSizeEvent& ev) {
761 m_is_resized = true;
762 ev.Skip();
763 });
764 Bind(wxEVT_RIGHT_UP, [&](wxMouseEvent& ev) {
765 m_menu.SetFilterName(m_filter_choice->GetStringSelection().ToStdString());
766 PopupMenu(&m_menu);
767 });
768 }
769
770 void OnContextClick() {
771 m_menu.SetFilterName(m_filter_choice->GetStringSelection().ToStdString());
772 PopupMenu(&m_menu);
773 }
774
775protected:
776 // Make sure the initial size is sane, don't meddle when user resizes
777 // dialog
778 wxSize DoGetBestClientSize() const override {
779 if (m_is_resized)
780 return wxSize(-1, -1);
781 else
782 return wxSize(85 * GetCharWidth(), 2.5 * GetCharHeight());
783 }
784
785private:
786 bool m_is_resized;
787 wxButton* m_log_button;
788 wxStaticText* m_log_label;
789 wxChoice* m_filter_choice;
790 TheMenu m_menu;
791};
792
793DataLogger::DataLogger(wxWindow* parent, const fs::path& path)
794 : m_parent(parent),
795 m_path(path),
796 m_stream(path, std::ios_base::app),
797 m_is_logging(false),
798 m_format(Format::kDefault),
799 m_log_start(std::chrono::steady_clock::now()) {}
800
801DataLogger::DataLogger(wxWindow* parent) : DataLogger(parent, NullLogfile()) {}
802
803void DataLogger::SetLogging(bool logging) { m_is_logging = logging; }
804
805void DataLogger::SetLogfile(const fs::path& path) {
806 m_stream = std::ofstream(path);
807 m_stream << "# timestamp_format: EPOCH_MILLIS\n";
808 const auto now = std::chrono::system_clock::now();
809 const std::time_t t_c = std::chrono::system_clock::to_time_t(now);
810 m_stream << "# Created at: " << std::ctime(&t_c) << " \n";
811 m_stream << "received_at,protocol,msg_type,source,raw_data\n";
812 m_stream << std::flush;
813 m_path = path;
814 OnNewLogfile.Notify(path.string());
815}
816
817void DataLogger::SetFormat(DataLogger::Format format) { m_format = format; }
818
819fs::path DataLogger::NullLogfile() {
820 if (wxPlatformInfo::Get().GetOperatingSystemId() & wxOS_WINDOWS)
821 return "NUL:";
822 else
823 return "/dev/null";
824}
825
826fs::path DataLogger::GetDefaultLogfile() {
827 if (m_path.stem() != NullLogfile().stem()) return m_path;
828 fs::path path(g_BasePlatform->GetHomeDir().ToStdString());
829 path /= "monitor";
830 path += (m_format == Format::kDefault ? ".log" : ".csv");
831 return path;
832}
833
834std::string DataLogger::GetFileDlgTypes() {
835 if (m_format == Format::kDefault)
836 return _("Log file (*.log)|*.log");
837 else
838 return _("Spreadsheet csv file(*.csv)|*.csv");
839}
840
841void DataLogger::Add(const Logline& ll) {
842 if (!m_is_logging || !ll.navmsg) return;
843 if (m_format == Format::kVdr && ll.navmsg->to_vdr().empty()) return;
844 if (m_format == DataLogger::Format::kVdr)
845 AddVdrLogline(ll, m_stream);
846 else
847 AddStdLogline(ll, m_stream,
848 m_format == DataLogger::Format::kCsv ? '|' : ' ',
849 m_log_start);
850}
851
852DataMonitor::DataMonitor(wxWindow* parent)
853 : wxFrame(parent, wxID_ANY, "Data Monitor", wxDefaultPosition,
854 wxDefaultSize, wxDEFAULT_FRAME_STYLE | wxFRAME_FLOAT_ON_PARENT,
855 kDataMonitorWindowName),
856 m_monitor_src([&](const std::shared_ptr<const NavMsg>& navmsg) {
857 auto msg = std::dynamic_pointer_cast<const Nmea0183Msg>(navmsg);
858 TtyPanel::AddIfExists(navmsg);
859 }),
860 m_quick_filter(nullptr),
861 m_logger(parent) {
862 auto vbox = new wxBoxSizer(wxVERTICAL);
863 auto tty_panel = new TtyPanel(this, 12);
864 vbox->Add(tty_panel, wxSizerFlags(1).Expand().Border());
865 vbox->Add(new wxStaticLine(this), wxSizerFlags().Expand().Border());
866
867 auto on_quick_filter_evt = [&, tty_panel] {
868 auto* quick_filter = dynamic_cast<QuickFilterPanel*>(m_quick_filter);
869 assert(quick_filter);
870 std::string value = quick_filter->GetValue();
871 tty_panel->SetQuickFilter(value);
872 };
873 m_quick_filter = new QuickFilterPanel(this, on_quick_filter_evt);
874 vbox->Add(m_quick_filter, wxSizerFlags());
875
876 auto on_stop = [&, tty_panel](bool stop) { tty_panel->OnStop(stop); };
877 auto status_line =
878 new StatusLine(this, m_quick_filter, tty_panel, on_stop, m_logger);
879 vbox->Add(status_line, wxSizerFlags().Expand());
880 SetSizer(vbox);
881 Fit();
882 Hide();
883
884 m_quick_filter->Bind(wxEVT_TEXT, [&, tty_panel](wxCommandEvent&) {
885 tty_panel->SetQuickFilter(GetLabel().ToStdString());
886 });
887 m_quick_filter->Hide();
888 tty_panel->SetOnRightClick(
889 [&, status_line] { status_line->OnContextClick(); });
890
891 Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& ev) { Hide(); });
892 Bind(wxEVT_RIGHT_UP, [status_line](wxMouseEvent& ev) {
893 status_line->OnContextClick();
894 ev.Skip();
895 });
896 m_filter_list_lstnr.Init(FilterEvents::GetInstance().filter_list_change,
897 [&](ObservedEvt&) { OnFilterListChange(); });
898 m_filter_update_lstnr.Init(
899 FilterEvents::GetInstance().filter_update,
900 [&](ObservedEvt& ev) { OnFilterUpdate(ev.GetString().ToStdString()); });
901
902 m_filter_apply_lstnr.Init(
903 FilterEvents::GetInstance().filter_apply,
904 [&](ObservedEvt& ev) { OnFilterApply(ev.GetString().ToStdString()); });
905}
906
907void DataMonitor::Add(const Logline& ll) {
909 m_logger.Add(ll);
910}
911
913 wxWindow* w = wxWindow::FindWindowByName("TtyPanel");
914 assert(w && "No TtyPanel found");
915 return w->IsShownOnScreen();
916}
917
918void DataMonitor::OnFilterListChange() {
919 wxWindow* w = wxWindow::FindWindowByName(kFilterChoiceName);
920 if (!w) return;
921 auto filter_choice = dynamic_cast<FilterChoice*>(w);
922 assert(filter_choice && "Wrong FilterChoice type (!)");
923 filter_choice->OnFilterListChange();
924}
925
926void DataMonitor::OnFilterUpdate(const std::string& name) {
927 if (name != m_current_filter) return;
928 wxWindow* w = wxWindow::FindWindowByName("TtyScroll");
929 if (!w) return;
930 auto tty_scroll = dynamic_cast<TtyScroll*>(w);
931 assert(tty_scroll && "Wrong TtyScroll type (!)");
932 tty_scroll->SetFilter(filters_on_disk::Read(name));
933}
934
935void DataMonitor::OnFilterApply(const std::string& name) {
936 wxWindow* w = wxWindow::FindWindowByName(kFilterChoiceName);
937 if (!w) return;
938 auto filter_choice = dynamic_cast<FilterChoice*>(w);
939 assert(filter_choice && "Wrong FilterChoice type (!)");
940 m_current_filter = name;
941 filter_choice->OnApply(name);
942}
943
944#pragma clang diagnostic pop
Copy to clipboard button.
Internal helper class.
EventVar OnNewLogfile
Notified with new path on filename change.
void Add(const Logline &ll) override
Add an input line to log output.
virtual bool IsVisible() const override
Return true if log is visible i.
const void Notify()
Notify all listeners, no data supplied.
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.
Define an action to be performed when a KeyProvider is notified.
Definition observable.h:228
void Init(const KeyProvider &kp, std::function< void(ObservedEvt &ev)> action)
Initiate an object yet not listening.
Definition observable.h:255
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:44
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 an 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.
Scrolled TTY-like window for logging, etc.
Definition tty_scroll.h:75
void SetQuickFilter(const std::string s)
Apply a quick filter directly matched against lines.
Definition tty_scroll.h:106
void Pause(bool pause)
Set the window to ignore Add() or not depending on pause.
Definition tty_scroll.h:94
void SetFilter(const NavmsgFilter &filter)
Apply a display filter.
Definition tty_scroll.h:100
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.
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.
NavmsgFilter Read(const std::string &name)
Read filter with given name from disk.
Filter storage routines.
Hooks into gui available in model.
Data monitor filter definitions.
Item in the log window.
Definition nmea_log.h:10
Scrolled TTY-like window for logging, related utilities.