1 /***************************************************************************
2 *
3 * Project: OpenCPN
4 *
5 ***************************************************************************
6 * Copyright (C) 2010 by David S. Register *
7 * *
8 * This program is free software; you can redistribute it and/or modify *
9 * it under the terms of the GNU General Public License as published by *
10 * the Free Software Foundation; either version 2 of the License, or *
11 * (at your option) any later version. *
12 * *
13 * This program is distributed in the hope that it will be useful, *
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
16 * GNU General Public License for more details. *
17 * *
18 * You should have received a copy of the GNU General Public License *
19 * along with this program; if not, write to the *
20 * Free Software Foundation, Inc., *
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
22 ***************************************************************************
23 */
24 #include <fstream>
25
26 #ifdef __MINGW32__
27 #undef IPV6STRICT // mingw FTBS fix: missing struct ip_mreq
28 #include <windows.h>
29 #endif
30
31 #include "wx/tokenzr.h"
32
33 #include "SoundFactory.h"
34 #include "AIS_Decoder.h"
35 #include "AIS_Target_Data.h"
36 #include "AISTargetAlertDialog.h"
37 #include "Select.h"
38 #include "georef.h"
39 #include "geodesic.h"
40 #include "OCPN_DataStreamEvent.h"
41 #include "OCPN_SignalKEvent.h"
42 #include "OCPNPlatform.h"
43 #include "pluginmanager.h"
44 #include "Track.h"
45 #include <multiplexer.h>
46 #include "config.h"
47 #include <cstdio>
48
49 #if !defined(NAN)
50 static const long long lNaN = 0xfff8000000000000;
51 #define NAN (*(double*)&lNaN)
52 #endif
53
54 extern AISTargetAlertDialog *g_pais_alert_dialog_active;
55 extern Select *pSelectAIS;
56 extern Select *pSelect;
57 extern MyFrame *gFrame;
58 extern bool bGPSValid;
59 extern bool g_bCPAMax;
60 extern double g_CPAMax_NM;
61 extern bool g_bCPAWarn;
62 extern double g_CPAWarn_NM;
63 extern bool g_bTCPA_Max;
64 extern double g_TCPA_Max;
65 extern bool g_bMarkLost;
66 extern double g_MarkLost_Mins;
67 extern bool g_bRemoveLost;
68 extern double g_RemoveLost_Mins;
69 extern double g_AISShowTracks_Mins;
70 extern bool g_bHideMoored;
71 extern double g_ShowMoored_Kts;
72 extern wxString g_sAIS_Alert_Sound_File;
73 extern bool g_bAIS_CPA_Alert_Suppress_Moored;
74 extern bool g_bAIS_ACK_Timeout;
75 extern double g_AckTimeout_Mins;
76 extern bool g_bShowAreaNotices;
77 extern bool g_bDrawAISSize;
78 extern bool g_bDrawAISRealtime;
79 extern double g_AIS_RealtPred_Kts;
80 extern bool g_bShowAISName;
81 extern int g_Show_Target_Name_Scale;
82 extern bool g_bAllowShowScaled;
83 extern bool g_bShowScaled;
84 extern bool g_bInlandEcdis;
85 extern int g_iSoundDeviceIndex;
86 extern bool g_bWplIsAprsPosition;
87 extern double gLat;
88 extern double gLon;
89 extern double gCog;
90 extern double gSog;
91 extern double gHdt;
92 extern bool g_bAIS_CPA_Alert;
93 extern bool g_bAIS_CPA_Alert_Audio;
94 extern ArrayOfMMSIProperties g_MMSI_Props_Array;
95 extern Route *pAISMOBRoute;
96 extern wxString AISTargetNameFileName;
97 extern MyConfig *pConfig;
98 extern TrackList *pTrackList;
99 extern OCPNPlatform *g_Platform;
100 extern PlugInManager *g_pi_manager;
101 extern Multiplexer *g_pMUX;
102 extern AIS_Decoder *g_pAIS;
103
104 extern wxString g_CmdSoundString;
105
106 bool g_benableAISNameCache;
107 bool g_bUseOnlyConfirmedAISName;
108 wxString GetShipNameFromFile(int);
109
110 wxDEFINE_EVENT(SOUND_PLAYED_EVTYPE, wxCommandEvent);
111
112 BEGIN_EVENT_TABLE(AIS_Decoder, wxEvtHandler)
113 EVT_TIMER(TIMER_AIS1, AIS_Decoder::OnTimerAIS)
114 EVT_TIMER(TIMER_DSC, AIS_Decoder::OnTimerDSC)
115 EVT_COMMAND(wxID_ANY, SOUND_PLAYED_EVTYPE, AIS_Decoder::OnSoundFinishedAISAudio)
116 END_EVENT_TABLE()
117
118 static int n_msgs;
119 static int n_msg1;
120 static int n_msg5;
121 static int n_msg24;
122 static bool b_firstrx;
123 static int first_rx_ticks;
124 static int rx_ticks;
125 static double arpa_ref_hdg = NAN;
126
127 extern const wxEventType wxEVT_OCPN_DATASTREAM;
128 extern int gps_watchdog_timeout_ticks;
129 extern bool g_bquiting;
130
onSoundFinished(void * ptr)131 static void onSoundFinished(void* ptr)
132 {
133 if (!g_bquiting) {
134 //auto aisDecoder = static_cast<AIS_Decoder*>(ptr);
135 wxCommandEvent ev(SOUND_PLAYED_EVTYPE);
136 wxPostEvent(g_pAIS, ev);
137 }
138 }
139
140 void AISshipNameCache(AIS_Target_Data *pTargetData,
141 AIS_Target_Name_Hash *AISTargetNamesC,
142 AIS_Target_Name_Hash *AISTargetNamesNC,
143 long mmsi );
144
AIS_Decoder(wxFrame * parent)145 AIS_Decoder::AIS_Decoder( wxFrame *parent )
146 : m_signalk_selfid(""),
147 AISTargetList(new AIS_Target_Hash())
148 {
149 AISTargetList = new AIS_Target_Hash;
150
151 // Load cached AIS target names from a file
152 AISTargetNamesC = new AIS_Target_Name_Hash;
153 AISTargetNamesNC = new AIS_Target_Name_Hash;
154
155 if(g_benableAISNameCache){
156 wxTextFile infile;
157 if ( infile.Open(AISTargetNameFileName) ){
158 AIS_Target_Name_Hash *HashFile = AISTargetNamesNC;
159 wxString line = infile.GetFirstLine();
160 while ( !infile.Eof() ) {
161 if( line.IsSameAs( wxT("+++==Confirmed Entry's==+++") ) )
162 HashFile = AISTargetNamesC;
163 else {
164 if( line.IsSameAs( wxT("+++==Non Confirmed Entry's==+++") ) )
165 HashFile = AISTargetNamesNC;
166 else{
167 wxStringTokenizer tokenizer( line, _T(",") );
168 int mmsi = wxAtoi( tokenizer.GetNextToken() );
169 wxString name = tokenizer.GetNextToken().Trim();
170 ( *HashFile )[mmsi] = name;
171 }
172 }
173 line = infile.GetNextLine();
174 }
175 }
176 infile.Close();
177 }
178
179 AIS_AreaNotice_Sources = new AIS_Target_Hash;
180 BuildERIShipTypeHash();
181
182 g_pais_alert_dialog_active = NULL;
183 m_bAIS_Audio_Alert_On = false;
184 m_AIS_Sound = 0;
185
186 m_n_targets = 0;
187
188 m_parent_frame = parent;
189
190 m_bAIS_AlertPlaying = false;
191
192 TimerAIS.SetOwner(this, TIMER_AIS1);
193 TimerAIS.Start(TIMER_AIS_MSEC,wxTIMER_CONTINUOUS);
194
195 m_ptentative_dsctarget = NULL;
196 m_dsc_timer.SetOwner( this, TIMER_DSC );
197
198
199 // Create/connect a dynamic event handler slot for wxEVT_OCPN_DATASTREAM(s)
200 Connect(wxEVT_OCPN_DATASTREAM, (wxObjectEventFunction)(wxEventFunction)&AIS_Decoder::OnEvtAIS);
201 Connect( EVT_OCPN_SIGNALKSTREAM, (wxObjectEventFunction) (wxEventFunction) &AIS_Decoder::OnEvtSignalK );
202 }
203
~AIS_Decoder(void)204 AIS_Decoder::~AIS_Decoder( void )
205 {
206 AIS_Target_Hash::iterator it;
207 AIS_Target_Hash *current_targets = GetTargetList();
208
209 for( it = ( *current_targets ).begin(); it != ( *current_targets ).end(); ++it ) {
210 AIS_Target_Data *td = it->second;
211
212 delete td;
213 }
214
215 delete AISTargetList;
216
217 delete AIS_AreaNotice_Sources;
218
219 //Write mmsi-shipsname to file in a safe way
220 wxTempFile outfile;
221 if ( outfile.Open(AISTargetNameFileName) )
222 {
223 wxString content;
224 content = wxT("+++==Confirmed Entry's==+++");
225 AIS_Target_Name_Hash::iterator it;
226 for ( it = AISTargetNamesC->begin(); it != AISTargetNamesC->end(); ++it )
227 {
228 content.append(_T("\r\n") );
229 content.append( wxString::Format(wxT("%i"),it->first ) );
230 content.append( _T(",") ).append(it->second );
231 }
232 content.append( _T("\r\n"));
233 content.append( _T("+++==Non Confirmed Entry's==+++") );
234 for ( it = AISTargetNamesNC->begin(); it != AISTargetNamesNC->end(); ++it )
235 {
236 content.append(_T("\r\n") );
237 content.append( wxString::Format(wxT("%i"),it->first ) );
238 content.append( _T(",") ).append(it->second );
239 }
240 outfile.Write( content );
241 outfile.Commit();
242 }
243
244
245 AISTargetNamesC->clear();
246 delete AISTargetNamesC;
247 AISTargetNamesNC->clear();
248 delete AISTargetNamesNC;
249
250 clear_hash_ERI();
251
252 m_dsc_timer.Stop();
253 m_AIS_Audio_Alert_Timer.Stop();
254 TimerAIS.Stop();
255
256 #ifdef AIS_DEBUG
257 printf("First message[1, 2] ticks: %d Last Message [1,2]ticks %d Difference: %d\n", first_rx_ticks, rx_ticks, rx_ticks - first_rx_ticks);
258 #endif
259 }
260
BuildERIShipTypeHash(void)261 void AIS_Decoder::BuildERIShipTypeHash(void)
262 {
263 make_hash_ERI(8000, _("Vessel, type unknown"));
264 make_hash_ERI(8150, _("Freightbarge"));
265 make_hash_ERI(8160, _("Tankbarge"));
266 make_hash_ERI(8163, _("Tankbarge, dry cargo as if liquid (e.g. cement)"));
267 make_hash_ERI(8450, _("Service vessel, police patrol, port service"));
268 make_hash_ERI(8430, _("Pushboat, single"));
269 make_hash_ERI(8510, _("Object, not otherwise specified"));
270 make_hash_ERI(8470, _("Object, towed, not otherwise specified"));
271 make_hash_ERI(8490, _("Bunkership"));
272 make_hash_ERI(8010, _("Motor freighter"));
273 make_hash_ERI(8020, _("Motor tanker"));
274 make_hash_ERI(8021, _("Motor tanker, liquid cargo, type N"));
275 make_hash_ERI(8022, _("Motor tanker, liquid cargo, type C"));
276 make_hash_ERI(8023, _("Motor tanker, dry cargo as if liquid (e.g. cement)"));
277 make_hash_ERI(8030, _("Container vessel"));
278 make_hash_ERI(8040, _("Gas tanker"));
279 make_hash_ERI(8050, _("Motor freighter, tug"));
280 make_hash_ERI(8060, _("Motor tanker, tug"));
281 make_hash_ERI(8070, _("Motor freighter with one or more ships alongside"));
282 make_hash_ERI(8080, _("Motor freighter with tanker"));
283 make_hash_ERI(8090, _("Motor freighter pushing one or more freighters"));
284 make_hash_ERI(8100, _("Motor freighter pushing at least one tank-ship"));
285 make_hash_ERI(8110, _("Tug, freighter"));
286 make_hash_ERI(8120, _("Tug, tanker"));
287 make_hash_ERI(8130, _("Tug freighter, coupled"));
288 make_hash_ERI(8140, _("Tug, freighter/tanker, coupled"));
289 make_hash_ERI(8161, _("Tankbarge, liquid cargo, type N"));
290 make_hash_ERI(8162, _("Tankbarge, liquid cargo, type C"));
291 make_hash_ERI(8170, _("Freightbarge with containers"));
292 make_hash_ERI(8180, _("Tankbarge, gas"));
293 make_hash_ERI(8210, _("Pushtow, one cargo barge"));
294 make_hash_ERI(8220, _("Pushtow, two cargo barges"));
295 make_hash_ERI(8230, _("Pushtow, three cargo barges"));
296 make_hash_ERI(8240, _("Pushtow, four cargo barges"));
297 make_hash_ERI(8250, _("Pushtow, five cargo barges"));
298 make_hash_ERI(8260, _("Pushtow, six cargo barges"));
299 make_hash_ERI(8270, _("Pushtow, seven cargo barges"));
300 make_hash_ERI(8280, _("Pushtow, eight cargo barges"));
301 make_hash_ERI(8290, _("Pushtow, nine or more barges"));
302 make_hash_ERI(8310, _("Pushtow, one tank/gas barge"));
303 make_hash_ERI(8320, _("Pushtow, two barges at least one tanker or gas barge"));
304 make_hash_ERI(8330, _("Pushtow, three barges at least one tanker or gas barge"));
305 make_hash_ERI(8340, _("Pushtow, four barges at least one tanker or gas barge"));
306 make_hash_ERI(8350, _("Pushtow, five barges at least one tanker or gas barge"));
307 make_hash_ERI(8360, _("Pushtow, six barges at least one tanker or gas barge"));
308 make_hash_ERI(8370, _("Pushtow, seven barges at least one tanker or gas barge"));
309 make_hash_ERI(8380, _("Pushtow, eight barges at least one tanker or gas barge"));
310 make_hash_ERI(8390, _("Pushtow, nine or more barges at least one tanker or gas barge"));
311 make_hash_ERI(8400, _("Tug, single"));
312 make_hash_ERI(8410, _("Tug, one or more tows"));
313 make_hash_ERI(8420, _("Tug, assisting a vessel or linked combination"));
314 make_hash_ERI(8430, _("Pushboat, single"));
315 make_hash_ERI(8440, _("Passenger ship, ferry, cruise ship, red cross ship"));
316 make_hash_ERI(8441, _("Ferry"));
317 make_hash_ERI(8442, _("Red cross ship"));
318 make_hash_ERI(8443, _("Cruise ship"));
319 make_hash_ERI(8444, _("Passenger ship without accommodation"));
320 make_hash_ERI(8460, _("Vessel, work maintenance craft, floating derrick, cable-ship, buoy-ship, dredge"));
321 make_hash_ERI(8480, _("Fishing boat"));
322 make_hash_ERI(8500, _("Barge, tanker, chemical"));
323 make_hash_ERI(1500, _("General cargo Vessel maritime"));
324 make_hash_ERI(1510, _("Unit carrier maritime"));
325 make_hash_ERI(1520, _("Bulk carrier maritime"));
326 make_hash_ERI(1530, _("Tanker"));
327 make_hash_ERI(1540, _("Liquified gas tanker"));
328 make_hash_ERI(1850, _("Pleasure craft, longer than 20 metres"));
329 make_hash_ERI(1900, _("Fast ship"));
330 make_hash_ERI(1910, _("Hydrofoil"));
331 }
332
333 //----------------------------------------------------------------------------------
334 // Handle events from AIS DataStream
335 //----------------------------------------------------------------------------------
OnEvtAIS(OCPN_DataStreamEvent & event)336 void AIS_Decoder::OnEvtAIS( OCPN_DataStreamEvent& event )
337 {
338 wxString message = event.ProcessNMEA4Tags();
339
340 int nr = 0;
341 if( !message.IsEmpty() )
342 {
343 if( message.Mid( 3, 3 ).IsSameAs( _T("VDM") ) ||
344 message.Mid( 3, 3 ).IsSameAs( _T("VDO") ) ||
345 message.Mid( 1, 5 ).IsSameAs( _T("FRPOS") ) ||
346 message.Mid( 1, 2 ).IsSameAs( _T("CD") ) ||
347 message.Mid( 3, 3 ).IsSameAs( _T("TLL") ) ||
348 message.Mid( 3, 3 ).IsSameAs( _T("TTM") ) ||
349 message.Mid( 3, 3 ).IsSameAs( _T("OSD") ) ||
350 ( g_bWplIsAprsPosition && message.Mid( 3, 3 ).IsSameAs( _T("WPL") ) ) )
351 {
352 nr = Decode( message );
353 if( nr == AIS_NoError ) {
354 g_pi_manager->SendAISSentenceToAllPlugIns(message);
355 }
356 gFrame->TouchAISActive();
357 }
358 }
359 }
360
361 //----------------------------------------------------------------------------------
362 // Handle events from SignalK DataStream
363 //----------------------------------------------------------------------------------
OnEvtSignalK(OCPN_SignalKEvent & event)364 void AIS_Decoder::OnEvtSignalK(OCPN_SignalKEvent &event)
365 {
366 wxJSONReader jsonReader;
367 wxJSONValue root;
368
369 std::string msgTerminated = event.GetString();
370 msgTerminated.append("\r\n");
371
372 int errors = jsonReader.Parse(msgTerminated, &root);
373 if(errors > 0)
374 return;
375
376 if(root.HasMember(_T("self"))) {
377 //m_signalk_selfid = _T("vessels.") + (root["self"].AsString());
378 m_signalk_selfid = (root["self"].AsString()); // Verified for OpenPlotter node.js server 1.20
379 }
380 if(m_signalk_selfid.IsEmpty()) {
381 return; // Don't handle any messages (with out self) until we know how we are
382 }
383 long mmsi = 0;
384 if(root.HasMember(_T("context"))
385 && root[_T("context")].IsString()) {
386 auto context = root[_T("context")].AsString();
387 if (context == m_signalk_selfid) {
388 #if 0
389 wxLogMessage(_T("** Ignore context own ship.."));
390 #endif
391 return;
392 }
393 wxString mmsi_string;
394 if(context.StartsWith(_T("vessels.urn:mrn:imo:mmsi:"), &mmsi_string) ||
395 context.StartsWith(_T("atons.urn:mrn:imo:mmsi:"), &mmsi_string) ||
396 context.StartsWith(_T("aircraft.urn:mrn:imo:mmsi:"), &mmsi_string) ) {
397 // wxLogMessage(wxString::Format(_T("Context: %s, %s"), context.c_str(), mmsi_string));
398 if(mmsi_string.ToLong(&mmsi)) {
399 //wxLogMessage(_T("Got MMSI from context."));
400 } else {
401 mmsi = 0;
402 }
403 }
404 }
405 if(mmsi == 0) {
406 return; // Only handle ships with MMSI for now
407 }
408 #if 0
409 wxString dbg;
410 wxJSONWriter writer;
411 writer.Write(root, dbg);
412
413 wxString msg( _T("AIS_Decoder::OnEvtSignalK: ") );
414 msg.append(dbg);
415 wxLogMessage(msg);
416 #endif
417 AIS_Target_Data *pTargetData = 0;
418 AIS_Target_Data *pStaleTarget = NULL;
419 bool bnewtarget = false;
420 int last_report_ticks;
421 wxDateTime now;
422 getAISTarget(mmsi, pTargetData, pStaleTarget, bnewtarget, last_report_ticks, now);
423 if(pTargetData) {
424 if (root.HasMember(_T("updates"))
425 && root[_T("updates")].IsArray()) {
426 wxJSONValue &updates = root[_T("updates")];
427 for (int i = 0; i < updates.Size(); ++i) {
428 handleUpdate(pTargetData, bnewtarget, updates[i]);
429 }
430 }
431 pTargetData->MMSI = mmsi;
432 // A SART can send wo any values first transmits. Detect class already here.
433 if ( 97 == mmsi / 10000000 ) { pTargetData->Class = AIS_SART; }
434 pTargetData->b_OwnShip = false;
435 ( *AISTargetList )[pTargetData->MMSI] = pTargetData;
436 }
437 }
438
handleUpdate(AIS_Target_Data * pTargetData,bool bnewtarget,wxJSONValue & update)439 void AIS_Decoder::handleUpdate(AIS_Target_Data *pTargetData,
440 bool bnewtarget,
441 wxJSONValue &update)
442 {
443 wxString sfixtime = "";
444
445 if(update.HasMember(_T("timestamp"))) {
446 sfixtime = update[_T("timestamp")].AsString();
447 }
448 if(update.HasMember(_T("values"))
449 && update[_T("values")].IsArray())
450 {
451 for (int j = 0; j < update[_T("values")].Size(); ++j) {
452 wxJSONValue &item = update[_T("values")][j];
453 updateItem(pTargetData, bnewtarget, item, sfixtime);
454 }
455 }
456 wxDateTime now = wxDateTime::Now();
457 pTargetData->m_utc_hour = now.ToUTC().GetHour();
458 pTargetData->m_utc_min = now.ToUTC().GetMinute();
459 pTargetData->m_utc_sec = now.ToUTC().GetSecond();
460 // pTargetData->NavStatus = 15; // undefined
461 pTargetData->b_active = true;
462 pTargetData->b_lost = false;
463
464 if( pTargetData->b_positionOnceValid ) {
465 long mmsi_long = pTargetData->MMSI;
466 SelectItem *pSel = pSelectAIS->AddSelectablePoint( pTargetData->Lat,
467 pTargetData->Lon,
468 (void *) mmsi_long,
469 SELTYPE_AISTARGET );
470 pSel->SetUserData( pTargetData->MMSI );
471 }
472 UpdateOneCPA(pTargetData);
473 if( pTargetData->b_show_track )
474 UpdateOneTrack( pTargetData );
475
476 }
477
updateItem(AIS_Target_Data * pTargetData,bool bnewtarget,wxJSONValue & item,wxString & sfixtime) const478 void AIS_Decoder::updateItem(AIS_Target_Data *pTargetData,
479 bool bnewtarget,
480 wxJSONValue &item,
481 wxString &sfixtime) const
482 {
483 if (item.HasMember(_T("path"))
484 && item.HasMember(_T("value"))) {
485
486 const wxString &update_path = item[_T("path")].AsString();
487 wxJSONValue &value = item[_T("value")];
488 if (update_path == _T("navigation.position")) {
489 if (value.HasMember(_T("latitude"))
490 && value.HasMember(_T("longitude"))) {
491 wxDateTime now = wxDateTime::Now();
492 now.MakeUTC();
493 double lat = value[_T("latitude")].AsDouble();
494 double lon = value[_T("longitude")].AsDouble();
495 pTargetData->PositionReportTicks = now.GetTicks();
496 pTargetData->StaticReportTicks = now.GetTicks();
497 pTargetData->Lat = lat;
498 pTargetData->Lon = lon;
499 pTargetData->b_positionOnceValid = true;
500 pTargetData->b_positionDoubtful = false;
501 }
502
503 if ( value.HasMember(_T("altitude")) ) {
504 pTargetData->altitude = value[_T("altitude ")].AsInt(); }
505 } else if (update_path == _T("navigation.speedOverGround")) {
506 pTargetData->SOG = value.AsDouble() * ms_to_knot_factor;
507 } else if (update_path == _T("navigation.courseOverGroundTrue")) {
508 pTargetData->COG = GEODESIC_RAD2DEG(value.AsDouble());
509 } else if (update_path == _T("navigation.headingTrue")) {
510 pTargetData->HDG = GEODESIC_RAD2DEG(value.AsDouble());
511 } else if (update_path == _T("navigation.rateOfTurn")) {
512 pTargetData->ROTAIS = 4.733*sqrt(value.AsDouble());
513 } else if (update_path == _T("design.aisShipType")) {
514 if (value.HasMember(_T("id"))) {
515 pTargetData->ShipType = value[_T("id")].AsUInt();
516 }
517 } else if (update_path == _T("atonType")) {
518 if (value.HasMember(_T("id"))) {
519 pTargetData->ShipType = value[_T("id")].AsUInt();
520 }
521 } else if (update_path == _T("virtual")) {
522 if (_T("true") == value.AsString()) {
523 pTargetData->NavStatus = ATON_VIRTUAL; }
524 else { pTargetData->NavStatus = ATON_REAL; }
525 } else if (update_path == _T("offPosition")) {
526 if (_T("true") == value.AsString()) {
527 if (ATON_REAL == pTargetData->NavStatus) {
528 pTargetData->NavStatus = ATON_REAL_OFFPOSITION;
529 }
530 else if (ATON_VIRTUAL == pTargetData->NavStatus) {
531 pTargetData->NavStatus = ATON_VIRTUAL_OFFPOSITION;
532 }
533 }
534 } else if (update_path == _T("design.draft")) {
535 if (value.HasMember(_T("maximum"))) {
536 pTargetData->Draft = value[_T("maximum")].AsDouble();
537 pTargetData->Euro_Draft = value[_T("maximum")].AsDouble();
538 }
539 if (value.HasMember(_T("current"))) {
540 double draft = value[_T("current")].AsDouble();
541 if (draft > 0) {
542 pTargetData->Draft = draft;
543 pTargetData->Euro_Draft = draft;
544 }
545 }
546 } else if (update_path == _T("design.length")) {
547 if (pTargetData->DimB == 0) {
548 if (value.HasMember(_T("overall"))) {
549 pTargetData->Euro_Length = value[_T("overall")].AsDouble();
550 pTargetData->DimA = value[_T("overall")].AsInt();
551 pTargetData->DimB = 0;
552 }
553 }
554 } else if (update_path == _T("sensors.ais.class")) {
555 auto aisclass = value.AsString();
556 if (aisclass == _T("A") ) { pTargetData->Class = AIS_CLASS_A; }
557 else if (aisclass == _T("B")) {
558 pTargetData->Class = AIS_CLASS_B;
559 pTargetData->NavStatus = UNDEFINED; // Class B targets have no status. Enforce this...
560 }
561 else if (aisclass == _T("BASE")) { pTargetData->Class = AIS_BASE; }
562 else if (aisclass == _T("ATON")) { pTargetData->Class = AIS_ATON; }
563 } else if (update_path == _T("sensors.ais.fromBow")) {
564 if(pTargetData->DimB == 0 && pTargetData->DimA != 0) {
565 int length = pTargetData->DimA;
566 pTargetData->DimA = value.AsInt();
567 pTargetData->DimB = length - value.AsInt();
568 }
569 } else if (update_path == _T("design.beam")) {
570 if (pTargetData->DimD == 0) {
571 pTargetData->Euro_Beam = value.AsDouble();
572 pTargetData->DimC = value.AsInt();
573 pTargetData->DimD = 0;
574 }
575 } else if (update_path == _T("sensors.ais.fromCenter")) {
576 if(pTargetData->DimD == 0 && pTargetData->DimC != 0) {
577 int beam = pTargetData->DimC;
578 int center = beam / 2;
579 pTargetData->DimC = center + value.AsInt();
580 pTargetData->DimD = beam - pTargetData->DimC;
581 }
582 } else if (update_path == _T("navigation.state")) {
583 auto state = value.AsString();
584 if (state == _T("motoring")) { pTargetData->NavStatus = UNDERWAY_USING_ENGINE; }
585 else if (state == _T("anchored")) { pTargetData->NavStatus = AT_ANCHOR; }
586 else if (state == _T("not under command")) { pTargetData->NavStatus = NOT_UNDER_COMMAND; }
587 else if (state == _T("restricted manouverability")) { pTargetData->NavStatus = RESTRICTED_MANOEUVRABILITY; }
588 else if (state == _T("constrained by draft")) { pTargetData->NavStatus = CONSTRAINED_BY_DRAFT; }
589 else if (state == _T("moored")) { pTargetData->NavStatus = MOORED; }
590 else if (state == _T("aground")) { pTargetData->NavStatus = AGROUND; }
591 else if (state == _T("fishing")) { pTargetData->NavStatus = FISHING; }
592 else if (state == _T("sailing")) { pTargetData->NavStatus = UNDERWAY_SAILING; }
593 else if (state == _T("hazardous material high speed")) { pTargetData->NavStatus = HSC; }
594 else if (state == _T("hazardous material wing in ground")) { pTargetData->NavStatus = WIG; }
595 else if (state == _T("ais-sart")) { pTargetData->NavStatus = RESERVED_14; }
596 else { pTargetData->NavStatus = UNDEFINED; }
597 } else if (update_path == _T("navigation.destination.commonName")) {
598 const wxString &destination = value.AsString();
599 strncpy(pTargetData->Destination,
600 destination.c_str(), 20);
601 } else if (update_path == _T("navigation.specialManeuver")) {
602 if (_T("not available") != value.AsString() && pTargetData->IMO < 1) {
603 const wxString &bluesign = value.AsString();
604 if ( _T("not engaged")== bluesign){
605 pTargetData->blue_paddle = 1;
606 }
607 if (_T("engaged") == bluesign) {
608 pTargetData->blue_paddle = 2;
609 }
610 pTargetData->b_blue_paddle = pTargetData->blue_paddle == 2 ? true: false;
611 }
612 } else if (update_path == _T("sensors.ais.designatedAreaCode")) {
613 if (value.AsInt() == 200) { pTargetData->b_hasInlandDac = true; } // European inland
614 } else if (update_path == _T("sensors.ais.functionalId")) {
615 if (value.AsInt() == 10 && // "Inland ship static and voyage related data"
616 pTargetData->b_hasInlandDac) {
617 pTargetData->b_isEuroInland = true;
618 }
619 } else if (update_path == _T("")) {
620 if(value.HasMember(_T("name"))) {
621 const wxString &name = value[_T("name")].AsString();
622 strncpy(pTargetData->ShipName, name.c_str(), 20 );
623 pTargetData->b_nameValid = true;
624 pTargetData->MID = 123; // Indicates a name from SignalK
625 } else if (value.HasMember(_T("registrations"))) {
626 const wxString &imo = value[_T("registrations")][_T("imo")].AsString();
627 pTargetData->IMO = wxAtoi(imo.Right(7));
628 } else if (value.HasMember(_T("communication"))) {
629 const wxString &callsign = value[_T("communication")][_T("callsignVhf")].AsString();
630 strncpy(pTargetData->CallSign, callsign.c_str(), 7);
631 }
632 if(value.HasMember("mmsi")) {
633 long mmsi;
634 if (value[_T("mmsi")].AsString().ToLong(&mmsi)) {
635 pTargetData->MMSI = mmsi;
636
637 if (97 == mmsi / 10000000) {
638 pTargetData->Class = AIS_SART;
639 }
640 if (111 == mmsi / 1000000) {
641 pTargetData->b_SarAircraftPosnReport = true;
642 }
643
644 AISshipNameCache(pTargetData, AISTargetNamesC, AISTargetNamesNC, mmsi);
645 }
646 }
647 } else {
648 wxLogMessage(wxString::Format(_T("** AIS_Decoder::updateItem: unhandled path %s"), update_path));
649 #if 1
650 wxString dbg;
651 wxJSONWriter writer;
652 writer.Write(item, dbg);
653 wxString msg( _T("update: ") );
654 msg.append(dbg);
655 wxLogMessage(msg);
656 #endif
657
658 }
659 }
660 }
661
662 //----------------------------------------------------------------------------------
663 // Decode a single AIVDO sentence to a Generic Position Report
664 //----------------------------------------------------------------------------------
DecodeSingleVDO(const wxString & str,GenericPosDatEx * pos,wxString * accumulator)665 AIS_Error AIS_Decoder::DecodeSingleVDO( const wxString& str, GenericPosDatEx *pos, wxString *accumulator )
666 {
667 // Make some simple tests for validity
668 if( str.Len() > 100 )
669 return AIS_NMEAVDX_TOO_LONG;
670
671 if( !NMEACheckSumOK( str ) )
672 return AIS_NMEAVDX_CHECKSUM_BAD;
673
674 if( !pos )
675 return AIS_GENERIC_ERROR;
676
677 if( !accumulator )
678 return AIS_GENERIC_ERROR;
679
680 // We only process AIVDO messages
681 if( !str.Mid( 1, 5 ).IsSameAs( _T("AIVDO") ) )
682 return AIS_GENERIC_ERROR;
683
684 // Use a tokenizer to pull out the first 4 fields
685 wxStringTokenizer tkz( str, _T(",") );
686
687 wxString token;
688 token = tkz.GetNextToken(); // !xxVDx
689
690 token = tkz.GetNextToken();
691 int nsentences = atoi( token.mb_str() );
692
693 token = tkz.GetNextToken();
694 int isentence = atoi( token.mb_str() );
695
696 token = tkz.GetNextToken(); // skip 2 fields
697 token = tkz.GetNextToken();
698
699 wxString string_to_parse;
700 string_to_parse.Clear();
701
702 // Fill the output structure with all NANs
703 pos->kLat = NAN;
704 pos->kLon = NAN;
705 pos->kCog = NAN;
706 pos->kSog = NAN;
707 pos->kHdt = NAN;
708 pos->kVar = NAN;
709 pos->kHdm = NAN;
710
711 // Simple case first
712 // First and only part of a one-part sentence
713 if( ( 1 == nsentences ) && ( 1 == isentence ) ) {
714 string_to_parse = tkz.GetNextToken(); // the encapsulated data
715 }
716
717 else if( nsentences > 1 ) {
718 if( 1 == isentence ) {
719 *accumulator = tkz.GetNextToken(); // the encapsulated data
720 }
721
722 else {
723 accumulator->Append(tkz.GetNextToken() );
724 }
725
726 if( isentence == nsentences ) { // ready to parse
727 string_to_parse = *accumulator;
728 }
729 }
730
731 if( string_to_parse.IsEmpty() && (nsentences > 1) ) { // not ready, so return with NAN
732 return AIS_INCOMPLETE_MULTIPART; // and non-zero return
733 }
734
735
736 // Create the bit accessible string
737 AIS_Bitstring strbit( string_to_parse.mb_str() );
738
739 AIS_Target_Data TargetData;
740
741 bool bdecode_result = Parse_VDXBitstring( &strbit, &TargetData );
742
743 if(bdecode_result) {
744 switch(TargetData.MID)
745 {
746 case 1:
747 case 2:
748 case 3:
749 case 18:
750 {
751 if( !TargetData.b_positionDoubtful ) {
752 pos->kLat = TargetData.Lat;
753 pos->kLon = TargetData.Lon;
754 }
755 else {
756 pos->kLat = NAN;
757 pos->kLon = NAN;
758 }
759
760 if(TargetData.COG == 360.0)
761 pos->kCog = NAN;
762 else
763 pos->kCog = TargetData.COG;
764
765
766 if(TargetData.SOG > 102.2)
767 pos->kSog = NAN;
768 else
769 pos->kSog = TargetData.SOG;
770
771 if((int)TargetData.HDG == 511)
772 pos->kHdt = NAN;
773 else
774 pos->kHdt = TargetData.HDG;
775
776 // VDO messages do not contain variation or magnetic heading
777 pos->kVar = NAN;
778 pos->kHdm = NAN;
779 break;
780 }
781 default:
782 return AIS_GENERIC_ERROR; // unrecognised sentence
783 }
784
785 return AIS_NoError;
786 }
787 else
788 return AIS_GENERIC_ERROR;
789 }
790
791
792 //----------------------------------------------------------------------------------------
793 // Decode NMEA VDM/VDO/FRPOS/DSCDSE/TTM/TLL/OSD/RSD/TLB/WPL sentence to AIS Target(s)
794 //----------------------------------------------------------------------------------------
Decode(const wxString & str)795 AIS_Error AIS_Decoder::Decode( const wxString& str )
796 {
797 AIS_Error ret = AIS_GENERIC_ERROR;
798 wxString string_to_parse;
799
800 double gpsg_lat, gpsg_lon, gpsg_mins, gpsg_degs;
801 double gpsg_cog, gpsg_sog, gpsg_utc_time;
802 int gpsg_utc_hour = 0;
803 int gpsg_utc_min = 0;
804 int gpsg_utc_sec = 0;
805 char gpsg_name_str[21];
806 wxString gpsg_date;
807
808 bool bdecode_result = false;
809
810 int gpsg_mmsi = 0;
811 int arpa_mmsi = 0;
812 int aprs_mmsi = 0;
813 int mmsi = 0;
814
815 long arpa_tgt_num = 0;
816 double arpa_sog = 0.;
817 double arpa_cog = 0.;
818 double arpa_lat = 0.;
819 double arpa_lon = 0.;
820 double arpa_dist = 0.;
821 double arpa_brg = 0.;
822 wxString arpa_brgunit;
823 wxString arpa_status;
824 wxString arpa_distunit;
825 wxString arpa_cogunit;
826 wxString arpa_reftarget;
827 double arpa_mins, arpa_degs;
828 double arpa_utc_time;
829 int arpa_utc_hour = 0;
830 int arpa_utc_min = 0;
831 int arpa_utc_sec = 0;
832 char arpa_name_str[21];
833 bool arpa_lost = true;
834 bool arpa_nottracked = false;
835
836 double aprs_lat = 0.;
837 double aprs_lon = 0.;
838 char aprs_name_str[21];
839 double aprs_mins, aprs_degs;
840
841 AIS_Target_Data *pTargetData = 0;
842 AIS_Target_Data *pStaleTarget = NULL;
843 bool bnewtarget = false;
844 int last_report_ticks;
845
846 // Make some simple tests for validity
847
848 if( str.Len() > 100 ) return AIS_NMEAVDX_TOO_LONG;
849
850 if( !NMEACheckSumOK( str ) ) {
851 return AIS_NMEAVDX_CHECKSUM_BAD;
852 }
853 if( str.Mid( 1, 2 ).IsSameAs( _T("CD") ) ) {
854 ProcessDSx( str );
855 return AIS_NoError;
856 }
857 else if( str.Mid( 3, 3 ).IsSameAs( _T("TTM") ) ) {
858 //$--TTM,xx,x.x,x.x,a,x.x,x.x,a,x.x,x.x,a,c--c,a,a*hh <CR><LF>
859 //or
860 //$--TTM,xx,x.x,x.x,a,x.x,x.x,a,x.x,x.x,a,c--c,a,a,hhmmss.ss,a*hh<CR><LF>
861 wxString string( str );
862 wxStringTokenizer tkz( string, _T(",*") );
863
864 wxString token;
865 token = tkz.GetNextToken(); //Sentence (xxTTM)
866 token = tkz.GetNextToken(); //1) Target Number
867 token.ToLong( &arpa_tgt_num );
868 token = tkz.GetNextToken(); // 2)Target Distance
869 token.ToDouble( &arpa_dist );
870 token = tkz.GetNextToken(); //3) Bearing from own ship
871 token.ToDouble( &arpa_brg );
872 arpa_brgunit = tkz.GetNextToken(); //4) Bearing Units
873 if ( arpa_brgunit == _T("R") )
874 {
875 if ( std::isnan(arpa_ref_hdg) )
876 {
877 if ( !std::isnan(gHdt) )
878 arpa_brg += gHdt;
879 else
880 arpa_brg += gCog;
881 }
882 else
883 arpa_brg += arpa_ref_hdg;
884 if ( arpa_brg >= 360. )
885 arpa_brg -= 360.;
886 }
887 token = tkz.GetNextToken(); //5) Target speed
888 token.ToDouble( &arpa_sog );
889 token = tkz.GetNextToken(); //6) Target Course
890 token.ToDouble( &arpa_cog );
891 arpa_cogunit = tkz.GetNextToken(); //7) Course Units
892 if ( arpa_cogunit == _T("R") )
893 {
894 if ( std::isnan(arpa_ref_hdg) )
895 {
896 if ( !std::isnan(gHdt) )
897 arpa_cog += gHdt;
898 else
899 arpa_cog += gCog;
900 }
901 else
902 arpa_cog += arpa_ref_hdg;
903 if ( arpa_cog >= 360. )
904 arpa_cog -= 360.;
905 }
906 token = tkz.GetNextToken(); //8) Distance of closest-point-of-approach
907 token = tkz.GetNextToken(); //9) Time until closest-point-of-approach "-" means increasing
908 arpa_distunit = tkz.GetNextToken(); //10)Speed/ dist unit
909 token = tkz.GetNextToken(); //11) Target name
910 if ( token == wxEmptyString )
911 token = wxString::Format( _T("ARPA %d"), arpa_tgt_num );
912 int len = wxMin(token.Length(),20);
913 strncpy( arpa_name_str, token.mb_str(), len );
914 arpa_name_str[len] = 0;
915 arpa_status = tkz.GetNextToken(); //12) Target Status
916 if ( arpa_status != _T( "L" ) ) {
917 arpa_lost = false;
918 } else if ( arpa_status != wxEmptyString )
919 arpa_nottracked = true;
920 arpa_reftarget = tkz.GetNextToken(); //13) Reference Target
921 if ( tkz.HasMoreTokens() )
922 {
923 token = tkz.GetNextToken();
924 token.ToDouble( &arpa_utc_time );
925 arpa_utc_hour = (int) ( arpa_utc_time / 10000.0 );
926 arpa_utc_min = (int) ( arpa_utc_time / 100.0 ) - arpa_utc_hour * 100;
927 arpa_utc_sec = (int) arpa_utc_time - arpa_utc_hour * 10000 - arpa_utc_min * 100;
928 } else {
929 arpa_utc_hour = wxDateTime::Now().ToUTC().GetHour();
930 arpa_utc_min = wxDateTime::Now().ToUTC().GetMinute();
931 arpa_utc_sec = wxDateTime::Now().ToUTC().GetSecond();
932 }
933
934 if( arpa_distunit == _T("K") )
935 {
936 arpa_dist = fromUsrDistance( arpa_dist, DISTANCE_KM );
937 arpa_sog = fromUsrSpeed(arpa_sog, SPEED_KMH);
938 } else if( arpa_distunit == _T("S") ) {
939 arpa_dist = fromUsrDistance( arpa_dist, DISTANCE_MI );
940 arpa_sog = fromUsrSpeed(arpa_sog, SPEED_MPH);
941 }
942
943 mmsi = arpa_mmsi = 199200000 + arpa_tgt_num; // 199 is INMARSAT-A MID, should not occur ever in AIS stream + we make sure we are out of the hashes for GPSGate buddies by being above 1992*
944 } else if( str.Mid( 3, 3 ).IsSameAs( _T("TLL") ) ) {
945 //$--TLL,xx,llll.lll,a,yyyyy.yyy,a,c--c,hhmmss.ss,a,a*hh<CR><LF>
946 //"$RATLL,01,5603.370,N,01859.976,E,ALPHA,015200.36,T,*75\r\n"
947 wxString aprs_tll_str;
948 wxString string( str );
949 wxStringTokenizer tkz( string, _T(",*") );
950
951 wxString token;
952 aprs_tll_str = tkz.GetNextToken(); //Sentence (xxTLL)
953 token = tkz.GetNextToken(); //1) Target number 00 - 99
954 token.ToLong( &arpa_tgt_num );
955 token = tkz.GetNextToken(); //2) Latitude, N/S
956 token.ToDouble( &arpa_lat );
957 arpa_degs = (int) ( arpa_lat / 100.0 );
958 arpa_mins = arpa_lat - arpa_degs * 100.0;
959 arpa_lat = arpa_degs + arpa_mins / 60.0;
960 token = tkz.GetNextToken(); // hemisphere N or S
961 if( token.Mid( 0, 1 ).Contains( _T("S") ) == true || token.Mid( 0, 1 ).Contains( _T("s") ) == true )
962 arpa_lat = 0. - arpa_lat;
963 token = tkz.GetNextToken(); //3) Longitude, E/W
964 token.ToDouble( &arpa_lon );
965 arpa_degs = (int) ( arpa_lon / 100.0 );
966 arpa_mins = arpa_lon - arpa_degs * 100.0;
967 arpa_lon = arpa_degs + arpa_mins / 60.0;
968 token = tkz.GetNextToken(); // hemisphere E or W
969 if( token.Mid( 0, 1 ).Contains( _T("W") ) == true || token.Mid( 0, 1 ).Contains( _T("w") ) == true )
970 arpa_lon = 0. - arpa_lon;
971 token = tkz.GetNextToken(); //4) Target name
972 if ( token == wxEmptyString )
973 token = wxString::Format( _T("ARPA %d"), arpa_tgt_num );
974 int len = wxMin(token.Length(),20);
975 strncpy( arpa_name_str, token.mb_str(), len );
976 arpa_name_str[len] = 0;
977 token = tkz.GetNextToken(); //5) UTC of data
978 token.ToDouble( &arpa_utc_time );
979 arpa_utc_hour = (int) ( arpa_utc_time / 10000.0 );
980 arpa_utc_min = (int) ( arpa_utc_time / 100.0 ) - arpa_utc_hour * 100;
981 arpa_utc_sec = (int) arpa_utc_time - arpa_utc_hour * 10000 - arpa_utc_min * 100;
982 arpa_status = tkz.GetNextToken(); //6) Target status: L = lost,tracked target has beenlost Q = query,target in the process of acquisition T = tracking
983 if( arpa_status != _T("L") )
984 arpa_lost = false;
985 else if ( arpa_status != wxEmptyString )
986 arpa_nottracked = true;
987 arpa_reftarget = tkz.GetNextToken(); //7) Reference target=R,null otherwise
988 mmsi = arpa_mmsi = 199200000 + arpa_tgt_num; // 199 is INMARSAT-A MID, should not occur ever in AIS stream + we make sure we are out of the hashes for GPSGate buddies by being above 1992*
989 } else if( str.Mid( 3, 3 ).IsSameAs( _T("OSD") ) ) {
990 //$--OSD,x.x,A,x.x,a,x.x,a,x.x,x.x,a*hh <CR><LF>
991 wxString string( str );
992 wxStringTokenizer tkz( string, _T(",*") );
993
994 wxString token;
995 token = tkz.GetNextToken(); //Sentence (xxOSD)
996 token = tkz.GetNextToken(); //1) Heading (true)
997 token.ToDouble( &arpa_ref_hdg );
998 //2) speed
999 //3) Vessel Course, degrees True
1000 //4) Course Reference, B/M/W/R/P (see note)
1001 //5) Vessel Speed
1002 //6) Speed Reference, B/M/W/R/P (see note)
1003 //7) Vessel Set, degrees True - Manually entered
1004 //8) Vessel drift (speed) - Manually entered
1005 //9) Speed Units K = km/h; N = Knots; S = statute miles/h
1006 } else if( str.Mid( 3, 3 ).IsSameAs( _T("WPL") ) ) {
1007 //** $--WPL,llll.ll,a,yyyyy.yy,a,c--c*hh<CR><LF>
1008 wxString string( str );
1009 wxStringTokenizer tkz( string, _T(",*") );
1010
1011 wxString token;
1012 token = tkz.GetNextToken(); //Sentence (xxWPL)
1013 token = tkz.GetNextToken(); //1) Latitude, N/S
1014 token.ToDouble( &aprs_lat );
1015 aprs_degs = (int) ( aprs_lat / 100.0 );
1016 aprs_mins = aprs_lat - aprs_degs * 100.0;
1017 aprs_lat = aprs_degs + aprs_mins / 60.0;
1018 token = tkz.GetNextToken(); //2) hemisphere N or S
1019 if( token.Mid( 0, 1 ).Contains( _T("S") ) == true || token.Mid( 0, 1 ).Contains( _T("s") ) == true )
1020 aprs_lat = 0. - aprs_lat;
1021 token = tkz.GetNextToken(); //3) Longitude, E/W
1022 token.ToDouble( &aprs_lon );
1023 aprs_degs = (int) ( aprs_lon / 100.0 );
1024 aprs_mins = aprs_lon - aprs_degs * 100.0;
1025 aprs_lon = aprs_degs + aprs_mins / 60.0;
1026 token = tkz.GetNextToken(); //4) hemisphere E or W
1027 if( token.Mid( 0, 1 ).Contains( _T("W") ) == true || token.Mid( 0, 1 ).Contains( _T("w") ) == true )
1028 aprs_lon = 0. - aprs_lon;
1029 token = tkz.GetNextToken(); //5) Target name
1030 int len = wxMin(token.Length(),20);
1031 int i, hash = 0;
1032 strncpy( aprs_name_str, token.mb_str(), len );
1033 aprs_name_str[len] = 0;
1034 hash = 0;
1035 for( i = 0; i < len; i++ ) {
1036 hash = hash * 10;
1037 hash += (int) ( aprs_name_str[i] );
1038 while( hash >= 100000 )
1039 hash = hash / 100000;
1040 }
1041 mmsi = aprs_mmsi = 199300000 + hash; // 199 is INMARSAT-A MID, should not occur ever in AIS stream + we make sure we are out of the hashes for GPSGate buddies and ARPA by being above 1993*
1042 } else if( str.Mid( 1, 5 ).IsSameAs( _T("FRPOS") ) ) {
1043 // parse a GpsGate Position message $FRPOS,.....
1044
1045 // Use a tokenizer to pull out the first 9 fields
1046 wxString string( str );
1047 wxStringTokenizer tkz( string, _T(",*") );
1048
1049 wxString token;
1050 token = tkz.GetNextToken(); // !$FRPOS
1051
1052 token = tkz.GetNextToken(); // latitude DDMM.MMMM
1053 token.ToDouble( &gpsg_lat );
1054 gpsg_degs = (int) ( gpsg_lat / 100.0 );
1055 gpsg_mins = gpsg_lat - gpsg_degs * 100.0;
1056 gpsg_lat = gpsg_degs + gpsg_mins / 60.0;
1057
1058 token = tkz.GetNextToken(); // hemisphere N or S
1059 if( token.Mid( 0, 1 ).Contains( _T("S") ) == true || token.Mid( 0, 1 ).Contains( _T("s") ) == true ) gpsg_lat = 0. - gpsg_lat;
1060
1061 token = tkz.GetNextToken(); // longitude DDDMM.MMMM
1062 token.ToDouble( &gpsg_lon );
1063 gpsg_degs = (int) ( gpsg_lon / 100.0 );
1064 gpsg_mins = gpsg_lon - gpsg_degs * 100.0;
1065 gpsg_lon = gpsg_degs + gpsg_mins / 60.0;
1066
1067 token = tkz.GetNextToken(); // hemisphere E or W
1068 if( token.Mid( 0, 1 ).Contains( _T("W") ) == true || token.Mid( 0, 1 ).Contains( _T("w") ) == true ) gpsg_lon = 0. - gpsg_lon;
1069
1070 token = tkz.GetNextToken(); // altitude AA.a
1071 // token.toDouble(&gpsg_alt);
1072
1073 token = tkz.GetNextToken(); // speed over ground SSS.SS knots
1074 token.ToDouble( &gpsg_sog );
1075
1076 token = tkz.GetNextToken(); // heading over ground HHH.hh degrees
1077 token.ToDouble( &gpsg_cog );
1078
1079 token = tkz.GetNextToken(); // date DDMMYY
1080 gpsg_date = token;
1081
1082 token = tkz.GetNextToken(); // time UTC hhmmss.dd
1083 token.ToDouble( &gpsg_utc_time );
1084 gpsg_utc_hour = (int) ( gpsg_utc_time / 10000.0 );
1085 gpsg_utc_min = (int) ( gpsg_utc_time / 100.0 ) - gpsg_utc_hour * 100;
1086 gpsg_utc_sec = (int) gpsg_utc_time - gpsg_utc_hour * 10000 - gpsg_utc_min * 100;
1087
1088 // now comes the name, followed by in * and NMEA checksum
1089
1090 token = tkz.GetNextToken();
1091 int i, len, hash = 0;
1092 len = wxMin(wxStrlen(token),20);
1093 strncpy( gpsg_name_str, token.mb_str(), len );
1094 gpsg_name_str[len] = 0;
1095 for( i = 0; i < len; i++ ) {
1096 hash = hash * 10;
1097 hash += (int) ( token[i] );
1098 while( hash >= 100000 )
1099 hash = hash / 100000;
1100 }
1101 gpsg_mmsi = 199000000 + hash; // 199 is INMARSAT-A MID, should not occur ever in AIS stream
1102 mmsi = gpsg_mmsi;
1103 } else if( !str.Mid( 3, 2 ).IsSameAs( _T("VD") ) ) {
1104 return AIS_NMEAVDX_BAD;
1105 }
1106
1107 // OK, looks like the sentence is OK
1108
1109 // Use a tokenizer to pull out the first 4 fields
1110 wxString string( str );
1111 wxStringTokenizer tkz( string, _T(",") );
1112
1113 wxString token;
1114 token = tkz.GetNextToken(); // !xxVDx
1115
1116 token = tkz.GetNextToken();
1117 nsentences = atoi( token.mb_str() );
1118
1119 token = tkz.GetNextToken();
1120 isentence = atoi( token.mb_str() );
1121
1122 token = tkz.GetNextToken();
1123 long lsequence_id = 0;
1124 token.ToLong( &lsequence_id );
1125
1126 token = tkz.GetNextToken();
1127 long lchannel;
1128 token.ToLong( &lchannel );
1129 // Now, some decisions
1130
1131 string_to_parse.Clear();
1132
1133 // Simple case first
1134 // First and only part of a one-part sentence
1135 if( ( 1 == nsentences ) && ( 1 == isentence ) ) {
1136 string_to_parse = tkz.GetNextToken(); // the encapsulated data
1137 }
1138
1139 else if( nsentences > 1 ) {
1140 if( 1 == isentence ) {
1141 sentence_accumulator = tkz.GetNextToken(); // the encapsulated data
1142 }
1143
1144 else {
1145 sentence_accumulator += tkz.GetNextToken();
1146 }
1147
1148 if( isentence == nsentences ) {
1149 string_to_parse = sentence_accumulator;
1150 }
1151 }
1152
1153 if( mmsi || ( !string_to_parse.IsEmpty() && ( string_to_parse.Len() < AIS_MAX_MESSAGE_LEN ) ) ) {
1154
1155 // Create the bit accessible string
1156 wxCharBuffer abuf = string_to_parse.ToUTF8();
1157 if( !abuf.data() ) // badly formed sentence?
1158 return AIS_GENERIC_ERROR;
1159
1160 AIS_Bitstring strbit( abuf.data() );
1161
1162 // Extract the MMSI
1163 if( !mmsi ) mmsi = strbit.GetInt( 9, 30 );
1164 long mmsi_long = mmsi;
1165 // Search the current AISTargetList for an MMSI match
1166 AIS_Target_Hash::iterator it = AISTargetList->find( mmsi );
1167 if( it == AISTargetList->end() ) // not found
1168 {
1169 pTargetData = new AIS_Target_Data;
1170 bnewtarget = true;
1171 m_n_targets++;
1172 } else {
1173 pTargetData = it->second; // find current entry
1174 pStaleTarget = pTargetData; // save a pointer to stale data
1175 }
1176 for(unsigned int i=0 ; i < g_MMSI_Props_Array.GetCount() ; i++){
1177 MMSIProperties *props = g_MMSI_Props_Array[i];
1178 if(mmsi == props->MMSI){
1179 // Check to see if this MMSI has been configured to be ignored completely...
1180 if(props->m_bignore)
1181 return AIS_NoError;
1182 // Check to see if this MMSI wants VDM translated to VDO or whether we want to persist it's track...
1183 else if (props->m_bVDM){
1184
1185 //Only single line VDM messages to be translated
1186 if( str.Mid( 3, 9 ).IsSameAs( wxT("VDM,1,1,,") ) )
1187 {
1188 int message_ID = strbit.GetInt( 1, 6 ); // Parse on message ID
1189 // Only translate the dynamic positionreport messages (1, 2, 3 or 18)
1190 if ( (message_ID <= 3) || (message_ID == 18) )
1191 {
1192 // set OwnShip to prevent target from being drawn
1193 pTargetData->b_OwnShip = true;
1194 //Rename nmea sentence to AIVDO and calc a new checksum
1195 wxString aivdostr = str;
1196 aivdostr.replace(1, 5, "AIVDO");
1197 unsigned char calculated_checksum = 0;
1198 wxString::iterator i;
1199 for( i = aivdostr.begin()+1; i != aivdostr.end() && *i != '*'; ++i)
1200 calculated_checksum ^= static_cast<unsigned char> (*i);
1201 // if i is not at least 3 positons befoere end, there is no checksum added
1202 // so also no need to add one now.
1203 if ( i <= aivdostr.end()-3 )
1204 aivdostr.replace( i+1, i+3, wxString::Format(_("%02X"), calculated_checksum));
1205
1206 gps_watchdog_timeout_ticks = 60; //increase watchdog time up to 1 minute
1207 //add the changed sentence into nmea stream
1208 OCPN_DataStreamEvent event( wxEVT_OCPN_DATASTREAM, 0 );
1209 std::string s = std::string( aivdostr.mb_str() );
1210 event.SetNMEAString( s );
1211 event.SetStream( NULL );
1212 g_pMUX->AddPendingEvent( event );
1213 }
1214 }
1215 return AIS_NoError;
1216 }
1217 else
1218 break;
1219 }
1220 }
1221
1222 // Grab the stale targets's last report time
1223 wxDateTime now = wxDateTime::Now();
1224 now.MakeGMT();
1225
1226 if( pStaleTarget )
1227 last_report_ticks = pStaleTarget->PositionReportTicks;
1228 else
1229 last_report_ticks = now.GetTicks();
1230
1231 // Delete the stale AIS Target selectable point
1232 if( pStaleTarget )
1233 pSelectAIS->DeleteSelectablePoint( (void *) mmsi_long, SELTYPE_AISTARGET );
1234
1235 if (pTargetData) {
1236 if( gpsg_mmsi ) {
1237 pTargetData->PositionReportTicks = now.GetTicks();
1238 pTargetData->StaticReportTicks = now.GetTicks();
1239 pTargetData->m_utc_hour = gpsg_utc_hour;
1240 pTargetData->m_utc_min = gpsg_utc_min;
1241 pTargetData->m_utc_sec = gpsg_utc_sec;
1242 pTargetData->m_date_string = gpsg_date;
1243 pTargetData->MMSI = gpsg_mmsi;
1244 pTargetData->NavStatus = 0; // underway
1245 pTargetData->Lat = gpsg_lat;
1246 pTargetData->Lon = gpsg_lon;
1247 pTargetData->b_positionOnceValid = true;
1248 pTargetData->COG = gpsg_cog;
1249 pTargetData->SOG = gpsg_sog;
1250 pTargetData->ShipType = 52; // buddy
1251 pTargetData->Class = AIS_GPSG_BUDDY;
1252 memcpy( pTargetData->ShipName, gpsg_name_str, SHIP_NAME_LEN );
1253 pTargetData->b_nameValid = true;
1254 pTargetData->b_active = true;
1255 pTargetData->b_lost = false;
1256
1257 bdecode_result = true;
1258 } else if( arpa_mmsi ) {
1259 pTargetData->m_utc_hour = arpa_utc_hour;
1260 pTargetData->m_utc_min = arpa_utc_min;
1261 pTargetData->m_utc_sec = arpa_utc_sec;
1262 pTargetData->MMSI = arpa_mmsi;
1263 pTargetData->NavStatus = 15; // undefined
1264 if( str.Mid( 3, 3 ).IsSameAs( _T("TLL") ) ) {
1265 if( !bnewtarget ) {
1266 int age_of_last = ( now.GetTicks() - pTargetData->PositionReportTicks );
1267 if ( age_of_last > 0 ) {
1268 ll_gc_ll_reverse( pTargetData->Lat, pTargetData->Lon, arpa_lat, arpa_lon, &pTargetData->COG, &pTargetData->SOG );
1269 pTargetData->SOG = pTargetData->SOG * 3600 / age_of_last;
1270 }
1271 }
1272 pTargetData->Lat = arpa_lat;
1273 pTargetData->Lon = arpa_lon;
1274 } else if( str.Mid( 3, 3 ).IsSameAs( _T("TTM") ) ) {
1275 if( arpa_dist != 0. ) //Not a new or turned off target
1276 ll_gc_ll( gLat, gLon, arpa_brg, arpa_dist, &pTargetData->Lat, &pTargetData->Lon );
1277 else
1278 arpa_lost = true;
1279 pTargetData->COG = arpa_cog;
1280 pTargetData->SOG = arpa_sog;
1281 }
1282 pTargetData->PositionReportTicks = now.GetTicks();
1283 pTargetData->StaticReportTicks = now.GetTicks();
1284 pTargetData->b_positionOnceValid = true;
1285 pTargetData->ShipType = 55; // arpa
1286 pTargetData->Class = AIS_ARPA;
1287
1288 memcpy( pTargetData->ShipName, arpa_name_str, SHIP_NAME_LEN );
1289 if( arpa_status != _T("Q") )
1290 pTargetData->b_nameValid = true;
1291 else
1292 pTargetData->b_nameValid = false;
1293 pTargetData->b_active = !arpa_lost;
1294 pTargetData->b_lost = arpa_nottracked;
1295
1296 bdecode_result = true;
1297 } else if( aprs_mmsi ) {
1298 pTargetData->m_utc_hour = now.GetHour();
1299 pTargetData->m_utc_min = now.GetMinute();
1300 pTargetData->m_utc_sec = now.GetSecond();
1301 pTargetData->MMSI = aprs_mmsi;
1302 pTargetData->NavStatus = 15; // undefined
1303 if( !bnewtarget ) {
1304 int age_of_last = (now.GetTicks() - pTargetData->PositionReportTicks);
1305 if ( age_of_last > 0 ) {
1306 ll_gc_ll_reverse( pTargetData->Lat, pTargetData->Lon, aprs_lat, aprs_lon, &pTargetData->COG, &pTargetData->SOG );
1307 pTargetData->SOG = pTargetData->SOG * 3600 / age_of_last;
1308 }
1309 }
1310 pTargetData->PositionReportTicks = now.GetTicks();
1311 pTargetData->StaticReportTicks = now.GetTicks();
1312 pTargetData->Lat = aprs_lat;
1313 pTargetData->Lon = aprs_lon;
1314 pTargetData->b_positionOnceValid = true;
1315 pTargetData->ShipType = 56; // aprs
1316 pTargetData->Class = AIS_APRS;
1317 memcpy( pTargetData->ShipName, aprs_name_str, SHIP_NAME_LEN );
1318 pTargetData->b_nameValid = true;
1319 pTargetData->b_active = true;
1320 pTargetData->b_lost = false;
1321
1322 bdecode_result = true;
1323 } else{
1324 // The normal Plain-Old AIS target code path....
1325 bdecode_result = Parse_VDXBitstring( &strbit, pTargetData ); // Parse the new data
1326 }
1327 // Update the most recent report period
1328 pTargetData->RecentPeriod = pTargetData->PositionReportTicks - last_report_ticks;
1329 }
1330 ret = AIS_NoError;
1331 } else{
1332 ret = AIS_Partial; // accumulating parts of a multi-sentence message
1333 pTargetData = 0;
1334 }
1335
1336
1337 if(pTargetData){
1338 // pTargetData is valid, either new or existing. Continue processing
1339
1340 m_pLatestTargetData = pTargetData;
1341
1342 if( str.Mid( 3, 3 ).IsSameAs( _T("VDO") ) )
1343 pTargetData->b_OwnShip = true;
1344 else{
1345 //set mmsi-props to default values
1346 pTargetData->b_OwnShip = false;
1347 if ( 0 == m_persistent_tracks.count( mmsi ) ) {
1348 //Normal target
1349 pTargetData->b_PersistTrack = false;
1350 } else {
1351 // The track persistency enabled in the query window
1352 pTargetData->b_PersistTrack = true;
1353 }
1354 pTargetData->b_NoTrack = false;
1355 }
1356
1357 // If the message was decoded correctly
1358 // Update the AIS Target information
1359 if( bdecode_result ) {
1360 AISshipNameCache(pTargetData, AISTargetNamesC, AISTargetNamesNC, mmsi);
1361 ( *AISTargetList )[pTargetData->MMSI] = pTargetData; // update the hash table entry
1362
1363 if( !pTargetData->area_notices.empty() ) {
1364 AIS_Target_Hash::iterator it = AIS_AreaNotice_Sources->find( pTargetData->MMSI );
1365 if( it == AIS_AreaNotice_Sources->end() )
1366 ( *AIS_AreaNotice_Sources ) [pTargetData->MMSI] = pTargetData;
1367 }
1368
1369
1370 // If this is not an ownship message, update the AIS Target in the Selectable list, and update the CPA info
1371 if( !pTargetData->b_OwnShip ) {
1372 if( pTargetData->b_positionOnceValid ) {
1373 long mmsi_long = pTargetData->MMSI;
1374 SelectItem *pSel = pSelectAIS->AddSelectablePoint( pTargetData->Lat,
1375 pTargetData->Lon, (void *) mmsi_long, SELTYPE_AISTARGET );
1376 pSel->SetUserData( pTargetData->MMSI );
1377 }
1378
1379
1380 // Calculate CPA info for this target immediately
1381 UpdateOneCPA( pTargetData );
1382
1383 // Update this target's track
1384 if( pTargetData->b_show_track )
1385 UpdateOneTrack( pTargetData );
1386 }
1387 // TODO add ais message call
1388 SendJSONMsg( pTargetData );
1389 } else {
1390 // printf("Unrecognised AIS message ID: %d\n", pTargetData->MID);
1391 if( bnewtarget ) {
1392 delete pTargetData; // this target is not going to be used
1393 m_n_targets--;
1394 } else {
1395 // If this is not an ownship message, update the AIS Target in the Selectable list
1396 // even if the message type was not recognized
1397 if( !pTargetData->b_OwnShip ) {
1398 if( pTargetData->b_positionOnceValid ) {
1399 long mmsi_long = pTargetData->MMSI;
1400 SelectItem *pSel = pSelectAIS->AddSelectablePoint( pTargetData->Lat,
1401 pTargetData->Lon, (void *) mmsi_long, SELTYPE_AISTARGET );
1402 pSel->SetUserData( pTargetData->MMSI );
1403 }
1404 }
1405 }
1406 }
1407 }
1408
1409 n_msgs++;
1410 #ifdef AIS_DEBUG
1411 if((n_msgs % 10000) == 0)
1412 printf("n_msgs %10d m_n_targets: %6d n_msg1: %10d n_msg5+24: %10d \n", n_msgs, m_n_targets, n_msg1, n_msg5 + n_msg24);
1413 #endif
1414
1415 return ret;
1416 }
1417
getAISTarget(long mmsi,AIS_Target_Data * & pTargetData,AIS_Target_Data * & pStaleTarget,bool & bnewtarget,int & last_report_ticks,wxDateTime & now)1418 void AIS_Decoder::getAISTarget(long mmsi,
1419 AIS_Target_Data *&pTargetData,
1420 AIS_Target_Data *&pStaleTarget,
1421 bool &bnewtarget,
1422 int &last_report_ticks,
1423 wxDateTime &now)
1424 {
1425 now= wxDateTime::Now();
1426 AIS_Target_Hash_wxImplementation_HashTable::iterator it = AISTargetList->find(mmsi);
1427 if( it == AISTargetList->end() ) // not found
1428 {
1429 pTargetData = new AIS_Target_Data;
1430 bnewtarget = true;
1431 m_n_targets++;
1432 } else {
1433 pTargetData = it->second; // find current entry
1434 pStaleTarget = pTargetData; // save a pointer to stale data
1435 }
1436
1437 // Grab the stale targets's last report time
1438 now.MakeGMT();
1439
1440 if( pStaleTarget )
1441 last_report_ticks = pStaleTarget->PositionReportTicks;
1442 else
1443 last_report_ticks = now.GetTicks();
1444
1445 // Delete the stale AIS Target selectable point
1446 if( pStaleTarget )
1447 pSelectAIS->DeleteSelectablePoint( (void *) mmsi, SELTYPE_AISTARGET );
1448 }
1449
ProcessDSx(const wxString & str,bool b_take_dsc)1450 AIS_Target_Data *AIS_Decoder::ProcessDSx( const wxString& str, bool b_take_dsc )
1451 {
1452 double dsc_lat = 0.;
1453 double dsc_lon = 0.;
1454 double dsc_mins, dsc_degs, dsc_tmp, dsc_addr;
1455 double dse_tmp, dse_addr;
1456 double dse_lat = 0.;
1457 double dse_lon = 0.;
1458 long dsc_fmt, dsc_quadrant;
1459
1460 int dsc_mmsi = 0;
1461 int dse_mmsi = 0;
1462 int mmsi = 0;
1463
1464 AIS_Target_Data *pTargetData = NULL;
1465
1466 // parse a DSC Position message $CDDSx,.....
1467 // Use a tokenizer to pull out the first 9 fields
1468 wxString string( str );
1469 wxStringTokenizer tkz( string, _T(",*") );
1470
1471 wxString token;
1472 token = tkz.GetNextToken(); // !$CDDS
1473
1474 if( str.Mid( 3, 3 ).IsSameAs( _T("DSC") ) ) {
1475 m_dsc_last_string = str;
1476
1477 token = tkz.GetNextToken(); // format specifier (02-area,12-distress,16-allships,20-individual,...)
1478 token.ToLong( &dsc_fmt );
1479
1480 token = tkz.GetNextToken(); // address i.e. mmsi*10 for received msg, or area spec
1481 token.ToDouble( &dsc_addr );
1482 dsc_mmsi = 0 - (int) ( dsc_addr / 10 ); // as per NMEA 0183 3.01
1483
1484 token = tkz.GetNextToken(); // category
1485 token = tkz.GetNextToken(); // nature of distress or telecommand1
1486 token = tkz.GetNextToken(); // comm type or telecommand2
1487
1488 token = tkz.GetNextToken(); // position or channel/freq
1489 token.ToDouble( &dsc_tmp );
1490
1491 token = tkz.GetNextToken(); // time or tel. no.
1492 token = tkz.GetNextToken(); // mmsi of ship in distress
1493 token = tkz.GetNextToken(); // nature of distress
1494 token = tkz.GetNextToken(); // acknowledgement
1495 token = tkz.GetNextToken(); // expansion indicator
1496
1497 dsc_quadrant = (int) (dsc_tmp / 1000000000.0);
1498
1499 if(dsc_quadrant > 3) // Position is "Unspecified", or 9999999999
1500 return NULL;
1501
1502 dsc_lat = (int) ( dsc_tmp / 100000.0 );
1503 dsc_lon = dsc_tmp - dsc_lat * 100000.0;
1504 dsc_lat = dsc_lat - dsc_quadrant * 10000;
1505 dsc_degs = (int) ( dsc_lat / 100.0 );
1506 dsc_mins = dsc_lat - dsc_degs * 100.0;
1507 dsc_lat = dsc_degs + dsc_mins / 60.0;
1508
1509 dsc_degs = (int) ( dsc_lon / 100.0 );
1510 dsc_mins = dsc_lon - dsc_degs * 100.0;
1511 dsc_lon = dsc_degs + dsc_mins / 60.0;
1512 switch( dsc_quadrant ) {
1513 case 0: break; // NE
1514 case 1: dsc_lon = -dsc_lon; break; // NW
1515 case 2: dsc_lat = -dsc_lat; break; // SE
1516 case 3: dsc_lon = -dsc_lon; dsc_lat = -dsc_lat; break; // SW
1517 default: break;
1518 }
1519 if( dsc_fmt != 02 )
1520 mmsi = (int) dsc_mmsi;
1521 } else if( str.Mid( 3, 3 ).IsSameAs( _T("DSE") ) ) {
1522
1523 token = tkz.GetNextToken(); // total number of sentences
1524 token = tkz.GetNextToken(); // sentence number
1525 token = tkz.GetNextToken(); // query/rely flag
1526 token = tkz.GetNextToken(); // vessel MMSI
1527 token.ToDouble( &dse_addr );
1528 dse_mmsi = 0 - (int) ( dse_addr / 10 ); // as per NMEA 0183 3.01
1529
1530 token = tkz.GetNextToken(); // code field
1531 token = tkz.GetNextToken(); // data field - position - 2*4 digits latlon .mins
1532 token.ToDouble( &dse_tmp );
1533 dse_lat = (int) ( dse_tmp / 10000.0 );
1534 dse_lon = (int) ( dse_tmp - dse_lat * 10000.0 );
1535 dse_lat = dse_lat / 600000.0;
1536 dse_lon = dse_lon / 600000.0;
1537
1538 mmsi = (int) dse_mmsi;
1539 }
1540
1541 // Get the last report time for this target, if it exists
1542 wxDateTime now = wxDateTime::Now();
1543 now.MakeGMT();
1544 int last_report_ticks = now.GetTicks();
1545
1546 // Search the current AISTargetList for an MMSI match
1547 AIS_Target_Hash::iterator it = AISTargetList->find( mmsi );
1548 AIS_Target_Data *pStaleTarget = NULL;
1549 if( it == AISTargetList->end() ) { // not found
1550 } else {
1551 pStaleTarget = it->second; // find current entry
1552 last_report_ticks = pStaleTarget->PositionReportTicks;
1553 }
1554
1555 if( dsc_mmsi ){
1556 // Create a tentative target, but do not post it pending receipt of extended data
1557 m_ptentative_dsctarget = new AIS_Target_Data;
1558
1559 m_ptentative_dsctarget->PositionReportTicks = now.GetTicks();
1560 m_ptentative_dsctarget->StaticReportTicks = now.GetTicks();
1561
1562 m_ptentative_dsctarget->MMSI = mmsi;
1563 m_ptentative_dsctarget->NavStatus = 0; // underway
1564 m_ptentative_dsctarget->Lat = dsc_lat;
1565 m_ptentative_dsctarget->Lon = dsc_lon;
1566 m_ptentative_dsctarget->b_positionOnceValid = true;
1567 m_ptentative_dsctarget->COG = 0;
1568 m_ptentative_dsctarget->SOG = 0;
1569 m_ptentative_dsctarget->ShipType = dsc_fmt; // DSC report
1570 m_ptentative_dsctarget->Class = AIS_DSC;
1571 m_ptentative_dsctarget->b_nameValid = true;
1572 if( dsc_fmt == 12 ) {
1573 snprintf( m_ptentative_dsctarget->ShipName, SHIP_NAME_LEN, "DISTRESS %d", std::abs(mmsi));
1574 }
1575 else {
1576 snprintf( m_ptentative_dsctarget->ShipName, SHIP_NAME_LEN, "POSITION %d", std::abs(mmsi));
1577 }
1578
1579 m_ptentative_dsctarget->b_active = true;
1580 m_ptentative_dsctarget->b_lost = false;
1581 m_ptentative_dsctarget->RecentPeriod = m_ptentative_dsctarget->PositionReportTicks - last_report_ticks;
1582
1583 // Start a timer, looking for an expected DSE extension message
1584 if( !b_take_dsc )
1585 m_dsc_timer.Start( 500, wxTIMER_ONE_SHOT);
1586 }
1587
1588 // Got an extension message, or the timer expired and no extension is expected
1589 if( dse_mmsi || b_take_dsc){
1590 if( m_ptentative_dsctarget ){
1591
1592 // stop the timer for sure
1593 m_dsc_timer.Stop();
1594
1595 // Update the extended information
1596 if( dse_mmsi ) {
1597 m_ptentative_dsctarget->Lat = m_ptentative_dsctarget->Lat + ( ( m_ptentative_dsctarget->Lat ) >= 0 ? dse_lat : -dse_lat );
1598 m_ptentative_dsctarget->Lon = m_ptentative_dsctarget->Lon + ( ( m_ptentative_dsctarget->Lon ) >= 0 ? dse_lon : -dse_lon );
1599 }
1600
1601 // Update the most recent report period
1602 m_ptentative_dsctarget->RecentPeriod = m_ptentative_dsctarget->PositionReportTicks - last_report_ticks;
1603
1604 // And post the target
1605
1606 // Search the current AISTargetList for an MMSI match
1607 AIS_Target_Hash::iterator it = AISTargetList->find( mmsi );
1608 if( it == AISTargetList->end() ) { // not found
1609 pTargetData = m_ptentative_dsctarget;
1610 } else {
1611 pTargetData = it->second; // find current entry
1612 AISTargetTrackList *ptrack = pTargetData->m_ptrack;
1613 pTargetData->CloneFrom( m_ptentative_dsctarget); // this will make an empty track list
1614
1615 delete pTargetData->m_ptrack; // get rid of the new empty one
1616 pTargetData->m_ptrack = ptrack; // and substitute the old track list
1617
1618 delete m_ptentative_dsctarget;
1619 }
1620
1621 // Reset for next time
1622 m_ptentative_dsctarget = NULL;
1623
1624 m_pLatestTargetData = pTargetData;
1625
1626 ( *AISTargetList )[pTargetData->MMSI] = pTargetData; // update the hash table entry
1627
1628 long mmsi_long = pTargetData->MMSI;
1629
1630 // Delete any stale Target selectable point
1631 if( pStaleTarget )
1632 pSelectAIS->DeleteSelectablePoint( (void *) mmsi_long, SELTYPE_AISTARGET );
1633 // And add the updated target
1634 SelectItem *pSel = pSelectAIS->AddSelectablePoint( pTargetData->Lat,
1635 pTargetData->Lon, (void *) mmsi_long, SELTYPE_AISTARGET );
1636 pSel->SetUserData( pTargetData->MMSI );
1637
1638 // Calculate CPA info for this target immediately
1639 UpdateOneCPA( pTargetData );
1640
1641 // Update this target's track
1642 if( pTargetData->b_show_track )
1643 UpdateOneTrack( pTargetData );
1644 }
1645
1646 }
1647
1648 return pTargetData;
1649 }
1650
1651
1652 //----------------------------------------------------------------------------
1653 // Parse a NMEA VDM/VDO Bitstring
1654 //----------------------------------------------------------------------------
Parse_VDXBitstring(AIS_Bitstring * bstr,AIS_Target_Data * ptd)1655 bool AIS_Decoder::Parse_VDXBitstring( AIS_Bitstring *bstr, AIS_Target_Data *ptd )
1656 {
1657 bool parse_result = false;
1658 bool b_posn_report = false;
1659
1660 wxDateTime now = wxDateTime::Now();
1661 now.MakeGMT( );
1662 int message_ID = bstr->GetInt( 1, 6 ); // Parse on message ID
1663 ptd->MID = message_ID;
1664 ptd->MMSI = bstr->GetInt( 9, 30 ); // MMSI is always in the same spot in the bitstream
1665
1666 switch( message_ID ){
1667 case 1: // Position Report
1668 case 2:
1669 case 3: {
1670 n_msg1++;
1671
1672 ptd->NavStatus = bstr->GetInt( 39, 4 );
1673 ptd->SOG = 0.1 * ( bstr->GetInt( 51, 10 ) );
1674
1675 int lon = bstr->GetInt( 62, 28 );
1676 if( lon & 0x08000000 ) // negative?
1677 lon |= 0xf0000000;
1678 double lon_tentative = lon / 600000.;
1679
1680 int lat = bstr->GetInt( 90, 27 );
1681 if( lat & 0x04000000 ) // negative?
1682 lat |= 0xf8000000;
1683 double lat_tentative = lat / 600000.;
1684
1685 if( ( lon_tentative <= 180. ) && ( lat_tentative <= 90. ) ) // Ship does not report Lat or Lon "unavailable"
1686 {
1687 ptd->Lon = lon_tentative;
1688 ptd->Lat = lat_tentative;
1689 ptd->b_positionDoubtful = false;
1690 ptd->b_positionOnceValid = true; // Got the position at least once
1691 ptd->PositionReportTicks = now.GetTicks();
1692 } else
1693 ptd->b_positionDoubtful = true;
1694
1695 // decode balance of message....
1696 ptd->COG = 0.1 * ( bstr->GetInt( 117, 12 ) );
1697 ptd->HDG = 1.0 * ( bstr->GetInt( 129, 9 ) );
1698
1699 ptd->ROTAIS = bstr->GetInt( 43, 8 );
1700 double rot_dir = 1.0;
1701
1702 if( ptd->ROTAIS == 128 ) ptd->ROTAIS = -128; // not available codes as -128
1703 else if( ( ptd->ROTAIS & 0x80 ) == 0x80 ) {
1704 ptd->ROTAIS = ptd->ROTAIS - 256; // convert to twos complement
1705 rot_dir = -1.0;
1706 }
1707
1708 ptd->ROTIND = wxRound( rot_dir * pow( ( ( (double) ptd->ROTAIS ) / 4.733 ), 2 ) ); // Convert to indicated ROT
1709
1710 ptd->m_utc_sec = bstr->GetInt( 138, 6 );
1711
1712 if( ( 1 == message_ID ) || ( 2 == message_ID ) ) // decode SOTDMA per 7.6.7.2.2
1713 {
1714 ptd->SyncState = bstr->GetInt( 151, 2 );
1715 ptd->SlotTO = bstr->GetInt( 153, 2 );
1716 if( ( ptd->SlotTO == 1 ) && ( ptd->SyncState == 0 ) ) // UTCDirect follows
1717 {
1718 ptd->m_utc_hour = bstr->GetInt( 155, 5 );
1719
1720 ptd->m_utc_min = bstr->GetInt( 160, 7 );
1721
1722 if( ( ptd->m_utc_hour < 24 ) && ( ptd->m_utc_min < 60 )
1723 && ( ptd->m_utc_sec < 60 ) ) {
1724 wxDateTime rx_time( ptd->m_utc_hour, ptd->m_utc_min, ptd->m_utc_sec );
1725 rx_ticks = rx_time.GetTicks();
1726 if( !b_firstrx ) {
1727 first_rx_ticks = rx_ticks;
1728 b_firstrx = true;
1729 }
1730 }
1731 }
1732 }
1733
1734 // Capture Euro Inland special passing arrangement signal ("stbd-stbd")
1735 ptd->blue_paddle = bstr->GetInt( 144, 2 );
1736 ptd->b_blue_paddle = ( ptd->blue_paddle == 2 ); // paddle is set
1737
1738 ptd->Class = AIS_CLASS_A;
1739
1740 // Check for SART and friends by looking at first two digits of MMSI
1741 int mmsi_start = ptd->MMSI / 10000000;
1742
1743 if( mmsi_start == 97 ) {
1744 ptd->Class = AIS_SART;
1745 ptd->StaticReportTicks = now.GetTicks(); // won't get a static report, so fake it here
1746
1747 // On receipt of Msg 3, force any existing SART target out of acknowledge mode
1748 // by adjusting its ack_time to yesterday
1749 // This will cause any previously "Acknowledged" SART to re-alert.
1750
1751 // On reflection, re-alerting seems a little excessive in real life use.
1752 // After all, the target is on-screen, and in the AIS target list.
1753 // So lets just honor the programmed ACK timout value for SART targets as well
1754 //ptd->m_ack_time = wxDateTime::Now() - wxTimeSpan::Day();
1755 }
1756
1757 parse_result = true; // so far so good
1758 b_posn_report = true;
1759
1760 break;
1761 }
1762
1763 case 18: {
1764 ptd->NavStatus = UNDEFINED; // Class B targets have no status. Enforce this...
1765
1766 ptd->SOG = 0.1 * ( bstr->GetInt( 47, 10 ) );
1767
1768 int lon = bstr->GetInt( 58, 28 );
1769 if( lon & 0x08000000 ) // negative?
1770 lon |= 0xf0000000;
1771 double lon_tentative = lon / 600000.;
1772
1773 int lat = bstr->GetInt( 86, 27 );
1774 if( lat & 0x04000000 ) // negative?
1775 lat |= 0xf8000000;
1776 double lat_tentative = lat / 600000.;
1777
1778 if( ( lon_tentative <= 180. ) && ( lat_tentative <= 90. ) ) // Ship does not report Lat or Lon "unavailable"
1779 {
1780 ptd->Lon = lon_tentative;
1781 ptd->Lat = lat_tentative;
1782 ptd->b_positionDoubtful = false;
1783 ptd->b_positionOnceValid = true; // Got the position at least once
1784 ptd->PositionReportTicks = now.GetTicks();
1785 } else
1786 ptd->b_positionDoubtful = true;
1787
1788 ptd->COG = 0.1 * ( bstr->GetInt( 113, 12 ) );
1789 ptd->HDG = 1.0 * ( bstr->GetInt( 125, 9 ) );
1790
1791 ptd->m_utc_sec = bstr->GetInt( 134, 6 );
1792
1793 ptd->Class = AIS_CLASS_B;
1794
1795 parse_result = true; // so far so good
1796 b_posn_report = true;
1797
1798 break;
1799 }
1800
1801 case 19: { // Class B mes_ID 19 Is same as mes_ID 18 until bit 139
1802 ptd->NavStatus = UNDEFINED; // Class B targets have no status. Enforce this...
1803 ptd->SOG = 0.1 * (bstr->GetInt(47, 10));
1804 int lon = bstr->GetInt(58, 28);
1805 if (lon & 0x08000000) // negative?
1806 lon |= 0xf0000000;
1807 double lon_tentative = lon / 600000.;
1808
1809 int lat = bstr->GetInt(86, 27);
1810 if (lat & 0x04000000) // negative?
1811 lat |= 0xf8000000;
1812 double lat_tentative = lat / 600000.;
1813
1814 if ((lon_tentative <= 180.) && (lat_tentative <= 90.)) // Ship does not report Lat or Lon "unavailable"
1815 {
1816 ptd->Lon = lon_tentative;
1817 ptd->Lat = lat_tentative;
1818 ptd->b_positionDoubtful = false;
1819 ptd->b_positionOnceValid = true; // Got the position at least once
1820 ptd->PositionReportTicks = now.GetTicks();
1821 } else
1822 ptd->b_positionDoubtful = true;
1823
1824 ptd->COG = 0.1 * (bstr->GetInt(113, 12));
1825 ptd->HDG = 1.0 * (bstr->GetInt(125, 9));
1826 ptd->m_utc_sec = bstr->GetInt(134, 6);
1827 //From bit 140 and forward data as of mes 5
1828 bstr->GetStr(144, 120, &ptd->ShipName[0], 20);
1829 ptd->b_nameValid = true;
1830 ptd->ShipType = (unsigned char)bstr->GetInt(264, 8);
1831 ptd->DimA = bstr->GetInt(272, 9);
1832 ptd->DimB = bstr->GetInt(281, 9);
1833 ptd->DimC = bstr->GetInt(290, 6);
1834 ptd->DimD = bstr->GetInt(296, 6);
1835
1836 ptd->Class = AIS_CLASS_B;
1837 parse_result = true; // so far so good
1838 b_posn_report = true;
1839
1840 break;
1841 }
1842
1843
1844 case 27: {
1845 // Long-range automatic identification system broadcast message
1846 // This message is used for long-range detection of AIS Class A and Class B vessels (typically by satellite).
1847
1848 // Define the constant to do the covertion from the internal encoded
1849 // position in message 27. The position is less accuate : 1/10 minute position resolution.
1850 int bitCorrection = 10;
1851 int resolution = 10;
1852
1853 // Default aout of bounce values.
1854 double lon_tentative = 181.;
1855 double lat_tentative = 91.;
1856
1857 #ifdef AIS_DEBUG
1858 printf("AIS Message 27 - received:\r\n");
1859 printf("MMSI : %i\r\n", ptd->MMSI );
1860 #endif
1861
1862 // It can be both a CLASS A and a CLASS B vessel - We have decided for CLASS A
1863 // TODO: Lookup to see if we have seen it as a CLASS B, and adjust.
1864 ptd->Class = AIS_CLASS_A;
1865
1866 ptd->NavStatus = bstr->GetInt( 39, 4 );
1867
1868 int lon = bstr->GetInt( 45,18 ) ;
1869 int lat = bstr->GetInt( 63, 17 );
1870
1871 lat_tentative = lat;
1872 lon_tentative = lon;
1873
1874 // Negative latitude?
1875 if( lat >= (0x4000000 >> bitCorrection)) {
1876 lat_tentative = (0x8000000 >> bitCorrection) - lat;
1877 lat_tentative *= -1;
1878 }
1879
1880 // Negative longitude?
1881 if(lon >= (0x8000000 >> bitCorrection)) {
1882 lon_tentative = (0x10000000 >> bitCorrection) - lon;
1883 lon_tentative *= -1;
1884 }
1885
1886 // Decode the internal position format.
1887 lat_tentative = lat_tentative / resolution / 60.0;
1888 lon_tentative = lon_tentative / resolution / 60.0;
1889
1890 #ifdef AIS_DEBUG
1891 printf("Latitude : %f\r\n", lat_tentative );
1892 printf("Longitude : %f\r\n", lon_tentative );
1893 #endif
1894
1895 // Get the latency of the position report.
1896 int positionLatency = bstr->GetInt( 95, 1 );
1897
1898 if ((lon_tentative <= 180.) && (lat_tentative <= 90.)) // Ship does not report Lat or Lon "unavailable"
1899 {
1900 ptd->Lon = lon_tentative;
1901 ptd->Lat = lat_tentative;
1902 ptd->b_positionDoubtful = false;
1903 ptd->b_positionOnceValid = true; // Got the position at least once
1904 if( positionLatency == 0 )
1905 {
1906 // The position is less than 5 seconds old.
1907 #ifdef AIS_DEBUG
1908 printf("Low latency position report.\r\n");
1909 #endif
1910 ptd->PositionReportTicks = now.GetTicks();
1911 }
1912 }
1913 else
1914 ptd->b_positionDoubtful = true;
1915
1916 ptd->SOG = 1.0 * (bstr->GetInt( 80, 6 ));
1917 ptd->COG = 1.0 * (bstr->GetInt( 85, 9 ));
1918
1919 b_posn_report = true;
1920 parse_result = true;
1921 break;
1922 }
1923
1924
1925 case 5: {
1926 n_msg5++;
1927 ptd->Class = AIS_CLASS_A;
1928
1929 // Get the AIS Version indicator
1930 // 0 = station compliant with Recommendation ITU-R M.1371-1
1931 // 1 = station compliant with Recommendation ITU-R M.1371-3
1932 // 2-3 = station compliant with future editions
1933 int AIS_version_indicator = bstr->GetInt( 39, 2 );
1934 if( AIS_version_indicator < 4 ) {
1935 ptd->IMO = bstr->GetInt( 41, 30 );
1936
1937 bstr->GetStr( 71, 42, &ptd->CallSign[0], 7 );
1938 bstr->GetStr( 113, 120, &ptd->ShipName[0], 20 );
1939 ptd->b_nameValid = true;
1940
1941 ptd->ShipType = (unsigned char) bstr->GetInt( 233, 8 );
1942
1943 ptd->DimA = bstr->GetInt( 241, 9 );
1944 ptd->DimB = bstr->GetInt( 250, 9 );
1945 ptd->DimC = bstr->GetInt( 259, 6 );
1946 ptd->DimD = bstr->GetInt( 265, 6 );
1947
1948 ptd->ETA_Mo = bstr->GetInt( 275, 4 );
1949 ptd->ETA_Day = bstr->GetInt( 279, 5 );
1950 ptd->ETA_Hr = bstr->GetInt( 284, 5 );
1951 ptd->ETA_Min = bstr->GetInt( 289, 6 );
1952
1953 ptd->Draft = (double) ( bstr->GetInt( 295, 8 ) ) / 10.0;
1954
1955 bstr->GetStr( 303, 120, &ptd->Destination[0], 20 );
1956
1957 ptd->StaticReportTicks = now.GetTicks();
1958
1959 parse_result = true;
1960 }
1961
1962 break;
1963 }
1964
1965 case 24: {
1966 int part_number = bstr->GetInt( 39, 2 );
1967 if( 0 == part_number ) {
1968 bstr->GetStr( 41, 120, &ptd->ShipName[0], 20 );
1969 ptd->b_nameValid = true;
1970 parse_result = true;
1971 n_msg24++;
1972 } else if( 1 == part_number ) {
1973 ptd->ShipType = (unsigned char) bstr->GetInt( 41, 8 );
1974 bstr->GetStr( 91, 42, &ptd->CallSign[0], 7 );
1975
1976 ptd->DimA = bstr->GetInt( 133, 9 );
1977 ptd->DimB = bstr->GetInt( 142, 9 );
1978 ptd->DimC = bstr->GetInt( 151, 6 );
1979 ptd->DimD = bstr->GetInt( 157, 6 );
1980 parse_result = true;
1981 }
1982 break;
1983 }
1984 case 4: // base station
1985 {
1986 ptd->Class = AIS_BASE;
1987
1988 ptd->m_utc_hour = bstr->GetInt( 62, 5 );
1989 ptd->m_utc_min = bstr->GetInt( 67, 6 );
1990 ptd->m_utc_sec = bstr->GetInt( 73, 6 );
1991 // (79, 1);
1992 int lon = bstr->GetInt( 80, 28 );
1993 if( lon & 0x08000000 ) // negative?
1994 lon |= 0xf0000000;
1995 double lon_tentative = lon / 600000.;
1996
1997 int lat = bstr->GetInt( 108, 27 );
1998 if( lat & 0x04000000 ) // negative?
1999 lat |= 0xf8000000;
2000 double lat_tentative = lat / 600000.;
2001
2002 if( ( lon_tentative <= 180. ) && ( lat_tentative <= 90. ) ) // Ship does not report Lat or Lon "unavailable"
2003 {
2004 ptd->Lon = lon_tentative;
2005 ptd->Lat = lat_tentative;
2006 ptd->b_positionDoubtful = false;
2007 ptd->b_positionOnceValid = true; // Got the position at least once
2008 ptd->PositionReportTicks = now.GetTicks();
2009 } else
2010 ptd->b_positionDoubtful = true;
2011
2012 ptd->COG = -1.;
2013 ptd->HDG = 511;
2014 ptd->SOG = -1.;
2015
2016 parse_result = true;
2017 b_posn_report = true;
2018
2019 break;
2020 }
2021 case 9: // Special Position Report (Standard SAR Aircraft Position Report)
2022 {
2023 ptd->SOG = bstr->GetInt( 51, 10 ) ;
2024
2025 int lon = bstr->GetInt( 62, 28 );
2026 if( lon & 0x08000000 ) // negative?
2027 lon |= 0xf0000000;
2028 double lon_tentative = lon / 600000.;
2029
2030 int lat = bstr->GetInt( 90, 27 );
2031 if( lat & 0x04000000 ) // negative?
2032 lat |= 0xf8000000;
2033 double lat_tentative = lat / 600000.;
2034
2035 if( ( lon_tentative <= 180. ) && ( lat_tentative <= 90. ) ) // Ship does not report Lat or Lon "unavailable"
2036 {
2037 ptd->Lon = lon_tentative;
2038 ptd->Lat = lat_tentative;
2039 ptd->b_positionDoubtful = false;
2040 ptd->b_positionOnceValid = true; // Got the position at least once
2041 ptd->PositionReportTicks = now.GetTicks();
2042 } else
2043 ptd->b_positionDoubtful = true;
2044
2045 // decode balance of message....
2046 ptd->COG = 0.1 * ( bstr->GetInt( 117, 12 ) );
2047
2048 int alt_tent = bstr->GetInt( 39, 12 );
2049 ptd->altitude = alt_tent;
2050
2051 ptd->b_SarAircraftPosnReport = true;
2052
2053 parse_result = true;
2054 b_posn_report = true;
2055
2056 break;
2057 }
2058 case 21: // Test Message (Aid to Navigation)
2059 {
2060 ptd->ShipType = (unsigned char) bstr->GetInt( 39, 5 );
2061 ptd->IMO = 0;
2062 ptd->SOG = 0;
2063 ptd->HDG = 0;
2064 ptd->COG = 0;
2065 ptd->ROTAIS = -128; // i.e. not available
2066 ptd->DimA = bstr->GetInt( 220, 9 );
2067 ptd->DimB = bstr->GetInt( 229, 9 );
2068 ptd->DimC = bstr->GetInt( 238, 6 );
2069 ptd->DimD = bstr->GetInt( 244, 6 );
2070 ptd->Draft = 0;
2071
2072 ptd->m_utc_sec = bstr->GetInt( 254, 6 );
2073
2074 int offpos = bstr->GetInt( 260, 1 ); // off position flag
2075 int virt = bstr->GetInt( 270, 1 ); // virtual flag
2076
2077 if( virt ) ptd->NavStatus = ATON_VIRTUAL;
2078 else
2079 ptd->NavStatus = ATON_REAL;
2080 if( ptd->m_utc_sec <= 59 /*&& !virt*/) {
2081 ptd->NavStatus += 1;
2082 if( offpos ) ptd->NavStatus += 1;
2083 }
2084
2085 bstr->GetStr( 44, 120, &ptd->ShipName[0], 20 ); // short name only, extension wont fit in Ship structure
2086
2087 if( bstr->GetBitCount() > 276 ) {
2088 int nx = ( ( bstr->GetBitCount() - 272 ) / 6 ) * 6;
2089 bstr->GetStr( 273, nx, &ptd->ShipNameExtension[0], 14 );
2090 ptd->ShipNameExtension[14] = 0;
2091 } else {
2092 ptd->ShipNameExtension[0] = 0;
2093 }
2094
2095 ptd->b_nameValid = true;
2096
2097 parse_result = true; // so far so good
2098
2099 ptd->Class = AIS_ATON;
2100
2101 int lon = bstr->GetInt( 165, 28 );
2102
2103 if( lon & 0x08000000 ) // negative?
2104 lon |= 0xf0000000;
2105 double lon_tentative = lon / 600000.;
2106
2107 int lat = bstr->GetInt( 193, 27 );
2108
2109 if( lat & 0x04000000 ) // negative?
2110 lat |= 0xf8000000;
2111 double lat_tentative = lat / 600000.;
2112
2113 if( ( lon_tentative <= 180. ) && ( lat_tentative <= 90. ) ) // Ship does not report Lat or Lon "unavailable"
2114 {
2115 ptd->Lon = lon_tentative;
2116 ptd->Lat = lat_tentative;
2117 ptd->b_positionDoubtful = false;
2118 ptd->b_positionOnceValid = true; // Got the position at least once
2119 ptd->PositionReportTicks = now.GetTicks();
2120 } else
2121 ptd->b_positionDoubtful = true;
2122
2123 b_posn_report = true;
2124 break;
2125 }
2126 case 8: // Binary Broadcast
2127 {
2128 int dac = bstr->GetInt( 41, 10 );
2129 int fi = bstr->GetInt( 51, 6 );
2130 if( dac == 200 ) // European inland
2131 {
2132 if( fi == 10 ) // "Inland ship static and voyage related data"
2133 {
2134 ptd->b_isEuroInland = true;
2135
2136 bstr->GetStr( 57, 48, &ptd->Euro_VIN[0], 8 );
2137 ptd->Euro_Length = ( (double) bstr->GetInt( 105, 13 ) ) / 10.0;
2138 ptd->Euro_Beam = ( (double) bstr->GetInt( 118, 10 ) ) / 10.0;
2139 ptd->UN_shiptype = bstr->GetInt( 128, 14 );
2140 ptd->Euro_Draft = ( (double) bstr->GetInt( 145, 11 ) ) / 100.0;
2141 parse_result = true;
2142 }
2143 }
2144 if( dac == 1 ) // IMO
2145 {
2146 if( fi == 22 ) // Area Notice
2147 {
2148 if( bstr->GetBitCount() >= 111 ) {
2149 Ais8_001_22 an;
2150 an.link_id = bstr->GetInt( 57, 10 );
2151 an.notice_type = bstr->GetInt( 67, 7 );
2152 an.month = bstr->GetInt( 74, 4 );
2153 an.day = bstr->GetInt( 78, 5 );
2154 an.hour = bstr->GetInt( 83, 5 );
2155 an.minute = bstr->GetInt( 88, 6 );
2156 an.duration_minutes = bstr->GetInt( 94, 18 );
2157
2158 wxDateTime now = wxDateTime::Now();
2159 now.MakeGMT();
2160
2161 an.start_time.Set( an.day, wxDateTime::Month( an.month - 1 ), now.GetYear(),
2162 an.hour, an.minute );
2163
2164 // msg is not supposed to be transmitted more than a day before it comes into effect,
2165 // so a start_time less than a day or two away might indicate a month rollover
2166 if( an.start_time > now + wxTimeSpan::Hours( 48 ) ) an.start_time.Set(
2167 an.day, wxDateTime::Month( an.month - 1 ), now.GetYear() - 1,
2168 an.hour, an.minute );
2169
2170 an.expiry_time = an.start_time + wxTimeSpan::Minutes( an.duration_minutes );
2171
2172 // msg is not supposed to be transmitted beyond expiration, so taking into account a
2173 // fudge factor for clock issues, assume an expiry date in the past indicates incorrect year
2174 if( an.expiry_time < now - wxTimeSpan::Hours( 24 ) ) {
2175 an.start_time.Set( an.day, wxDateTime::Month( an.month - 1 ),
2176 now.GetYear() + 1, an.hour, an.minute );
2177 an.expiry_time = an.start_time
2178 + wxTimeSpan::Minutes( an.duration_minutes );
2179 }
2180
2181 int subarea_count = ( bstr->GetBitCount() - 111 ) / 87;
2182 for( int i = 0; i < subarea_count; ++i ) {
2183 int base = 111 + i * 87;
2184 Ais8_001_22_SubArea sa;
2185 sa.shape = bstr->GetInt( base + 1, 3 );
2186 int scale_factor = 1;
2187 if( sa.shape == AIS8_001_22_SHAPE_TEXT ) {
2188 char t[15];
2189 t[14] = 0;
2190 bstr->GetStr( base + 4, 84, t, 14 );
2191 sa.text = wxString( t, wxConvUTF8 );
2192 } else {
2193 int scale_multipliers[4] = { 1, 10, 100, 1000 };
2194 scale_factor = scale_multipliers[bstr->GetInt( base + 4, 2 )];
2195 switch( sa.shape ){
2196 case AIS8_001_22_SHAPE_CIRCLE:
2197 case AIS8_001_22_SHAPE_SECTOR:
2198 sa.radius_m = bstr->GetInt( base + 58, 12 ) * scale_factor;
2199 // FALL THROUGH
2200 case AIS8_001_22_SHAPE_RECT:
2201 sa.longitude = bstr->GetInt( base + 6, 25, true ) / 60000.0;
2202 sa.latitude = bstr->GetInt( base + 31, 24, true ) / 60000.0;
2203 break;
2204 case AIS8_001_22_SHAPE_POLYLINE:
2205 case AIS8_001_22_SHAPE_POLYGON:
2206 for( int i = 0; i < 4; ++i ) {
2207 sa.angles[i] = bstr->GetInt( base + 6 + i * 20, 10 )
2208 * 0.5;
2209 sa.dists_m[i] = bstr->GetInt( base + 16 + i * 20, 10 )
2210 * scale_factor;
2211 }
2212 }
2213 if( sa.shape == AIS8_001_22_SHAPE_RECT ) {
2214 sa.e_dim_m = bstr->GetInt( base + 58, 8 ) * scale_factor;
2215 sa.n_dim_m = bstr->GetInt( base + 66, 8 ) * scale_factor;
2216 sa.orient_deg = bstr->GetInt( base + 74, 9 );
2217 }
2218 if( sa.shape == AIS8_001_22_SHAPE_SECTOR ) {
2219 sa.left_bound_deg = bstr->GetInt( 70, 9 );
2220 sa.right_bound_deg = bstr->GetInt( 79, 9 );
2221 }
2222 }
2223 an.sub_areas.push_back( sa );
2224 }
2225 ptd->area_notices[an.link_id] = an;
2226 parse_result = true;
2227 }
2228 }
2229 }
2230 break;
2231 }
2232 case 14: // Safety Related Broadcast
2233 {
2234 // Always capture the MSG_14 text
2235 char msg_14_text[968];
2236 if( bstr->GetBitCount() > 40 ) {
2237 int nx = ( ( bstr->GetBitCount() - 40 ) / 6 ) * 6;
2238 int nd = bstr->GetStr( 41, nx, msg_14_text, 968 );
2239 nd = wxMax(0, nd);
2240 nd = wxMin(nd, 967);
2241 msg_14_text[nd] = 0;
2242 ptd->MSG_14_text = wxString( msg_14_text, wxConvUTF8 );
2243 }
2244 parse_result = true; // so far so good
2245
2246 break;
2247 }
2248
2249 case 6: // Addressed Binary Message
2250 {
2251 break;
2252 }
2253 case 7: // Binary Ack
2254 {
2255 break;
2256 }
2257 default: {
2258 break;
2259 }
2260
2261 }
2262
2263 if( b_posn_report ) ptd->b_lost = false;
2264
2265 if( true == parse_result ) {
2266 // Revalidate the target under some conditions
2267 if( !ptd->b_active && !ptd->b_positionDoubtful && b_posn_report ) ptd->b_active = true;
2268 }
2269
2270 return parse_result;
2271 }
2272
NMEACheckSumOK(const wxString & str_in)2273 bool AIS_Decoder::NMEACheckSumOK( const wxString& str_in )
2274 {
2275
2276 unsigned char checksum_value = 0;
2277 int sentence_hex_sum;
2278
2279 wxCharBuffer buf = str_in.ToUTF8();
2280 if( !buf.data())
2281 return false; // cannot decode string
2282
2283 char str_ascii[AIS_MAX_MESSAGE_LEN + 1];
2284 strncpy( str_ascii, buf.data(), AIS_MAX_MESSAGE_LEN );
2285 str_ascii[AIS_MAX_MESSAGE_LEN] = '\0';
2286
2287 int string_length = strlen( str_ascii );
2288
2289 int payload_length = 0;
2290 while( ( payload_length < string_length ) && ( str_ascii[payload_length] != '*' ) ) // look for '*'
2291 payload_length++;
2292
2293 if( payload_length == string_length ) return false; // '*' not found at all, no checksum
2294
2295 int index = 1; // Skip over the $ at the begining of the sentence
2296
2297 while( index < payload_length ) {
2298 checksum_value ^= str_ascii[index];
2299 index++;
2300 }
2301
2302 if( string_length > 4 ) {
2303 char scanstr[3];
2304 scanstr[0] = str_ascii[payload_length + 1];
2305 scanstr[1] = str_ascii[payload_length + 2];
2306 scanstr[2] = 0;
2307 sscanf( scanstr, "%2x", &sentence_hex_sum );
2308
2309 if( sentence_hex_sum == checksum_value ) return true;
2310 }
2311
2312 return false;
2313 }
2314
UpdateAllCPA(void)2315 void AIS_Decoder::UpdateAllCPA( void )
2316 {
2317 // Iterate thru all the targets
2318 AIS_Target_Hash::iterator it;
2319 AIS_Target_Hash *current_targets = GetTargetList();
2320
2321 for( it = ( *current_targets ).begin(); it != ( *current_targets ).end(); ++it ) {
2322 AIS_Target_Data *td = it->second;
2323
2324 if( NULL != td ) UpdateOneCPA( td );
2325 }
2326 }
2327
UpdateAllTracks(void)2328 void AIS_Decoder::UpdateAllTracks( void )
2329 {
2330 // Iterate thru all the targets
2331 AIS_Target_Hash::iterator it;
2332 AIS_Target_Hash *current_targets = GetTargetList();
2333
2334 for( it = ( *current_targets ).begin(); it != ( *current_targets ).end(); ++it ) {
2335 AIS_Target_Data *td = it->second;
2336
2337 if( NULL != td ) UpdateOneTrack( td );
2338 }
2339 }
2340
UpdateOneTrack(AIS_Target_Data * ptarget)2341 void AIS_Decoder::UpdateOneTrack( AIS_Target_Data *ptarget )
2342 {
2343 if( !ptarget->b_positionOnceValid ) return;
2344 // Reject for unbelievable jumps (corrupted/bad data)
2345 if ( ptarget->m_ptrack->GetCount() > 0 )
2346 {
2347 AISTargetTrackPoint *LastTrackpoint = ptarget->m_ptrack->GetLast()->GetData();
2348 if ( fabs( LastTrackpoint->m_lat - ptarget->Lat ) > .1 || fabs( LastTrackpoint->m_lon - ptarget->Lon ) > .1 )
2349 {
2350 // after an unlikely jump in pos, the last trackpoint might also be wrong
2351 // just to be sure we do delete this one as well.
2352 ptarget->m_ptrack->pop_back();
2353 ptarget->b_positionDoubtful = true;
2354 return;
2355 }
2356 }
2357
2358 // Add the newest point
2359 AISTargetTrackPoint *ptrackpoint = new AISTargetTrackPoint;
2360 ptrackpoint->m_lat = ptarget->Lat;
2361 ptrackpoint->m_lon = ptarget->Lon;
2362 ptrackpoint->m_time = wxDateTime::Now().GetTicks();
2363
2364 ptarget->m_ptrack->Append( ptrackpoint );
2365
2366 if( ptarget->b_PersistTrack )
2367 {
2368 Track *t;
2369 if ( 0 == m_persistent_tracks.count( ptarget->MMSI ) )
2370 {
2371 t = new Track();
2372 t->SetName( wxString::Format( _T("AIS %s (%u) %s %s"), ptarget->GetFullName().c_str(), ptarget->MMSI, wxDateTime::Now().FormatISODate().c_str(), wxDateTime::Now().FormatISOTime().c_str() ) );
2373 pTrackList->Append( t );
2374 pConfig->AddNewTrack( t );
2375 m_persistent_tracks[ptarget->MMSI] = t;
2376 }
2377 else
2378 {
2379 t = m_persistent_tracks[ptarget->MMSI];
2380 }
2381 TrackPoint *tp = t->GetLastPoint();
2382 vector2D point( ptrackpoint->m_lon, ptrackpoint->m_lat );
2383 TrackPoint *tp1 = t->AddNewPoint( point, wxDateTime(ptrackpoint->m_time).ToUTC() );
2384 if( tp )
2385 {
2386 pSelect->AddSelectableTrackSegment( tp->m_lat, tp->m_lon, tp1->m_lat,
2387 tp1->m_lon, tp, tp1, t );
2388 }
2389
2390 //We do not want dependency on the GUI here, do we?
2391 // if( pRouteManagerDialog && pRouteManagerDialog->IsShown() )
2392 // pRouteManagerDialog->UpdateTrkListCtrl();
2393 }
2394
2395 // Walk the list, removing any track points that are older than the stipulated time
2396
2397 time_t test_time = wxDateTime::Now().GetTicks() - (time_t) ( g_AISShowTracks_Mins * 60 );
2398
2399 wxAISTargetTrackListNode *node = ptarget->m_ptrack->GetFirst();
2400 while( node ) {
2401 AISTargetTrackPoint *ptrack_point = node->GetData();
2402
2403 if( ptrack_point->m_time < test_time ) {
2404 if( ptarget->m_ptrack->DeleteObject( ptrack_point ) ) {
2405 node = ptarget->m_ptrack->GetFirst(); // restart the list
2406 }
2407 } else
2408 node = node->GetNext();
2409 }
2410 }
2411
DeletePersistentTrack(Track * track)2412 void AIS_Decoder::DeletePersistentTrack( Track *track )
2413 {
2414 for(std::map<int, Track*>::iterator iterator = m_persistent_tracks.begin(); iterator != m_persistent_tracks.end(); iterator++)
2415 {
2416 if( iterator->second == track )
2417 {
2418 m_persistent_tracks.erase(iterator);
2419 break;
2420 }
2421 }
2422 }
2423
UpdateAllAlarms(void)2424 void AIS_Decoder::UpdateAllAlarms( void )
2425 {
2426 m_bGeneralAlert = false; // no alerts yet
2427
2428 // Iterate thru all the targets
2429 AIS_Target_Hash::iterator it;
2430 AIS_Target_Hash *current_targets = GetTargetList();
2431
2432 for( it = ( *current_targets ).begin(); it != ( *current_targets ).end(); ++it ) {
2433 AIS_Target_Data *td = it->second;
2434
2435 if( NULL != td ) {
2436 // Maintain General Alert
2437 if( !m_bGeneralAlert ) {
2438 // Quick check on basic condition
2439 if( ( td->CPA < g_CPAWarn_NM ) && ( td->TCPA > 0 ) && ( td->Class != AIS_ATON ) && ( td->Class != AIS_BASE ) )
2440 m_bGeneralAlert = true;
2441
2442 // Some options can suppress general alerts
2443 if( g_bAIS_CPA_Alert_Suppress_Moored && ( td->SOG <= g_ShowMoored_Kts ) )
2444 m_bGeneralAlert = false;
2445
2446 // Skip distant targets if requested
2447 if( ( g_bCPAMax ) && ( td->Range_NM > g_CPAMax_NM ) )
2448 m_bGeneralAlert = false;
2449
2450 // Skip if TCPA is too long
2451 if( ( g_bTCPA_Max ) && ( td->TCPA > g_TCPA_Max ) )
2452 m_bGeneralAlert = false;
2453
2454 // SART targets always alert if "Active"
2455 if( td->Class == AIS_SART && td->NavStatus == 14)
2456 m_bGeneralAlert = true;
2457
2458 // DSC Distress targets always alert
2459 if( ( td->Class == AIS_DSC ) && ( td->ShipType == 12 ) )
2460 m_bGeneralAlert = true;
2461 }
2462
2463 ais_alert_type this_alarm = AIS_NO_ALERT;
2464
2465 // SART targets always alert if "Active"
2466 if( td->Class == AIS_SART && td->NavStatus == 14)
2467 this_alarm = AIS_ALERT_SET;
2468
2469 // DSC Distress targets always alert
2470 if( ( td->Class == AIS_DSC ) && ( td->ShipType == 12 ) )
2471 this_alarm = AIS_ALERT_SET;
2472
2473 if( g_bCPAWarn && td->b_active && td->b_positionOnceValid &&
2474 ( td->Class != AIS_SART ) && ( td->Class != AIS_DSC ) ) {
2475 // Skip anchored/moored(interpreted as low speed) targets if requested
2476 if( ( g_bHideMoored ) && ( td->SOG <= g_ShowMoored_Kts ) ) { // dsr
2477 td->n_alert_state = AIS_NO_ALERT;
2478 continue;
2479 }
2480
2481 // No Alert on moored(interpreted as low speed) targets if so requested
2482 if( g_bAIS_CPA_Alert_Suppress_Moored && ( td->SOG <= g_ShowMoored_Kts ) ) { // dsr
2483 td->n_alert_state = AIS_NO_ALERT;
2484 continue;
2485 }
2486
2487 // No alert for my Follower
2488 bool hit = false;
2489 for(unsigned int i=0 ; i < g_MMSI_Props_Array.GetCount() ; i++){
2490 MMSIProperties *props = g_MMSI_Props_Array[i];
2491 if(td->MMSI == props->MMSI){
2492 if (props->m_bFollower) {
2493 hit = true;
2494 td->n_alert_state = AIS_NO_ALERT;
2495 }
2496 break;
2497 }
2498 }
2499 if (hit) continue;
2500
2501 // Skip distant targets if requested
2502 if( g_bCPAMax ) {
2503 if( td->Range_NM > g_CPAMax_NM ) {
2504 td->n_alert_state = AIS_NO_ALERT;
2505 continue;
2506 }
2507 }
2508
2509 if( ( td->CPA < g_CPAWarn_NM ) && ( td->TCPA > 0 ) && ( td->Class != AIS_ATON ) && ( td->Class != AIS_BASE )) {
2510 if( g_bTCPA_Max ) {
2511 if( td->TCPA < g_TCPA_Max ) this_alarm = AIS_ALERT_SET;
2512 } else
2513 this_alarm = AIS_ALERT_SET;
2514 }
2515 }
2516
2517
2518 // Maintain the timer for in_ack flag
2519 // SART and DSC targets always maintain ack timeout
2520
2521 if( g_bAIS_ACK_Timeout || (td->Class == AIS_SART) || ((td->Class == AIS_DSC) && (td->ShipType == 12))) {
2522 if( td->b_in_ack_timeout ) {
2523 wxTimeSpan delta = wxDateTime::Now() - td->m_ack_time;
2524 if( delta.GetMinutes() > g_AckTimeout_Mins ) td->b_in_ack_timeout = false;
2525 }
2526 } else {
2527 // Not using ack timeouts.
2528 // If a target has been acknowledged, leave it ack'ed until it goes out of AIS_ALARM_SET state
2529 if( td->b_in_ack_timeout ){
2530 if( this_alarm == AIS_NO_ALERT )
2531 td->b_in_ack_timeout = false;
2532 }
2533 }
2534
2535 td->n_alert_state = this_alarm;
2536
2537 }
2538 }
2539 }
2540
UpdateOneCPA(AIS_Target_Data * ptarget)2541 void AIS_Decoder::UpdateOneCPA( AIS_Target_Data *ptarget )
2542 {
2543 ptarget->Range_NM = -1.; // Defaults
2544 ptarget->Brg = -1.;
2545
2546 // Compute the current Range/Brg to the target
2547 // This should always be possible even if GPS data is not valid
2548 // because O must always have a position for own-ship. Plugins need
2549 // AIS target range and bearing from own-ship position even if GPS is not valid.
2550 double brg, dist;
2551 DistanceBearingMercator( ptarget->Lat, ptarget->Lon, gLat, gLon, &brg, &dist );
2552 ptarget->Range_NM = dist;
2553 ptarget->Brg = brg;
2554
2555 if( dist <= 1e-5 ) ptarget->Brg = -1.0; // Brg is undefined if Range == 0.
2556
2557 if( !ptarget->b_positionOnceValid || !bGPSValid ) {
2558 ptarget->bCPA_Valid = false;
2559 return;
2560 }
2561
2562 // There can be no collision between ownship and itself....
2563 // This can happen if AIVDO messages are received, and there is another source of ownship position, like NMEA GLL
2564 // The two positions are always temporally out of sync, and one will always be exactly in front of the other one.
2565 if( ptarget->b_OwnShip ) {
2566 ptarget->CPA = 100;
2567 ptarget->TCPA = -100;
2568 ptarget->bCPA_Valid = false;
2569 return;
2570 }
2571
2572 double cpa_calc_ownship_cog = gCog;
2573 double cpa_calc_target_cog = ptarget->COG;
2574
2575 // Ownship is not reporting valid SOG, so no way to calculate CPA
2576 if( std::isnan(gSog) || ( gSog > 102.2 ) ) {
2577 ptarget->bCPA_Valid = false;
2578 return;
2579 }
2580
2581 // Ownship is maybe anchored and not reporting COG
2582 if( std::isnan(gCog) || gCog == 360.0 ) {
2583 if( gSog < .01 ) cpa_calc_ownship_cog = 0.; // substitute value
2584 // for the case where SOG ~= 0, and COG is unknown.
2585 else {
2586 ptarget->bCPA_Valid = false;
2587 return;
2588 }
2589 }
2590
2591 // Target is maybe anchored and not reporting COG
2592 if( ptarget->COG == 360.0 ) {
2593 if( ptarget->SOG > 102.2 ) {
2594 ptarget->bCPA_Valid = false;
2595 return;
2596 } else if( ptarget->SOG < .01 ) cpa_calc_target_cog = 0.; // substitute value
2597 // for the case where SOG ~= 0, and COG is unknown.
2598 else {
2599 ptarget->bCPA_Valid = false;
2600 return;
2601 }
2602 }
2603
2604 // Express the SOGs as meters per hour
2605 double v0 = gSog * 1852.;
2606 double v1 = ptarget->SOG * 1852.;
2607
2608 if( ( v0 < 1e-6 ) && ( v1 < 1e-6 ) ) {
2609 ptarget->TCPA = 0.;
2610 ptarget->CPA = 0.;
2611
2612 ptarget->bCPA_Valid = false;
2613 } else {
2614 // Calculate the TCPA first
2615
2616 // Working on a Reduced Lat/Lon orthogonal plotting sheet....
2617 // Get easting/northing to target, in meters
2618
2619 double east1 = ( ptarget->Lon - gLon ) * 60 * 1852;
2620 double north1 = ( ptarget->Lat - gLat ) * 60 * 1852;
2621
2622 double east = east1 * ( cos( gLat * PI / 180. ) );
2623
2624 double north = north1;
2625
2626 // Convert COGs trigonometry to standard unit circle
2627 double cosa = cos( ( 90. - cpa_calc_ownship_cog ) * PI / 180. );
2628 double sina = sin( ( 90. - cpa_calc_ownship_cog ) * PI / 180. );
2629 double cosb = cos( ( 90. - cpa_calc_target_cog ) * PI / 180. );
2630 double sinb = sin( ( 90. - cpa_calc_target_cog ) * PI / 180. );
2631
2632 // These will be useful
2633 double fc = ( v0 * cosa ) - ( v1 * cosb );
2634 double fs = ( v0 * sina ) - ( v1 * sinb );
2635
2636 double d = ( fc * fc ) + ( fs * fs );
2637 double tcpa;
2638
2639 // the tracks are almost parallel
2640 if( fabs( d ) < 1e-6 ) tcpa = 0.;
2641 else
2642 // Here is the equation for t, which will be in hours
2643 tcpa = ( ( fc * east ) + ( fs * north ) ) / d;
2644
2645 // Convert to minutes
2646 ptarget->TCPA = tcpa * 60.;
2647
2648 // Calculate CPA
2649 // Using TCPA, predict ownship and target positions
2650
2651 double OwnshipLatCPA, OwnshipLonCPA, TargetLatCPA, TargetLonCPA;
2652
2653 ll_gc_ll( gLat, gLon, cpa_calc_ownship_cog, gSog * tcpa, &OwnshipLatCPA, &OwnshipLonCPA );
2654 ll_gc_ll( ptarget->Lat, ptarget->Lon, cpa_calc_target_cog, ptarget->SOG * tcpa,
2655 &TargetLatCPA, &TargetLonCPA );
2656
2657 // And compute the distance
2658 ptarget->CPA = DistGreatCircle( OwnshipLatCPA, OwnshipLonCPA, TargetLatCPA, TargetLonCPA );
2659
2660 ptarget->bCPA_Valid = true;
2661
2662 if( ptarget->TCPA < 0 ) ptarget->bCPA_Valid = false;
2663 }
2664 }
2665
OnSoundFinishedAISAudio(wxCommandEvent & event)2666 void AIS_Decoder::OnSoundFinishedAISAudio( wxCommandEvent& event )
2667 {
2668 // By clearing this flag the main event loop will trigger repeated
2669 // sounds for as long as the alert condition remains.
2670 m_bAIS_AlertPlaying = false;
2671 }
2672
OnTimerDSC(wxTimerEvent & event)2673 void AIS_Decoder::OnTimerDSC( wxTimerEvent& event )
2674 {
2675 // Timer expired, no CDDSE message was received, so accept the latest CDDSC message
2676 if(m_ptentative_dsctarget){
2677 ProcessDSx( m_dsc_last_string, true );
2678 }
2679 }
2680
2681
OnTimerAIS(wxTimerEvent & event)2682 void AIS_Decoder::OnTimerAIS( wxTimerEvent& event )
2683 {
2684 TimerAIS.Stop();
2685
2686 // Scrub the target hash list
2687 // removing any targets older than stipulated age
2688
2689 wxDateTime now = wxDateTime::Now();
2690 now.MakeGMT();
2691
2692 AIS_Target_Hash::iterator it;
2693 AIS_Target_Hash *current_targets = GetTargetList();
2694
2695 it = ( *current_targets ).begin();
2696 std::vector<int> remove_array; // collector for MMSI of targets to be removed
2697
2698 while( it != ( *current_targets ).end() ) {
2699 AIS_Target_Data *td = it->second;
2700
2701 if( NULL == td ) // This should never happen, but I saw it once....
2702 {
2703 current_targets->erase( it );
2704 break; // leave the loop
2705 }
2706
2707 int target_posn_age = now.GetTicks() - td->PositionReportTicks;
2708 int target_static_age = now.GetTicks() - td->StaticReportTicks;
2709
2710 // Global variables controlling lost target handling
2711 //g_bMarkLost
2712 //g_MarkLost_Mins // Minutes until black "cross out
2713 //g_bRemoveLost
2714 //g_RemoveLost_Mins); // minutes until target is removed from screen and internal lists
2715
2716 //g_bInlandEcdis
2717
2718 // Mark lost targets if specified
2719 double removelost_Mins = fmax(g_RemoveLost_Mins,g_MarkLost_Mins);
2720
2721 if (g_bInlandEcdis && (td->Class != AIS_ARPA)) {
2722 double iECD_LostTimeOut = 0.0;
2723 //special rules apply for europe inland ecdis timeout settings. overrule option settings
2724 //Won't apply for ARPA targets where the radar has all control
2725 if ( td->Class == AIS_CLASS_B){
2726 if( (td->NavStatus == MOORED) || (td->NavStatus == AT_ANCHOR) )
2727 iECD_LostTimeOut = 18 * 60;
2728 else
2729 iECD_LostTimeOut = 180;
2730
2731 }
2732 if ( td->Class == AIS_CLASS_A){
2733 if( (td->NavStatus == MOORED) || (td->NavStatus == AT_ANCHOR) ){
2734 if(td->SOG < 3.)
2735 iECD_LostTimeOut = 18 * 60;
2736 else
2737 iECD_LostTimeOut = 60;
2738 }
2739 else
2740 iECD_LostTimeOut = 60;
2741 }
2742
2743 if( ( target_posn_age > iECD_LostTimeOut ) && ( td->Class != AIS_GPSG_BUDDY ) )
2744 td->b_active = false;
2745
2746 removelost_Mins = (2 * iECD_LostTimeOut) / 60.;
2747 }
2748 else if( g_bMarkLost ) {
2749 if( ( target_posn_age > g_MarkLost_Mins * 60 ) && ( td->Class != AIS_GPSG_BUDDY ) )
2750 td->b_active = false;
2751 }
2752
2753 if( td->Class == AIS_SART )
2754 removelost_Mins = 18.0;
2755
2756 // Remove lost targets if specified
2757
2758 if( g_bRemoveLost || g_bInlandEcdis ) {
2759 bool b_arpalost = ( td->Class == AIS_ARPA && td->b_lost ); //A lost ARPA target would be deleted at once
2760 if ( ( ( target_posn_age > removelost_Mins * 60 ) && ( td->Class != AIS_GPSG_BUDDY ) ) || b_arpalost ) {
2761 // So mark the target as lost, with unknown position, and make it not selectable
2762 td->b_lost = true;
2763 td->b_positionOnceValid = false;
2764 td->COG = 360.0;
2765 td->SOG = 103.0;
2766 td->HDG = 511.0;
2767 td->ROTAIS = -128;
2768
2769 SendJSONMsg(td);
2770
2771 long mmsi_long = td->MMSI;
2772 pSelectAIS->DeleteSelectablePoint( (void *) mmsi_long, SELTYPE_AISTARGET );
2773
2774 // If we have not seen a static report in 3 times the removal spec,
2775 // then remove the target from all lists
2776 // or a lost ARPA target.
2777 if ( target_static_age > removelost_Mins * 60 * 3 || b_arpalost ) {
2778 td->b_removed = true;
2779 SendJSONMsg(td);
2780 remove_array.push_back(td->MMSI); //Add this target to removal list
2781 }
2782 }
2783 }
2784
2785 // Remove any targets specified as to be "ignored", so that they won't trigger phantom alerts (e.g. SARTs)
2786 for(unsigned int i=0 ; i < g_MMSI_Props_Array.GetCount() ; i++){
2787 MMSIProperties *props = g_MMSI_Props_Array[i];
2788 if(td->MMSI == props->MMSI){
2789 if(props->m_bignore) {
2790 remove_array.push_back(td->MMSI); //Add this target to removal list
2791 td->b_removed = true;
2792 SendJSONMsg(td);
2793 }
2794 break;
2795 }
2796 }
2797
2798
2799 ++it;
2800 }
2801
2802 // Remove all the targets collected in remove_array in one pass
2803 for(unsigned int i=0 ; i < remove_array.size() ; i++){
2804 AIS_Target_Hash::iterator itd = current_targets->find( remove_array[i] );
2805 if(itd != current_targets->end() ){
2806 AIS_Target_Data *td = itd->second;
2807 current_targets->erase(itd);
2808 delete td;
2809 }
2810 }
2811
2812 UpdateAllCPA();
2813 UpdateAllAlarms();
2814
2815 // Update the general suppression flag
2816 m_bSuppressed = false;
2817 if( g_bAIS_CPA_Alert_Suppress_Moored || g_bHideMoored
2818 || (g_bShowScaled && g_bAllowShowScaled) )
2819 m_bSuppressed = true;
2820
2821 m_bAIS_Audio_Alert_On = false; // default, may be set on
2822
2823 // Process any Alarms
2824
2825 // If the AIS Alert Dialog is not currently shown....
2826
2827 // Scan all targets, looking for SART, DSC Distress, and CPA incursions
2828 // In the case of multiple targets of the same type, select the shortest range or shortest TCPA
2829 AIS_Target_Data *palert_target = NULL;
2830
2831 if( NULL == g_pais_alert_dialog_active ) {
2832 pAISMOBRoute = NULL; // Reset the AISMOB auto route.
2833 double tcpa_min = 1e6; // really long
2834 double sart_range = 1e6;
2835 AIS_Target_Data *palert_target_cpa = NULL;
2836 AIS_Target_Data *palert_target_sart = NULL;
2837 AIS_Target_Data *palert_target_dsc = NULL;
2838
2839 for( it = ( *current_targets ).begin(); it != ( *current_targets ).end(); ++it ) {
2840 AIS_Target_Data *td = it->second;
2841 if( td ) {
2842 if( (td->Class != AIS_SART) && (td->Class != AIS_DSC) ) {
2843
2844 if( g_bAIS_CPA_Alert && td->b_active ) {
2845 if( ( AIS_ALERT_SET == td->n_alert_state ) && !td->b_in_ack_timeout ) {
2846 if( td->TCPA < tcpa_min ) {
2847 tcpa_min = td->TCPA;
2848 palert_target_cpa = td;
2849 }
2850 }
2851 }
2852 }
2853 else if( (td->Class == AIS_DSC ) && ( td->ShipType == 12) ){
2854 if( td->b_active ) {
2855 if( ( AIS_ALERT_SET == td->n_alert_state ) && !td->b_in_ack_timeout ) {
2856 palert_target_dsc = td;
2857 }
2858 }
2859 }
2860
2861 else if( td->Class == AIS_SART ){
2862 if( td->b_active ) {
2863 if( ( AIS_ALERT_SET == td->n_alert_state ) && !td->b_in_ack_timeout ) {
2864 if( td->Range_NM < sart_range ) {
2865 tcpa_min = sart_range;
2866 palert_target_sart = td;
2867 }
2868 }
2869 }
2870 }
2871 }
2872 }
2873
2874 // Which of multiple targets?
2875 // Give priority to SART targets, then DSC Distress, then CPA incursion
2876
2877 palert_target = palert_target_cpa;
2878
2879 if( palert_target_sart )
2880 palert_target = palert_target_sart;
2881
2882 if( palert_target_dsc )
2883 palert_target = palert_target_dsc;
2884
2885
2886 // Show the alert
2887 if( palert_target ) {
2888
2889 bool b_jumpto = (palert_target->Class == AIS_SART) || (palert_target->Class == AIS_DSC);
2890 bool b_createWP = palert_target->Class == AIS_DSC;
2891 bool b_ack = palert_target->Class != AIS_DSC;
2892
2893 // Show the Alert dialog
2894
2895 // See FS# 968/998
2896 // If alert occurs while OCPN is iconized to taskbar, then clicking the taskbar icon
2897 // only brings up the Alert dialog, and not the entire application.
2898 // This is an OS specific behavior, not seen on linux or Mac.
2899 // This patch will allow the audio alert to occur, and the visual alert will pop up soon
2900 // after the user selects the OCPN icon from the taskbar. (on the next timer tick, probably)
2901
2902 #ifndef __OCPN__ANDROID__
2903 if( gFrame->IsIconized() || !gFrame->IsActive() )
2904 gFrame->RequestUserAttention();
2905 #endif
2906
2907 if( !gFrame->IsIconized() ){
2908 AISTargetAlertDialog *pAISAlertDialog = new AISTargetAlertDialog();
2909 pAISAlertDialog->Create( palert_target->MMSI, m_parent_frame, this,
2910 b_jumpto, b_createWP, b_ack,
2911 -1, _("AIS Alert"));
2912 g_Platform->PositionAISAlert(pAISAlertDialog);
2913
2914 g_pais_alert_dialog_active = pAISAlertDialog;
2915 pAISAlertDialog->Show(); // Show modeless, so it stays on the screen
2916 }
2917
2918 // Audio alert if requested
2919 m_bAIS_Audio_Alert_On = true; // always on when alert is first shown
2920 }
2921 }
2922
2923 // The AIS Alert dialog is already shown. If the dialog MMSI number is still alerted, update the dialog
2924 // otherwise, destroy the dialog
2925 else {
2926 palert_target = Get_Target_Data_From_MMSI( g_pais_alert_dialog_active->Get_Dialog_MMSI() );
2927
2928 if( palert_target ) {
2929 if( ( ( AIS_ALERT_SET == palert_target->n_alert_state )
2930 && !palert_target->b_in_ack_timeout )
2931 || ( palert_target->Class == AIS_SART ) ) {
2932 g_pais_alert_dialog_active->UpdateText();
2933 } else {
2934 g_pais_alert_dialog_active->Close();
2935 m_bAIS_Audio_Alert_On = false;
2936 }
2937
2938 if( true == palert_target->b_suppress_audio )
2939 m_bAIS_Audio_Alert_On = false;
2940 else
2941 m_bAIS_Audio_Alert_On = true;
2942 } else { // this should not happen, however...
2943 g_pais_alert_dialog_active->Close();
2944 m_bAIS_Audio_Alert_On = false;
2945 }
2946
2947 }
2948
2949 // At this point, the audio flag is set
2950 // Honor the global flag
2951 if( !g_bAIS_CPA_Alert_Audio )
2952 m_bAIS_Audio_Alert_On = false;
2953
2954 if( m_bAIS_Audio_Alert_On ) {
2955 if (!m_AIS_Sound) {
2956 m_AIS_Sound = SoundFactory();
2957 }
2958 if ( !AIS_AlertPlaying() ) {
2959 m_bAIS_AlertPlaying = true;
2960 m_AIS_Sound->SetCmd( g_CmdSoundString.mb_str( wxConvUTF8 ) );
2961 m_AIS_Sound->Load(g_sAIS_Alert_Sound_File, g_iSoundDeviceIndex);
2962 if ( m_AIS_Sound->IsOk( ) ) {
2963 m_AIS_Sound->SetFinishedCallback( onSoundFinished, this );
2964 if ( !m_AIS_Sound->Play( ) )
2965 m_bAIS_AlertPlaying = false;
2966 }
2967 else
2968 m_bAIS_AlertPlaying = false;
2969 }
2970 }
2971 // If a SART Alert is active, check to see if the MMSI has special properties set
2972 // indicating that this Alert is a MOB for THIS ship.
2973 if(palert_target && (palert_target->Class == AIS_SART) ){
2974 for(unsigned int i=0 ; i < g_MMSI_Props_Array.GetCount() ; i++){
2975 if(palert_target->MMSI == g_MMSI_Props_Array[i]->MMSI){
2976 if(pAISMOBRoute)
2977 gFrame->UpdateAISMOBRoute(palert_target);
2978 else
2979 gFrame->ActivateAISMOBRoute(palert_target);
2980 break;
2981 }
2982 }
2983 }
2984
2985 TimerAIS.Start( TIMER_AIS_MSEC, wxTIMER_CONTINUOUS );
2986 }
2987
Get_Target_Data_From_MMSI(int mmsi)2988 AIS_Target_Data *AIS_Decoder::Get_Target_Data_From_MMSI( int mmsi )
2989 {
2990 if( AISTargetList->find( mmsi ) == AISTargetList->end() ) // if entry does not exist....
2991 return NULL;
2992 else
2993 return ( *AISTargetList )[mmsi]; // find current entry
2994 }
2995
2996
2997
2998 ArrayOfMMSIProperties g_MMSI_Props_Array;
2999
3000
3001 // MMSIProperties Implementation
3002
MMSIProperties(wxString & spec)3003 MMSIProperties::MMSIProperties( wxString &spec )
3004 {
3005 Init();
3006 wxStringTokenizer tkz( spec, _T(";") );
3007 wxString s;
3008
3009 s = tkz.GetNextToken();
3010 long mmsil;
3011 s.ToLong(&mmsil);
3012 MMSI = (int)mmsil;
3013
3014 s = tkz.GetNextToken();
3015 if(s.Len()){
3016 if(s.Upper() == _T("ALWAYS"))
3017 TrackType = TRACKTYPE_ALWAYS;
3018 else if(s.Upper() == _T("NEVER"))
3019 TrackType = TRACKTYPE_NEVER;
3020 }
3021
3022 s = tkz.GetNextToken();
3023 if(s.Len()){
3024 if(s.Upper() == _T("IGNORE"))
3025 m_bignore = true;
3026 }
3027
3028 s = tkz.GetNextToken();
3029 if(s.Len()){
3030 if(s.Upper() == _T("MOB"))
3031 m_bMOB = true;
3032 }
3033
3034 s = tkz.GetNextToken();
3035 if(s.Len()){
3036 if(s.Upper() == _T("VDM"))
3037 m_bVDM = true;
3038 }
3039
3040 s = tkz.GetNextToken();
3041 if (s.Len()){
3042 if (s.Upper() == _T("FOLLOWER"))
3043 m_bFollower = true;
3044 }
3045
3046 s = tkz.GetNextToken();
3047 if(s.Len()){
3048 if(s.Upper() == _T("PERSIST"))
3049 m_bPersistentTrack = true;
3050 }
3051
3052 s = tkz.GetNextToken();
3053 if (s.Len()){
3054 m_ShipName = s.Upper();
3055 }
3056 }
3057
~MMSIProperties()3058 MMSIProperties::~MMSIProperties()
3059 {
3060 }
3061
Init(void)3062 void MMSIProperties::Init(void )
3063 {
3064 MMSI = -1;
3065 TrackType = TRACKTYPE_DEFAULT;
3066 m_bignore = false;
3067 m_bMOB = false;
3068 m_bVDM = false;
3069 m_bFollower = false;
3070 m_bPersistentTrack = false;
3071 m_ShipName = wxEmptyString;
3072 }
3073
Serialize(void)3074 wxString MMSIProperties::Serialize( void )
3075 {
3076 wxString sMMSI;
3077 wxString s;
3078
3079 sMMSI.Printf(_T("%d"), MMSI);
3080 sMMSI << _T(";");
3081
3082 if(TrackType){
3083 if(TRACKTYPE_ALWAYS == TrackType)
3084 sMMSI << _T("always");
3085 else if(TRACKTYPE_NEVER == TrackType)
3086 sMMSI << _T("never");
3087 }
3088 sMMSI << _T(";");
3089
3090 if(m_bignore){
3091 sMMSI << _T("ignore");
3092 }
3093 sMMSI << _T(";");
3094
3095 if(m_bMOB){
3096 sMMSI << _T("MOB");
3097 }
3098 sMMSI << _T(";");
3099
3100 if(m_bVDM){
3101 sMMSI << _T("VDM");
3102 }
3103 sMMSI << _T(";");
3104
3105 if (m_bFollower){
3106 sMMSI << _T("Follower");
3107 }
3108 sMMSI << _T(";");
3109
3110 if(m_bPersistentTrack){
3111 sMMSI << _T("PERSIST");
3112 }
3113 sMMSI << _T(";");
3114
3115 if (m_ShipName == wxEmptyString) {
3116 m_ShipName = GetShipNameFromFile(MMSI);
3117 }
3118 sMMSI << m_ShipName;
3119 return sMMSI;
3120 }
3121
AISshipNameCache(AIS_Target_Data * pTargetData,AIS_Target_Name_Hash * AISTargetNamesC,AIS_Target_Name_Hash * AISTargetNamesNC,long mmsi)3122 void AISshipNameCache(AIS_Target_Data *pTargetData,
3123 AIS_Target_Name_Hash *AISTargetNamesC,
3124 AIS_Target_Name_Hash *AISTargetNamesNC,
3125 long mmsi)
3126 {
3127 if (g_benableAISNameCache) {
3128 wxString ship_name = wxEmptyString;
3129
3130 // Check for valid name data
3131 if (!pTargetData->b_nameValid) {
3132 AIS_Target_Name_Hash::iterator it = AISTargetNamesC->find(mmsi);
3133 if (it != AISTargetNamesC->end()) {
3134 ship_name = (*AISTargetNamesC)[mmsi].Left(20);
3135 strncpy(pTargetData->ShipName, ship_name.mb_str(), ship_name.length() + 1);
3136 pTargetData->b_nameValid = true;
3137 pTargetData->b_nameFromCache = true;
3138 }
3139 else
3140 if (!g_bUseOnlyConfirmedAISName) {
3141 it = AISTargetNamesNC->find(mmsi);
3142 if (it != AISTargetNamesNC->end()) {
3143 ship_name = (*AISTargetNamesNC)[mmsi].Left(20);
3144 strncpy(pTargetData->ShipName, ship_name.mb_str(), ship_name.length() + 1);
3145 pTargetData->b_nameValid = true;
3146 pTargetData->b_nameFromCache = true;
3147 }
3148 }
3149 }
3150 // else there IS a valid name, lets check if it is in one of the hash lists.
3151 else if ((pTargetData->MID == 5) || (pTargetData->MID == 24) ||
3152 (pTargetData->MID == 19) || (pTargetData->MID == 123) ) { //123: Has got a name from SignalK
3153 // This message contains ship static data, so has a name field
3154 pTargetData->b_nameFromCache = false;
3155 ship_name = trimAISField(pTargetData->ShipName);
3156 AIS_Target_Name_Hash::iterator itC = AISTargetNamesC->find(mmsi);
3157 AIS_Target_Name_Hash::iterator itNC = AISTargetNamesNC->find(mmsi);
3158 if (itC != AISTargetNamesC->end())
3159 { //There is a confirmed entry for this mmsi
3160 if ((*AISTargetNamesC)[mmsi] == ship_name)
3161 { //Received name is same as confirmed name
3162 if (itNC != AISTargetNamesNC->end())
3163 { //there is also an entry in the NC list, delete it
3164 AISTargetNamesNC->erase(itNC);
3165 }
3166 }
3167 else
3168 { //There is a confirmed name but that one is different
3169 if (itNC != AISTargetNamesNC->end())
3170 { //there is an entry in the NC list check if name is same
3171 if ((*AISTargetNamesNC)[mmsi] == ship_name)
3172 { //Same name is already in NC list so promote till confirmed list
3173 (*AISTargetNamesC)[mmsi] = ship_name;
3174 // And delete from NC list
3175 AISTargetNamesNC->erase(itNC);
3176 }
3177 else { //A different name is in the NC list, update with received one
3178 (*AISTargetNamesNC)[mmsi] = ship_name;
3179 }
3180 if (g_bUseOnlyConfirmedAISName)
3181 strncpy(pTargetData->ShipName, (*AISTargetNamesC)[mmsi].mb_str(), (*AISTargetNamesC)[mmsi].Left(20).Length() + 1);
3182 }
3183 }
3184 }
3185 else { //No confirmed entry available
3186 if (itNC != AISTargetNamesNC->end())
3187 { //there is an entry in the NC list,
3188 if ((*AISTargetNamesNC)[mmsi] == ship_name)
3189 { //Received name same as already in NC list, promote to confirmen
3190 (*AISTargetNamesC)[mmsi] = ship_name;
3191 // And delete from NC list
3192 AISTargetNamesNC->erase(itNC);
3193 }
3194 else { //entry in NC list is not same as received one
3195 (*AISTargetNamesNC)[mmsi] = ship_name;
3196 }
3197 }
3198 else { //No entry in NC list so add it
3199 (*AISTargetNamesNC)[mmsi] = ship_name;
3200 }
3201 if (g_bUseOnlyConfirmedAISName) { //copy back previous name
3202 strncpy(pTargetData->ShipName, "Unknown ", SHIP_NAME_LEN);
3203 }
3204 }
3205 }
3206 }
3207 }
3208
GetShipNameFromFile(int nmmsi)3209 wxString GetShipNameFromFile(int nmmsi)
3210 {
3211 wxString name = wxEmptyString;
3212 if (g_benableAISNameCache){
3213 std::ifstream infile(AISTargetNameFileName.mb_str());
3214 if (infile) {
3215 std::string line;
3216 while (getline(infile, line)) {
3217 wxStringTokenizer tokenizer(wxString::FromUTF8(line.c_str()), _T(","));
3218 if (nmmsi == wxAtoi(tokenizer.GetNextToken())) {
3219 name = tokenizer.GetNextToken().Trim();
3220 break;
3221 }
3222 else tokenizer.GetNextToken();
3223 }
3224 }
3225 infile.close();
3226 }
3227 return name;
3228 }
3229
SendJSONMsg(AIS_Target_Data * pTarget)3230 void AIS_Decoder::SendJSONMsg(AIS_Target_Data* pTarget)
3231 {
3232 // Only send messages if someone is listening...
3233 if(!g_pi_manager->GetJSONMessageTargetCount())
3234 return;
3235
3236 // Do JSON message to all Plugin to inform of target
3237 wxJSONValue jMsg;
3238
3239 wxLongLong t = ::wxGetLocalTimeMillis();
3240
3241 jMsg[wxS("Source")] = wxS("AIS_Decoder");
3242 jMsg[wxT("Type")] = wxT("Information");
3243 jMsg[wxT("Msg")] = wxS("AIS Target");
3244 jMsg[wxT("MsgId")] = t.GetValue();
3245 jMsg[wxS("lat")] = pTarget->Lat;
3246 jMsg[wxS("lon")] = pTarget->Lon;
3247 jMsg[wxS("sog")] = pTarget->SOG;
3248 jMsg[wxS("cog")] = pTarget->COG;
3249 jMsg[wxS("hdg")] = pTarget->HDG;
3250 jMsg[wxS("mmsi")] = pTarget->MMSI;
3251 jMsg[wxS("class")] = pTarget->Class;
3252 jMsg[wxS("ownship")] = pTarget->b_OwnShip;
3253 jMsg[wxS("active")] = pTarget->b_active;
3254 jMsg[wxS("lost")] = pTarget->b_lost;
3255 wxString l_ShipName = wxString::FromUTF8(pTarget->ShipName);
3256 for(size_t i =0; i < l_ShipName.Len(); i++) {
3257 if(l_ShipName.GetChar(i) == '@') l_ShipName.SetChar(i, '\n');
3258 }
3259 jMsg[wxS("shipname")] = l_ShipName;
3260 wxString l_CallSign = wxString::FromUTF8(pTarget->CallSign);
3261 for(size_t i =0; i < l_CallSign.Len(); i++) {
3262 if(l_CallSign.GetChar(i) == '@') l_CallSign.SetChar(i, '\n');
3263 }
3264 jMsg[wxS("callsign")] = l_CallSign;
3265 jMsg[wxS("removed")] = pTarget->b_removed;
3266 g_pi_manager->SendJSONMessageToAllPlugins( wxT("AIS"), jMsg );
3267 }
3268