OpenCPN Partial API docs
Loading...
Searching...
No Matches
ais_info_gui.cpp
Go to the documentation of this file.
1/**************************************************************************
2 * Copyright (C) 2010 by David S. Register *
3 * Copyright (C) 2022 Alec Leamas *
4 * *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
9 * *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
14 * *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, see <https://www.gnu.org/licenses/>. *
17 **************************************************************************/
18
25#include <memory>
26
27// For compilers that support precompilation, includes "wx.h".
28#include <wx/wxprec.h>
29
30#ifndef WX_PRECOMP
31#include <wx/wx.h>
32#endif // precompiled headers
33
34#include <wx/datetime.h>
35#include <wx/event.h>
36#include <wx/string.h>
37
38#include "o_sound/o_sound.h"
39#include "gl_headers.h" // Must be before anything including GL stuff
40
41#include "model/ais_decoder.h"
44#include "model/gui_vars.h"
45#include "model/navobj_db.h"
46#include "model/route_point.h"
47
48#include "ais_info_gui.h"
50#include "chcanv.h"
51#include "navutil.h"
52#include "ocpn_frame.h"
53#include "ocpn_platform.h"
54#include "routemanagerdialog.h"
55#include "undo.h"
56
57wxDEFINE_EVENT(EVT_AIS_DEL_TRACK, wxCommandEvent);
58wxDEFINE_EVENT(EVT_AIS_INFO, ObservedEvt);
59wxDEFINE_EVENT(EVT_AIS_NEW_TRACK, wxCommandEvent);
60wxDEFINE_EVENT(EVT_AIS_TOUCH, wxCommandEvent);
61wxDEFINE_EVENT(EVT_AIS_WP, wxCommandEvent);
62wxDEFINE_EVENT(SOUND_PLAYED_EVTYPE, wxCommandEvent);
63
64AisInfoGui *g_pAISGUI; // global instance
65
66static void onSoundFinished(void *ptr) {
67 if (!g_bquiting) {
68 wxCommandEvent ev(SOUND_PLAYED_EVTYPE);
69 wxPostEvent(g_pAISGUI, ev); // FIXME(leamas): Review sound handling.
70 }
71}
72
73static void OnNewAisWaypoint(RoutePoint *pWP) {
74 NavObj_dB::GetInstance().InsertRoutePoint(pWP);
75
77 pRouteManagerDialog->UpdateWptListCtrl();
78 if (gFrame->GetPrimaryCanvas()) {
79 gFrame->GetPrimaryCanvas()->undo->BeforeUndoableAction(
80 Undo_CreateWaypoint, pWP, Undo_HasParent, NULL);
81 gFrame->GetPrimaryCanvas()->undo->AfterUndoableAction(NULL);
82 gFrame->RefreshAllCanvas(false);
83 gFrame->InvalidateAllGL();
84 }
85}
86
87const static char *const kDeleteTrackPrompt = _(R"(
88This AIS target has Persistent Tracking selected by MMSI properties
89A Persistent track recording will therefore be restarted for this target.
90
91Do you instead want to stop Persistent Tracking for this target?
92)");
93
94static void OnDeleteTrack(MmsiProperties *props) {
95 if (wxID_NO == OCPNMessageBox(NULL, kDeleteTrackPrompt, _("OpenCPN Info"),
96 wxYES_NO | wxCENTER, 60)) {
97 props->m_bPersistentTrack = true;
98 }
99}
100
101AisInfoGui::AisInfoGui() {
102 ais_info_listener.Listen(g_pAIS->info_update, this, EVT_AIS_INFO);
103
104 Bind(EVT_AIS_INFO, [&](ObservedEvt &ev) {
105 auto ptr = ev.GetSharedPtr();
106 auto palert_target = std::static_pointer_cast<const AisTargetData>(ptr);
107 ShowAisInfo(palert_target);
108 });
109
110 ais_touch_listener.Listen(g_pAIS->touch_state, this, EVT_AIS_TOUCH);
111 Bind(EVT_AIS_TOUCH, [&](wxCommandEvent ev) { gFrame->TouchAISActive(); });
112
113 ais_wp_listener.Listen(g_pAIS->new_ais_wp, this, EVT_AIS_WP);
114 Bind(EVT_AIS_WP, [&](wxCommandEvent ev) {
115 auto pWP = static_cast<RoutePoint *>(ev.GetClientData());
116 OnNewAisWaypoint(pWP);
117 });
118
119 ais_new_track_listener.Listen(g_pAIS->new_ais_wp, this, EVT_AIS_NEW_TRACK);
120 Bind(EVT_AIS_NEW_TRACK, [&](wxCommandEvent ev) {
121 auto t = static_cast<Track *>(ev.GetClientData());
122 NavObj_dB::GetInstance().InsertTrack(t);
123 });
124
125 ais_del_track_listener.Listen(g_pAIS->new_ais_wp, this, EVT_AIS_DEL_TRACK);
126 Bind(EVT_AIS_DEL_TRACK, [&](wxCommandEvent ev) {
127 auto t = static_cast<MmsiProperties *>(ev.GetClientData());
128 OnDeleteTrack(t);
129 });
130
131 Bind(SOUND_PLAYED_EVTYPE,
132 [&](wxCommandEvent ev) { OnSoundFinishedAISAudio(ev); });
133
134 m_AIS_Sound = 0;
135 m_bAIS_Audio_Alert_On = false;
136 m_bAIS_AlertPlaying = false;
137 m_alarm_defer_count = -1;
138 m_lastMMSItime = wxDateTime::Now();
139}
140
141void AisInfoGui::OnSoundFinishedAISAudio(wxCommandEvent &event) {
142 // By clearing this flag the main event loop will trigger repeated
143 // sounds for as long as the alert condition remains.
144
145 // Unload/reset OcpnSound object.
146 m_AIS_Sound->UnLoad();
147
148 m_bAIS_AlertPlaying = false;
149}
150
151void AisInfoGui::ShowAisInfo(
152 std::shared_ptr<const AisTargetData> palert_target) {
153 if (!palert_target) return;
154
155 int audioType = AISAUDIO_NONE;
156
157 switch (palert_target->Class) {
158 case AIS_DSC:
159 audioType = AISAUDIO_DSC;
160 break;
161 case AIS_SART:
162 audioType = AISAUDIO_SART;
163 break;
164 default:
165 audioType = AISAUDIO_CPA;
166 break;
167 }
168
169 // Maybe Reset deferral counter
170 // Arrange to reset deferral counter if 5 seconds have passed without an alarm
171 int last_alert_MMSI = m_lastMMSI;
172 wxDateTime last_alert_time = m_lastMMSItime;
173
174 if (palert_target->MMSI != last_alert_MMSI) {
175 wxTimeSpan dt = wxDateTime::Now() - last_alert_time;
176 if (dt.GetSeconds() > 5) {
177 m_alarm_defer_count = -1; // reset the counter
178 }
179 }
180
181 m_lastMMSI = palert_target->MMSI;
182 m_lastMMSItime = wxDateTime::Now();
183
184 // printf("defer count: %d\n", m_alarm_defer_count);
185
186 // If no alert dialog shown yet...
187 if (!g_pais_alert_dialog_active) {
188 // Manage deferred Alarm dialog and sound
189 if (m_alarm_defer_count < 0) {
190 m_alarm_defer_count = g_AIS_alert_delay;
191 } else {
192 if (m_alarm_defer_count >= 1) {
193 m_alarm_defer_count--;
194 }
195 }
196 if (m_alarm_defer_count == 0) { // Fire the Alarm, with sound
197 bool b_jumpto = (palert_target->Class == AIS_SART) ||
198 (palert_target->Class == AIS_DSC);
199 bool b_createWP = palert_target->Class == AIS_DSC;
200 bool b_ack = palert_target->Class != AIS_DSC;
201
202 // Show the Alert dialog
203
204 // See FS# 968/998
205 // If alert occurs while OCPN is iconized to taskbar, then clicking
206 // the taskbar icon only brings up the Alert dialog, and not the
207 // entire application. This is an OS specific behavior, not seen on
208 // linux or Mac. This patch will allow the audio alert to occur, and
209 // the visual alert will pop up soon after the user selects the OCPN
210 // icon from the taskbar. (on the next timer tick, probably)
211
212#ifndef __ANDROID__
213// if (gFrame->IsIconized() || !gFrame->IsActive())
214// gFrame->RequestUserAttention();
215#endif
216
217 if (!gFrame->IsIconized()) {
218 AISTargetAlertDialog *pAISAlertDialog = new AISTargetAlertDialog();
219 pAISAlertDialog->Create(palert_target->MMSI, gFrame, g_pAIS, b_jumpto,
220 b_createWP, b_ack, -1, _("AIS Alert"));
221
222 g_pais_alert_dialog_active = pAISAlertDialog;
223
224 wxTimeSpan alertLifeTime(0, 1, 0,
225 0); // Alert default lifetime, 1 minute.
226 auto alert_dlg_active =
227 dynamic_cast<AISTargetAlertDialog *>(g_pais_alert_dialog_active);
228 alert_dlg_active->dtAlertExpireTime = wxDateTime::Now() + alertLifeTime;
229 g_Platform->PositionAISAlert(pAISAlertDialog);
230
231 pAISAlertDialog->Show(); // Show modeless, so it stays on the screen
232 }
233
234 // Audio alert if requested
235 m_bAIS_Audio_Alert_On = true; // always on when alert is first shown
236 }
237 }
238
239 // The AIS Alert dialog is already shown. If the dialog MMSI number is
240 // still alerted, update the dialog otherwise, destroy the dialog
241 else {
242 // Find the target with shortest CPA, ignoring DSC and SART targets
243 double tcpa_min = 1e6; // really long
244 AisTargetData *palert_target_lowestcpa = NULL;
245 const auto &current_targets = g_pAIS->GetTargetList();
246 for (auto &it : current_targets) {
247 auto td = it.second;
248 if (td) {
249 if ((td->Class != AIS_SART) && (td->Class != AIS_DSC)) {
250 if (g_bAIS_CPA_Alert && td->b_active) {
251 if ((AIS_ALERT_SET == td->n_alert_state) && !td->b_in_ack_timeout) {
252 if (td->TCPA < tcpa_min) {
253 tcpa_min = td->TCPA;
254 palert_target_lowestcpa = td.get();
255 }
256 }
257 }
258 }
259 }
260 }
261
262 // Get the target currently displayed
263 auto alert_dlg_active =
264 dynamic_cast<AISTargetAlertDialog *>(g_pais_alert_dialog_active);
265 palert_target =
266 g_pAIS->Get_Target_Data_From_MMSI(alert_dlg_active->Get_Dialog_MMSI());
267
268 // If the currently displayed target is not alerted, it must be in "expiry
269 // delay" We should cancel that alert display now, and pick up the more
270 // critical CPA target on the next timer tick
271 if (palert_target) {
272 if (AIS_NO_ALERT == palert_target->n_alert_state) {
273 if (palert_target_lowestcpa) {
274 palert_target = NULL;
275 }
276 m_alarm_defer_count = -1;
277 }
278 }
279
280 if (palert_target) {
281 wxDateTime now = wxDateTime::Now();
282 if (((AIS_ALERT_SET == palert_target->n_alert_state) &&
283 !palert_target->b_in_ack_timeout) ||
284 (palert_target->Class == AIS_SART)) {
285 alert_dlg_active->UpdateText();
286 // Retrigger the alert expiry timeout if alerted now
287 wxTimeSpan alertLifeTime(0, 1, 0,
288 0); // Alert default lifetime, 1 minute.
289 alert_dlg_active->dtAlertExpireTime = wxDateTime::Now() + alertLifeTime;
290 }
291 // In "expiry delay"?
292 else if (!palert_target->b_in_ack_timeout &&
293 (now.IsEarlierThan(alert_dlg_active->dtAlertExpireTime))) {
294 alert_dlg_active->UpdateText();
295 } else {
296 alert_dlg_active->Close();
297 m_bAIS_Audio_Alert_On = false;
298 m_alarm_defer_count = -1;
299 }
300
301 if (true == palert_target->b_suppress_audio)
302 m_bAIS_Audio_Alert_On = false;
303 else
304 m_bAIS_Audio_Alert_On = true;
305 } else { // this should not happen, however...
306 alert_dlg_active->Close();
307 m_bAIS_Audio_Alert_On = false;
308 m_alarm_defer_count = -1;
309 }
310 }
311
312 // At this point, the audio flag (m_bAIS_Audio_Alert_On) is set
313 // Honor the global flag
314 if (!g_bAIS_CPA_Alert_Audio) m_bAIS_Audio_Alert_On = false;
315
316 // Ensure that an Alert dialog in visible before activating sound
317 auto alert_dlg_active_audio_check =
318 dynamic_cast<AISTargetAlertDialog *>(g_pais_alert_dialog_active);
319 if (alert_dlg_active_audio_check && alert_dlg_active_audio_check->IsShown()) {
320 if (m_bAIS_Audio_Alert_On) {
321 if (!m_AIS_Sound) {
322 m_AIS_Sound = o_sound::Factory();
323 }
324 if (!AIS_AlertPlaying()) {
325 m_bAIS_AlertPlaying = true;
326 wxString soundFile;
327 switch (audioType) {
328 case AISAUDIO_DSC:
329 if (g_bAIS_DSC_Alert_Audio) soundFile = g_DSC_sound_file;
330 break;
331 case AISAUDIO_SART:
332 if (g_bAIS_SART_Alert_Audio) soundFile = g_SART_sound_file;
333 break;
334 case AISAUDIO_CPA:
335 default:
336 if (g_bAIS_GCPA_Alert_Audio) soundFile = g_AIS_sound_file;
337 break;
338 }
339
340 m_AIS_Sound->Load(soundFile, g_iSoundDeviceIndex);
341 if (m_AIS_Sound->IsOk()) {
342 m_AIS_Sound->SetFinishedCallback(onSoundFinished, this);
343 if (!m_AIS_Sound->Play()) {
344 delete m_AIS_Sound;
345 m_AIS_Sound = 0;
346 m_bAIS_AlertPlaying = false;
347 }
348 } else {
349 delete m_AIS_Sound;
350 m_AIS_Sound = 0;
351 m_bAIS_AlertPlaying = false;
352 }
353 }
354 }
355 }
356
357 // If a SART Alert is active, check to see if the MMSI has special properties
358 // set indicating that this Alert is a MOB for THIS ship.
359 if (palert_target && (palert_target->Class == AIS_SART)) {
360 for (unsigned int i = 0; i < g_MMSI_Props_Array.GetCount(); i++) {
361 if (palert_target->MMSI == g_MMSI_Props_Array[i]->MMSI) {
362 if (pAISMOBRoute)
363 gFrame->UpdateAISMOBRoute(palert_target.get());
364 else
365 gFrame->ActivateAISMOBRoute(palert_target.get());
366 break;
367 }
368 }
369 }
370}
ArrayOfMmsiProperties g_MMSI_Props_Array
Global instance.
AisDecoder * g_pAIS
Global instance.
Class AisDecoder and helpers.
AisInfoGui * g_pAISGUI
Global instance.
Global state for AIS decoder.
Class AISTargetAlertDialog and helpers.
AIS target definitions.
Generic Chart canvas base.
Dialog for displaying AIS target alerts.
EventVar new_ais_wp
Notified when new AIS wp is created.
EventVar info_update
Notified when AIS user dialogs should update.
EventVar touch_state
Notified when gFrame->TouchAISActive() should be invoked.
Handles the AIS information GUI and sound alerts.
Process incoming AIS messages.
Definition ais_decoder.h:74
void Listen(const std::string &key, wxEvtHandler *listener, wxEventType evt)
Set object to send wxEventType ev to listener on changes in key.
Custom event class for OpenCPN's notification system.
std::shared_ptr< const void > GetSharedPtr() const
Gets the event's payload data.
Represents a waypoint or mark within the navigation system.
Definition route_point.h:71
Represents a track, which is a series of connected track points.
Definition track.h:117
Platform independent GL includes.
Miscellaneous globals primarely used by gui layer, not persisted in configuration file.
MySQL based storage for routes, tracks, etc.
Utility functions.
OpenCPN top window.
OpenCPN Platform specific support utilities.
Waypoint or mark abstraction.
Route * pAISMOBRoute
Global instance.
Definition routeman.cpp:61
RouteManagerDialog * pRouteManagerDialog
Global instance.
Manage routes dialog.
Framework for Undo features.