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