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