OpenCPN Partial API docs
All Classes Namespaces Functions Variables Pages
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// 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 <memory>
35
36#include <wx/datetime.h>
37#include <wx/event.h>
38#include <wx/string.h>
39
40#include "ais_decoder.h"
41#include "ais_info_gui.h"
42#include "AISTargetAlertDialog.h"
43#include "ais_target_data.h"
44#include "chcanv.h"
45#include "ocpn_frame.h"
46#include "OCPNPlatform.h"
47#include "navutil.h"
48#include "routemanagerdialog.h"
49#include "route_point.h"
50#include "SoundFactory.h"
51#include "undo.h"
52
53wxDEFINE_EVENT(EVT_AIS_DEL_TRACK, wxCommandEvent);
54wxDEFINE_EVENT(EVT_AIS_INFO, ObservedEvt);
55wxDEFINE_EVENT(EVT_AIS_NEW_TRACK, wxCommandEvent);
56wxDEFINE_EVENT(EVT_AIS_TOUCH, wxCommandEvent);
57wxDEFINE_EVENT(EVT_AIS_WP, wxCommandEvent);
58wxDEFINE_EVENT(SOUND_PLAYED_EVTYPE, wxCommandEvent);
59
60extern AisDecoder *g_pAIS;
61extern AISTargetAlertDialog *g_pais_alert_dialog_active;
62extern ArrayOfMmsiProperties g_MMSI_Props_Array;
63extern bool g_bAIS_CPA_Alert;
64extern bool g_bAIS_CPA_Alert_Audio;
65extern bool g_bAIS_DSC_Alert_Audio;
66extern bool g_bAIS_GCPA_Alert_Audio;
67extern bool g_bAIS_SART_Alert_Audio;
68extern bool g_bquiting;
69extern int g_iSoundDeviceIndex;
70extern OCPNPlatform *g_Platform;
71extern Route *pAISMOBRoute;
72extern wxString g_AIS_sound_file;
73extern wxString g_CmdSoundString;
74extern wxString g_DSC_sound_file;
75extern wxString g_SART_sound_file;
76extern MyConfig* pConfig;
77extern RouteManagerDialog *pRouteManagerDialog;
78extern MyFrame* gFrame;
79extern AisInfoGui *g_pAISGUI;
80
81
82static void onSoundFinished(void *ptr) {
83 if (!g_bquiting) {
84 wxCommandEvent ev(SOUND_PLAYED_EVTYPE);
85 wxPostEvent(g_pAISGUI, ev); // FIXME(leamas): Review sound handling.
86 }
87}
88
89static void OnNewAisWaypoint(RoutePoint* pWP) {
90 pConfig->AddNewWayPoint(pWP, -1); // , -1 use auto next num
91 if (pRouteManagerDialog && pRouteManagerDialog->IsShown())
92 pRouteManagerDialog->UpdateWptListCtrl();
93 if (gFrame->GetPrimaryCanvas()) {
94 gFrame->GetPrimaryCanvas()->undo->BeforeUndoableAction(
95 Undo_CreateWaypoint, pWP, Undo_HasParent, NULL);
96 gFrame->GetPrimaryCanvas()->undo->AfterUndoableAction(NULL);
97 gFrame->RefreshAllCanvas(false);
98 gFrame->InvalidateAllGL();
99 }
100}
101
102const static char* const kDeleteTrackPrompt =
103R"""(
104This AIS target has Persistent Tracking selected by MMSI properties
105A Persistent track recording will therefore be restarted for this target.
106
107Do you instead want to stop Persistent Tracking for this target?
108)""";
109
110
111static void OnDeleteTrack(MmsiProperties* props) {
112 if (wxID_NO == OCPNMessageBox(NULL, kDeleteTrackPrompt, _("OpenCPN Info"),
113 wxYES_NO | wxCENTER, 60))
114 {
115 props->m_bPersistentTrack = true;
116 }
117}
118
119
120AisInfoGui::AisInfoGui() {
121 ais_info_listener.Listen(g_pAIS->info_update, this, EVT_AIS_INFO);
122
123 Bind(EVT_AIS_INFO, [&](ObservedEvt &ev) {
124 auto ptr = ev.GetSharedPtr();
125 auto palert_target = std::static_pointer_cast<const AisTargetData>(ptr);
126 ShowAisInfo(palert_target); }
127 );
128
129 ais_touch_listener.Listen(g_pAIS->touch_state, this, EVT_AIS_TOUCH);
130 Bind(EVT_AIS_TOUCH, [&](wxCommandEvent ev) { gFrame->TouchAISActive(); });
131
132 ais_wp_listener.Listen(g_pAIS->new_ais_wp, this, EVT_AIS_WP);
133 Bind(EVT_AIS_WP, [&](wxCommandEvent ev) {
134 auto pWP = static_cast<RoutePoint*>(ev.GetClientData());
135 OnNewAisWaypoint(pWP); });
136
137 ais_new_track_listener.Listen(g_pAIS->new_ais_wp, this,
138 EVT_AIS_NEW_TRACK);
139 Bind(EVT_AIS_NEW_TRACK, [&](wxCommandEvent ev) {
140 auto t = static_cast<Track*>(ev.GetClientData());
141 pConfig->AddNewTrack(t); });
142
143 ais_del_track_listener.Listen(g_pAIS->new_ais_wp, this,
144 EVT_AIS_DEL_TRACK);
145 Bind(EVT_AIS_DEL_TRACK, [&](wxCommandEvent ev) {
146 auto t = static_cast< MmsiProperties*>(ev.GetClientData());
147 OnDeleteTrack(t); });
148
149 Bind(SOUND_PLAYED_EVTYPE, [&](wxCommandEvent ev) {
150 OnSoundFinishedAISAudio(ev); });
151
152 m_AIS_Sound = 0;
153 m_bAIS_Audio_Alert_On = false;
154 m_bAIS_AlertPlaying = false;
155
156}
157
158void AisInfoGui::OnSoundFinishedAISAudio(wxCommandEvent &event) {
159 // By clearing this flag the main event loop will trigger repeated
160 // sounds for as long as the alert condition remains.
161 m_bAIS_AlertPlaying = false;
162}
163
164void AisInfoGui::ShowAisInfo(std::shared_ptr<const AisTargetData> palert_target) {
165 int audioType = AISAUDIO_NONE;
166 if (!palert_target) return;
167
168 // If no alert dialog shown yet...
169 if (!g_pais_alert_dialog_active) {
170 bool b_jumpto = (palert_target->Class == AIS_SART) ||
171 (palert_target->Class == AIS_DSC);
172 bool b_createWP = palert_target->Class == AIS_DSC;
173 bool b_ack = palert_target->Class != AIS_DSC;
174
175 // Show the Alert dialog
176
177 // See FS# 968/998
178 // If alert occurs while OCPN is iconized to taskbar, then clicking
179 // the taskbar icon only brings up the Alert dialog, and not the
180 // entire application. This is an OS specific behavior, not seen on
181 // linux or Mac. This patch will allow the audio alert to occur, and
182 // the visual alert will pop up soon after the user selects the OCPN
183 // icon from the taskbar. (on the next timer tick, probably)
184
185#ifndef __ANDROID__
186 if (gFrame->IsIconized() || !gFrame->IsActive())
187 gFrame->RequestUserAttention();
188#endif
189
190 if (!gFrame->IsIconized()) {
191 AISTargetAlertDialog *pAISAlertDialog = new AISTargetAlertDialog();
192 pAISAlertDialog->Create(palert_target->MMSI, gFrame, g_pAIS,
193 b_jumpto, b_createWP, b_ack, -1,
194 _("AIS Alert"));
195 g_pais_alert_dialog_active = pAISAlertDialog;
196
197 wxTimeSpan alertLifeTime(0, 1, 0,
198 0); // Alert default lifetime, 1 minute.
199 g_pais_alert_dialog_active->dtAlertExpireTime = wxDateTime::Now() + alertLifeTime;
200 g_Platform->PositionAISAlert(pAISAlertDialog);
201
202 pAISAlertDialog->Show(); // Show modeless, so it stays on the screen
203 }
204
205 // Audio alert if requested
206 m_bAIS_Audio_Alert_On = true; // always on when alert is first shown
207 }
208
209
210 // The AIS Alert dialog is already shown. If the dialog MMSI number is
211 // still alerted, update the dialog otherwise, destroy the dialog
212 else {
213 // Find the target with shortest CPA, ignoring DSC and SART targets
214 double tcpa_min = 1e6; // really long
215 AisTargetData *palert_target_lowestcpa = NULL;
216 const auto& current_targets = g_pAIS->GetTargetList();
217 for (auto& it : current_targets) {
218 auto td = it.second;
219 if (td) {
220 if ((td->Class != AIS_SART) && (td->Class != AIS_DSC)) {
221 if (g_bAIS_CPA_Alert && td->b_active) {
222 if ((AIS_ALERT_SET == td->n_alert_state) && !td->b_in_ack_timeout) {
223 if (td->TCPA < tcpa_min) {
224 tcpa_min = td->TCPA;
225 palert_target_lowestcpa = td.get();
226 }
227 }
228 }
229 }
230 }
231 }
232
233 // Get the target currently displayed
234 palert_target = g_pAIS->Get_Target_Data_From_MMSI(
235 g_pais_alert_dialog_active->Get_Dialog_MMSI());
236
237 // If the currently displayed target is not alerted, it must be in "expiry
238 // delay" We should cancel that alert display now, and pick up the more
239 // critical CPA target on the next timer tick
240 if (palert_target) {
241 if (AIS_NO_ALERT == palert_target->n_alert_state) {
242 if (palert_target_lowestcpa) {
243 palert_target = NULL;
244 }
245 }
246 }
247
248 if (palert_target) {
249 wxDateTime now = wxDateTime::Now();
250 if (((AIS_ALERT_SET == palert_target->n_alert_state) &&
251 !palert_target->b_in_ack_timeout) ||
252 (palert_target->Class == AIS_SART) ) {
253 g_pais_alert_dialog_active->UpdateText();
254 // Retrigger the alert expiry timeout if alerted now
255 wxTimeSpan alertLifeTime(0, 1, 0,
256 0); // Alert default lifetime, 1 minute.
257 g_pais_alert_dialog_active->dtAlertExpireTime = wxDateTime::Now() + alertLifeTime;
258 }
259 // In "expiry delay"?
260 else if (!palert_target->b_in_ack_timeout &&
261 (now.IsEarlierThan(g_pais_alert_dialog_active->dtAlertExpireTime))) {
262 g_pais_alert_dialog_active->UpdateText();
263 } else {
264 g_pais_alert_dialog_active->Close();
265 m_bAIS_Audio_Alert_On = false;
266 }
267
268 if (true == palert_target->b_suppress_audio)
269 m_bAIS_Audio_Alert_On = false;
270 else
271 m_bAIS_Audio_Alert_On = true;
272 } else { // this should not happen, however...
273 g_pais_alert_dialog_active->Close();
274 m_bAIS_Audio_Alert_On = false;
275 }
276 }
277
278 // At this point, the audio flag is set
279 // Honor the global flag
280 if (!g_bAIS_CPA_Alert_Audio) m_bAIS_Audio_Alert_On = false;
281
282 if (m_bAIS_Audio_Alert_On) {
283 if (!m_AIS_Sound) {
284 m_AIS_Sound = SoundFactory(g_CmdSoundString.mb_str(wxConvUTF8));
285 }
286 if (!AIS_AlertPlaying()) {
287 m_bAIS_AlertPlaying = true;
288 wxString soundFile;
289 switch (audioType) {
290 case AISAUDIO_CPA:
291 default:
292 if (g_bAIS_GCPA_Alert_Audio) soundFile = g_AIS_sound_file;
293 break;
294 case AISAUDIO_DSC:
295 if (g_bAIS_DSC_Alert_Audio) soundFile = g_DSC_sound_file;
296 break;
297 case AISAUDIO_SART:
298 if (g_bAIS_SART_Alert_Audio) soundFile = g_SART_sound_file;
299 break;
300 }
301
302 m_AIS_Sound->Load(soundFile, g_iSoundDeviceIndex);
303 if (m_AIS_Sound->IsOk()) {
304 m_AIS_Sound->SetFinishedCallback(onSoundFinished, this);
305 if (!m_AIS_Sound->Play()) m_bAIS_AlertPlaying = false;
306 } else
307 m_bAIS_AlertPlaying = false;
308 }
309 }
310 // If a SART Alert is active, check to see if the MMSI has special properties
311 // set indicating that this Alert is a MOB for THIS ship.
312 if (palert_target && (palert_target->Class == AIS_SART)) {
313 for (unsigned int i = 0; i < g_MMSI_Props_Array.GetCount(); i++) {
314 if (palert_target->MMSI == g_MMSI_Props_Array[i]->MMSI) {
315 if (pAISMOBRoute)
316 gFrame->UpdateAISMOBRoute(palert_target.get());
317 else
318 gFrame->ActivateAISMOBRoute(palert_target.get());
319 break;
320 }
321 }
322 }
323}
EventVar new_ais_wp
Notified when new AIS wp is created.
Definition: ais_decoder.h:117
EventVar info_update
Notified when AIS user dialogs should update.
Definition: ais_decoder.h:111
EventVar touch_state
Notified when gFrame->TouchAISActive() should be invoked.
Definition: ais_decoder.h:114
void Listen(const std::string &key, wxEvtHandler *listener, wxEventType evt)
Set object to send wxEventType ev to listener on changes in key.
Definition: observable.cpp:98
Adds a std::shared<void> element to wxCommandEvent.
Definition: ocpn_plugin.h:1615
Definition: route.h:70
Definition: track.h:79