OpenCPN Partial API docs
Loading...
Searching...
No Matches
ais_decoder.cpp
Go to the documentation of this file.
1/**************************************************************************
2 * Copyright (C) 2010 by David S. Register *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
8 * *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, write to the *
16 * Free Software Foundation, Inc., *
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
18 ***************************************************************************/
19
26// For compilers that support precompilation, includes "wx.h".
27#include <wx/wxprec.h>
28
29#ifndef WX_PRECOMP
30#include <wx/wx.h>
31#endif
32
33#include <algorithm>
34#include <cstdio>
35#include <fstream>
36
37#ifdef __MINGW32__
38#undef IPV6STRICT // mingw FTBS fix: missing struct ip_mreq
39#include <windows.h>
40#endif
41
42#include <wx/datetime.h>
43#include <wx/event.h>
44#include <wx/log.h>
45#include <wx/string.h>
46#include <wx/textfile.h>
47#include <wx/timer.h>
48#include <wx/tokenzr.h>
49#include <wx/filename.h>
50
51// Be sure to include these before ais_decoder.h
52// to avoid a conflict with rapidjson/fwd.h
53#include "rapidjson/document.h"
54#include "rapidjson/writer.h"
55#include "rapidjson/stringbuffer.h"
56
57#include "model/ais_decoder.h"
59#include "model/meteo_points.h"
60#include "model/ais_target_data.h"
62#include "model/config_vars.h"
63#include "model/geodesic.h"
64#include "model/georef.h"
65#include "model/idents.h"
66#include "model/multiplexer.h"
67#include "model/navutil_base.h"
68#include "model/own_ship.h"
69#include "model/route_point.h"
70#include "model/select.h"
71#include "model/track.h"
72#include "N2KParser.h"
73
74#if !defined(NAN)
75static const long long lNaN = 0xfff8000000000000;
76#define NAN (*(double *)&lNaN)
77#endif
78
79wxEvtHandler *g_pais_alert_dialog_active;
80
81AisDecoder *g_pAIS;
82Select *pSelectAIS;
83bool g_bUseOnlyConfirmedAISName;
84wxString GetShipNameFromFile(int);
85wxString AISTargetNameFileName;
86
87unsigned g_OwnShipmmsi;
88
89wxDEFINE_EVENT(EVT_N0183_VDO, ObservedEvt);
90wxDEFINE_EVENT(EVT_N0183_VDM, ObservedEvt);
91wxDEFINE_EVENT(EVT_N0183_FRPOS, ObservedEvt);
92wxDEFINE_EVENT(EVT_N0183_CDDSC, ObservedEvt);
93wxDEFINE_EVENT(EVT_N0183_CDDSE, ObservedEvt);
94wxDEFINE_EVENT(EVT_N0183_TLL, ObservedEvt);
95wxDEFINE_EVENT(EVT_N0183_TTM, ObservedEvt);
96wxDEFINE_EVENT(EVT_N0183_OSD, ObservedEvt);
97wxDEFINE_EVENT(EVT_N0183_WPL, ObservedEvt);
98wxDEFINE_EVENT(EVT_SIGNALK, ObservedEvt);
99wxDEFINE_EVENT(EVT_N2K_129038, ObservedEvt);
100wxDEFINE_EVENT(EVT_N2K_129039, ObservedEvt);
101wxDEFINE_EVENT(EVT_N2K_129041, ObservedEvt);
102wxDEFINE_EVENT(EVT_N2K_129794, ObservedEvt);
103wxDEFINE_EVENT(EVT_N2K_129809, ObservedEvt);
104wxDEFINE_EVENT(EVT_N2K_129810, ObservedEvt);
105wxDEFINE_EVENT(EVT_N2K_129793, ObservedEvt);
106
107BEGIN_EVENT_TABLE(AisDecoder, wxEvtHandler)
108EVT_TIMER(TIMER_AIS1, AisDecoder::OnTimerAIS)
109EVT_TIMER(TIMER_DSC, AisDecoder::OnTimerDSC)
110END_EVENT_TABLE()
111
112static constexpr double ms_to_knot_factor = 1.9438444924406;
113
114static int n_msgs;
115static int n_msg1;
116static int n_msg5;
117static int n_msg24;
118static bool b_firstrx;
119static int first_rx_ticks;
120static int rx_ticks;
121static double arpa_ref_hdg = NAN;
122
123static inline double GeodesicRadToDeg(double rads) {
124 return rads * 180.0 / M_PI;
125}
126
127static inline double MS2KNOTS(double ms) { return ms * 1.9438444924406; }
128
129static bool isBuoyMmsi(unsigned msi) {
130 // IMO standard is not yet(?) implemented for (net)buoys
131 // This adaption, based on real-world outcomes, is used instead
132 // Consider any not valid MMSI number for a class B target (message 18 or 19)
133 // to be a "net buoy"
134 int mid = msi / 1000000;
135 if ((mid > 200 && mid < 880) || mid >= 970) {
136 // MID is stated to be >200 <800. Handheld AIS starts with 8+mid thus 880
137 return false;
138 } else {
139 return true;
140 }
141 return false;
142}
143
144int AisMeteoNewMmsi(int, int, int, int, int);
145int origin_mmsi = 0;
146
147void AISshipNameCache(AisTargetData *pTargetData,
148 AIS_Target_Name_Hash *AISTargetNamesC,
149 AIS_Target_Name_Hash *AISTargetNamesNC, long mmsi);
150static void BuildERIShipTypeHash() {
151 make_hash_ERI(8000, _("Vessel, type unknown"));
152 make_hash_ERI(8150, _("Freightbarge"));
153 make_hash_ERI(8160, _("Tankbarge"));
154 make_hash_ERI(8163, _("Tankbarge, dry cargo as if liquid (e.g. cement)"));
155 make_hash_ERI(8450, _("Service vessel, police patrol, port service"));
156 make_hash_ERI(8430, _("Pushboat, single"));
157 make_hash_ERI(8510, _("Object, not otherwise specified"));
158 make_hash_ERI(8470, _("Object, towed, not otherwise specified"));
159 make_hash_ERI(8490, _("Bunkership"));
160 make_hash_ERI(8010, _("Motor freighter"));
161 make_hash_ERI(8020, _("Motor tanker"));
162 make_hash_ERI(8021, _("Motor tanker, liquid cargo, type N"));
163 make_hash_ERI(8022, _("Motor tanker, liquid cargo, type C"));
164 make_hash_ERI(8023, _("Motor tanker, dry cargo as if liquid (e.g. cement)"));
165 make_hash_ERI(8030, _("Container vessel"));
166 make_hash_ERI(8040, _("Gas tanker"));
167 make_hash_ERI(8050, _("Motor freighter, tug"));
168 make_hash_ERI(8060, _("Motor tanker, tug"));
169 make_hash_ERI(8070, _("Motor freighter with one or more ships alongside"));
170 make_hash_ERI(8080, _("Motor freighter with tanker"));
171 make_hash_ERI(8090, _("Motor freighter pushing one or more freighters"));
172 make_hash_ERI(8100, _("Motor freighter pushing at least one tank-ship"));
173 make_hash_ERI(8110, _("Tug, freighter"));
174 make_hash_ERI(8120, _("Tug, tanker"));
175 make_hash_ERI(8130, _("Tug freighter, coupled"));
176 make_hash_ERI(8140, _("Tug, freighter/tanker, coupled"));
177 make_hash_ERI(8161, _("Tankbarge, liquid cargo, type N"));
178 make_hash_ERI(8162, _("Tankbarge, liquid cargo, type C"));
179 make_hash_ERI(8170, _("Freightbarge with containers"));
180 make_hash_ERI(8180, _("Tankbarge, gas"));
181 make_hash_ERI(8210, _("Pushtow, one cargo barge"));
182 make_hash_ERI(8220, _("Pushtow, two cargo barges"));
183 make_hash_ERI(8230, _("Pushtow, three cargo barges"));
184 make_hash_ERI(8240, _("Pushtow, four cargo barges"));
185 make_hash_ERI(8250, _("Pushtow, five cargo barges"));
186 make_hash_ERI(8260, _("Pushtow, six cargo barges"));
187 make_hash_ERI(8270, _("Pushtow, seven cargo barges"));
188 make_hash_ERI(8280, _("Pushtow, eight cargo barges"));
189 make_hash_ERI(8290, _("Pushtow, nine or more barges"));
190 make_hash_ERI(8310, _("Pushtow, one tank/gas barge"));
191 make_hash_ERI(8320,
192 _("Pushtow, two barges at least one tanker or gas barge"));
193 make_hash_ERI(8330,
194 _("Pushtow, three barges at least one tanker or gas barge"));
195 make_hash_ERI(8340,
196 _("Pushtow, four barges at least one tanker or gas barge"));
197 make_hash_ERI(8350,
198 _("Pushtow, five barges at least one tanker or gas barge"));
199 make_hash_ERI(8360,
200 _("Pushtow, six barges at least one tanker or gas barge"));
201 make_hash_ERI(8370,
202 _("Pushtow, seven barges at least one tanker or gas barge"));
203 make_hash_ERI(8380,
204 _("Pushtow, eight barges at least one tanker or gas barge"));
205 make_hash_ERI(
206 8390, _("Pushtow, nine or more barges at least one tanker or gas barge"));
207 make_hash_ERI(8400, _("Tug, single"));
208 make_hash_ERI(8410, _("Tug, one or more tows"));
209 make_hash_ERI(8420, _("Tug, assisting a vessel or linked combination"));
210 make_hash_ERI(8430, _("Pushboat, single"));
211 make_hash_ERI(8440, _("Passenger ship, ferry, cruise ship, red cross ship"));
212 make_hash_ERI(8441, _("Ferry"));
213 make_hash_ERI(8442, _("Red cross ship"));
214 make_hash_ERI(8443, _("Cruise ship"));
215 make_hash_ERI(8444, _("Passenger ship without accommodation"));
216 make_hash_ERI(8460, _("Vessel, work maintenance craft, floating derrick, "
217 "cable-ship, buoy-ship, dredge"));
218 make_hash_ERI(8480, _("Fishing boat"));
219 make_hash_ERI(8500, _("Barge, tanker, chemical"));
220 make_hash_ERI(1500, _("General cargo Vessel maritime"));
221 make_hash_ERI(1510, _("Unit carrier maritime"));
222 make_hash_ERI(1520, _("Bulk carrier maritime"));
223 make_hash_ERI(1530, _("Tanker"));
224 make_hash_ERI(1540, _("Liquified gas tanker"));
225 make_hash_ERI(1850, _("Pleasure craft, longer than 20 metres"));
226 make_hash_ERI(1900, _("Fast ship"));
227 make_hash_ERI(1910, _("Hydrofoil"));
228}
229
230// DSE Expansion characters, decode table from ITU-R M.825
231static wxString DecodeDSEExpansionCharacters(const wxString &dseData) {
232 wxString result;
233 char lookupTable[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ',
234 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
235 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
236 'W', 'X', 'Y', 'Z', '.', ',', '-', '/', ' '};
237
238 for (size_t i = 0; i < dseData.length(); i += 2) {
239 result.append(1,
240 lookupTable[strtol(dseData.Mid(i, 2).data(), nullptr, 10)]);
241 }
242 return result;
243}
244
245static void getMmsiProperties(std::shared_ptr<AisTargetData> &pTargetData) {
246 for (unsigned int i = 0; i < g_MMSI_Props_Array.GetCount(); i++) {
247 if (pTargetData->MMSI == g_MMSI_Props_Array[i]->MMSI) {
248 MmsiProperties *props = g_MMSI_Props_Array[i];
249 pTargetData->b_isFollower = props->m_bFollower;
250 pTargetData->b_mPropPersistTrack = props->m_bPersistentTrack;
251 if (TRACKTYPE_NEVER == props->TrackType) {
252 pTargetData->b_show_track = false;
253 } else if (TRACKTYPE_ALWAYS == props->TrackType) {
254 pTargetData->b_show_track = true;
255 }
256 break;
257 }
258 }
259}
260
261//----------------------------------------------------------------------------
262// Parse a NMEA VDM/VDO Bitstring
263//----------------------------------------------------------------------------
264static bool Parse_VDXBitstring(AisBitstring *bstr,
265 const std::shared_ptr<AisTargetData> &ptd) {
266 bool parse_result = false;
267 bool b_posn_report = false;
268
269 wxDateTime now = wxDateTime::Now();
270 now.MakeGMT();
271 int message_ID = bstr->GetInt(1, 6); // Parse on message ID
272 ptd->MID = message_ID;
273
274 // Save for Ais8_001_31 and ais8_367_33 (class AIS_METEO)
275 int met_mmsi = ptd->MMSI;
276
277 // MMSI is always in the same spot in the bitstream
278 ptd->MMSI = bstr->GetInt(9, 30);
279
280 switch (message_ID) {
281 case 1: // Position Report
282 case 2:
283 case 3: {
284 n_msg1++;
285
286 ptd->NavStatus = bstr->GetInt(39, 4);
287 ptd->SOG = 0.1 * (bstr->GetInt(51, 10));
288
289 int lon = bstr->GetInt(62, 28);
290 if (lon & 0x08000000) // negative?
291 lon |= 0xf0000000;
292 double lon_tentative = lon / 600000.;
293
294 int lat = bstr->GetInt(90, 27);
295 if (lat & 0x04000000) // negative?
296 lat |= 0xf8000000;
297 double lat_tentative = lat / 600000.;
298
299 if ((lon_tentative <= 180.) &&
300 (lat_tentative <=
301 90.)) // Ship does not report Lat or Lon "unavailable"
302 {
303 ptd->Lon = lon_tentative;
304 ptd->Lat = lat_tentative;
305 ptd->b_positionDoubtful = false;
306 ptd->b_positionOnceValid = true; // Got the position at least once
307 ptd->LastPositionReportTicks = ptd->PositionReportTicks;
308 ptd->PositionReportTicks = now.GetTicks();
309 } else
310 ptd->b_positionDoubtful = true;
311
312 // decode balance of message....
313 ptd->COG = 0.1 * (bstr->GetInt(117, 12));
314 ptd->HDG = 1.0 * (bstr->GetInt(129, 9));
315
316 ptd->ROTAIS = bstr->GetInt(43, 8);
317 double rot_dir = 1.0;
318
319 if (ptd->ROTAIS == 128)
320 ptd->ROTAIS = -128; // not available codes as -128
321 else if ((ptd->ROTAIS & 0x80) == 0x80) {
322 ptd->ROTAIS = ptd->ROTAIS - 256; // convert to twos complement
323 rot_dir = -1.0;
324 }
325
326 ptd->ROTIND = wxRound(rot_dir * pow((((double)ptd->ROTAIS) / 4.733),
327 2)); // Convert to indicated ROT
328
329 ptd->m_utc_sec = bstr->GetInt(138, 6);
330
331 if ((1 == message_ID) ||
332 (2 == message_ID)) // decode SOTDMA per 7.6.7.2.2
333 {
334 ptd->SyncState = bstr->GetInt(151, 2);
335 ptd->SlotTO = bstr->GetInt(153, 2);
336 if ((ptd->SlotTO == 1) && (ptd->SyncState == 0)) // UTCDirect follows
337 {
338 ptd->m_utc_hour = bstr->GetInt(155, 5);
339
340 ptd->m_utc_min = bstr->GetInt(160, 7);
341
342 if ((ptd->m_utc_hour < 24) && (ptd->m_utc_min < 60) &&
343 (ptd->m_utc_sec < 60)) {
344 wxDateTime rx_time(ptd->m_utc_hour, ptd->m_utc_min, ptd->m_utc_sec);
345 rx_ticks = rx_time.GetTicks();
346 if (!b_firstrx) {
347 first_rx_ticks = rx_ticks;
348 b_firstrx = true;
349 }
350 }
351 }
352 }
353
354 // Capture Euro Inland special passing arrangement signal ("stbd-stbd")
355 ptd->blue_paddle = bstr->GetInt(144, 2);
356 ptd->b_blue_paddle = (ptd->blue_paddle == 2); // paddle is set
357
358 if (!ptd->b_isDSCtarget) ptd->Class = AIS_CLASS_A;
359
360 // Check for SART and friends by looking at first two digits of MMSI
361 int mmsi_start = ptd->MMSI / 10000000;
362
363 if (mmsi_start == 97) {
364 ptd->Class = AIS_SART;
365 ptd->StaticReportTicks =
366 now.GetTicks(); // won't get a static report, so fake it here
367
368 // On receipt of Msg 3, force any existing SART target out of
369 // acknowledge mode by adjusting its ack_time to yesterday This will
370 // cause any previously "Acknowledged" SART to re-alert.
371
372 // On reflection, re-alerting seems a little excessive in real life
373 // use. After all, the target is on-screen, and in the AIS target
374 // list. So lets just honor the programmed ACK timout value for SART
375 // targets as well
376 // ptd->m_ack_time = wxDateTime::Now() - wxTimeSpan::Day();
377 }
378
379 parse_result = true; // so far so good
380 b_posn_report = true;
381
382 break;
383 }
384
385 case 18: {
386 ptd->NavStatus =
387 UNDEFINED; // Class B targets have no status. Enforce this...
388
389 ptd->SOG = 0.1 * (bstr->GetInt(47, 10));
390
391 int lon = bstr->GetInt(58, 28);
392 if (lon & 0x08000000) // negative?
393 lon |= 0xf0000000;
394 double lon_tentative = lon / 600000.;
395
396 int lat = bstr->GetInt(86, 27);
397 if (lat & 0x04000000) // negative?
398 lat |= 0xf8000000;
399 double lat_tentative = lat / 600000.;
400
401 if ((lon_tentative <= 180.) &&
402 (lat_tentative <=
403 90.)) // Ship does not report Lat or Lon "unavailable"
404 {
405 ptd->Lon = lon_tentative;
406 ptd->Lat = lat_tentative;
407 ptd->b_positionDoubtful = false;
408 ptd->b_positionOnceValid = true; // Got the position at least once
409 ptd->LastPositionReportTicks = ptd->PositionReportTicks;
410 ptd->PositionReportTicks = now.GetTicks();
411 } else
412 ptd->b_positionDoubtful = true;
413
414 ptd->COG = 0.1 * (bstr->GetInt(113, 12));
415 ptd->HDG = 1.0 * (bstr->GetInt(125, 9));
416
417 ptd->m_utc_sec = bstr->GetInt(134, 6);
418
419 if (!ptd->b_isDSCtarget) {
420 if (!isBuoyMmsi(ptd->MMSI))
421 ptd->Class = AIS_CLASS_B;
422 else
423 ptd->Class = AIS_BUOY;
424 }
425 parse_result = true; // so far so good
426 b_posn_report = true;
427
428 break;
429 }
430
431 case 19: { // Class B mes_ID 19 Is same as mes_ID 18 until bit 139
432 ptd->NavStatus =
433 UNDEFINED; // Class B targets have no status. Enforce this...
434 ptd->SOG = 0.1 * (bstr->GetInt(47, 10));
435 int lon = bstr->GetInt(58, 28);
436 if (lon & 0x08000000) // negative?
437 lon |= 0xf0000000;
438 double lon_tentative = lon / 600000.;
439
440 int lat = bstr->GetInt(86, 27);
441 if (lat & 0x04000000) // negative?
442 lat |= 0xf8000000;
443 double lat_tentative = lat / 600000.;
444
445 if ((lon_tentative <= 180.) &&
446 (lat_tentative <=
447 90.)) // Ship does not report Lat or Lon "unavailable"
448 {
449 ptd->Lon = lon_tentative;
450 ptd->Lat = lat_tentative;
451 ptd->b_positionDoubtful = false;
452 ptd->b_positionOnceValid = true; // Got the position at least once
453 ptd->LastPositionReportTicks = ptd->PositionReportTicks;
454 ptd->PositionReportTicks = now.GetTicks();
455 } else
456 ptd->b_positionDoubtful = true;
457
458 ptd->COG = 0.1 * (bstr->GetInt(113, 12));
459 ptd->HDG = 1.0 * (bstr->GetInt(125, 9));
460 ptd->m_utc_sec = bstr->GetInt(134, 6);
461 // From bit 140 and forward data as of mes 5
462 bstr->GetStr(144, 120, &ptd->ShipName[0], SHIP_NAME_LEN);
463 ptd->b_nameValid = true;
464 if (!ptd->b_isDSCtarget) {
465 ptd->ShipType = (unsigned char)bstr->GetInt(264, 8);
466 }
467 ptd->DimA = bstr->GetInt(272, 9);
468 ptd->DimB = bstr->GetInt(281, 9);
469 ptd->DimC = bstr->GetInt(290, 6);
470 ptd->DimD = bstr->GetInt(296, 6);
471
472 if (!ptd->b_isDSCtarget) {
473 // Although outdated, message 19 is used by many "ATON" for net buoys
474 if (!isBuoyMmsi(ptd->MMSI))
475 ptd->Class = AIS_CLASS_B;
476 else
477 ptd->Class = AIS_BUOY;
478 }
479 parse_result = true; // so far so good
480 b_posn_report = true;
481
482 break;
483 }
484
485 case 27: {
486 // Long-range automatic identification system broadcast message
487 // This message is used for long-range detection of AIS Class A and Class
488 // B vessels (typically by satellite).
489
490 // Define the constant to do the covertion from the internal encoded
491 // position in message 27. The position is less accuate : 1/10 minute
492 // position resolution.
493 int bitCorrection = 10;
494 int resolution = 10;
495
496 // Default aout of bounce values.
497 double lon_tentative = 181.;
498 double lat_tentative = 91.;
499
500#ifdef AIS_DEBUG
501 printf("AIS Message 27 - received:\r\n");
502 printf("MMSI : %i\r\n", ptd->MMSI);
503#endif
504
505 // It can be both a CLASS A and a CLASS B vessel - We have decided for
506 // CLASS A
507 // TODO: Lookup to see if we have seen it as a CLASS B, and adjust.
508 if (!ptd->b_isDSCtarget) ptd->Class = AIS_CLASS_A;
509
510 ptd->NavStatus = bstr->GetInt(39, 4);
511
512 int lon = bstr->GetInt(45, 18);
513 int lat = bstr->GetInt(63, 17);
514
515 lat_tentative = lat;
516 lon_tentative = lon;
517
518 // Negative latitude?
519 if (lat >= (0x4000000 >> bitCorrection)) {
520 lat_tentative = (0x8000000 >> bitCorrection) - lat;
521 lat_tentative *= -1;
522 }
523
524 // Negative longitude?
525 if (lon >= (0x8000000 >> bitCorrection)) {
526 lon_tentative = (0x10000000 >> bitCorrection) - lon;
527 lon_tentative *= -1;
528 }
529
530 // Decode the internal position format.
531 lat_tentative = lat_tentative / resolution / 60.0;
532 lon_tentative = lon_tentative / resolution / 60.0;
533
534#ifdef AIS_DEBUG
535 printf("Latitude : %f\r\n", lat_tentative);
536 printf("Longitude : %f\r\n", lon_tentative);
537#endif
538
539 // Get the latency of the position report.
540 int positionLatency = bstr->GetInt(95, 1);
541
542 if ((lon_tentative <= 180.) &&
543 (lat_tentative <=
544 90.)) // Ship does not report Lat or Lon "unavailable"
545 {
546 ptd->Lon = lon_tentative;
547 ptd->Lat = lat_tentative;
548 ptd->b_positionDoubtful = false;
549 ptd->b_positionOnceValid = true; // Got the position at least once
550 if (positionLatency == 0) {
551// The position is less than 5 seconds old.
552#ifdef AIS_DEBUG
553 printf("Low latency position report.\r\n");
554#endif
555 ptd->LastPositionReportTicks = ptd->PositionReportTicks;
556 ptd->PositionReportTicks = now.GetTicks();
557 }
558 } else
559 ptd->b_positionDoubtful = true;
560
561 ptd->SOG = 1.0 * (bstr->GetInt(80, 6));
562 ptd->COG = 1.0 * (bstr->GetInt(85, 9));
563
564 b_posn_report = true;
565 parse_result = true;
566 break;
567 }
568
569 case 5: {
570 n_msg5++;
571 if (!ptd->b_isDSCtarget) ptd->Class = AIS_CLASS_A;
572
573 // Get the AIS Version indicator
574 // 0 = station compliant with Recommendation ITU-R M.1371-1
575 // 1 = station compliant with Recommendation ITU-R M.1371-3
576 // 2-3 = station compliant with future editions
577 int AIS_version_indicator = bstr->GetInt(39, 2);
578 if (AIS_version_indicator < 4) {
579 ptd->IMO = bstr->GetInt(41, 30);
580
581 bstr->GetStr(71, 42, &ptd->CallSign[0], 7);
582 bstr->GetStr(113, 120, &ptd->ShipName[0], SHIP_NAME_LEN);
583 ptd->b_nameValid = true;
584 if (!ptd->b_isDSCtarget) {
585 ptd->ShipType = (unsigned char)bstr->GetInt(233, 8);
586 }
587
588 ptd->DimA = bstr->GetInt(241, 9);
589 ptd->DimB = bstr->GetInt(250, 9);
590 ptd->DimC = bstr->GetInt(259, 6);
591 ptd->DimD = bstr->GetInt(265, 6);
592
593 ptd->ETA_Mo = bstr->GetInt(275, 4);
594 ptd->ETA_Day = bstr->GetInt(279, 5);
595 ptd->ETA_Hr = bstr->GetInt(284, 5);
596 ptd->ETA_Min = bstr->GetInt(289, 6);
597
598 ptd->Draft = (double)(bstr->GetInt(295, 8)) / 10.0;
599
600 bstr->GetStr(303, 120, &ptd->Destination[0], DESTINATION_LEN - 1);
601
602 ptd->StaticReportTicks = now.GetTicks();
603
604 parse_result = true;
605 }
606
607 break;
608 }
609
610 case 24: { // Static data report
611 int part_number = bstr->GetInt(39, 2);
612 if (0 == part_number) {
613 bstr->GetStr(41, 120, &ptd->ShipName[0], SHIP_NAME_LEN);
614 ptd->b_nameValid = true;
615 parse_result = true;
616 n_msg24++;
617 } else if (1 == part_number) {
618 if (!ptd->b_isDSCtarget) {
619 ptd->ShipType = (unsigned char)bstr->GetInt(41, 8);
620 }
621 bstr->GetStr(91, 42, &ptd->CallSign[0], 7);
622
623 ptd->DimA = bstr->GetInt(133, 9);
624 ptd->DimB = bstr->GetInt(142, 9);
625 ptd->DimC = bstr->GetInt(151, 6);
626 ptd->DimD = bstr->GetInt(157, 6);
627 parse_result = true;
628 }
629 break;
630 }
631 case 4: // base station
632 {
633 ptd->Class = AIS_BASE;
634
635 ptd->m_utc_hour = bstr->GetInt(62, 5);
636 ptd->m_utc_min = bstr->GetInt(67, 6);
637 ptd->m_utc_sec = bstr->GetInt(73, 6);
638 // (79, 1);
639 int lon = bstr->GetInt(80, 28);
640 if (lon & 0x08000000) // negative?
641 lon |= 0xf0000000;
642 double lon_tentative = lon / 600000.;
643
644 int lat = bstr->GetInt(108, 27);
645 if (lat & 0x04000000) // negative?
646 lat |= 0xf8000000;
647 double lat_tentative = lat / 600000.;
648
649 if ((lon_tentative <= 180.) &&
650 (lat_tentative <=
651 90.)) // Ship does not report Lat or Lon "unavailable"
652 {
653 ptd->Lon = lon_tentative;
654 ptd->Lat = lat_tentative;
655 ptd->b_positionDoubtful = false;
656 ptd->b_positionOnceValid = true; // Got the position at least once
657 ptd->LastPositionReportTicks = ptd->PositionReportTicks;
658 ptd->PositionReportTicks = now.GetTicks();
659 } else
660 ptd->b_positionDoubtful = true;
661
662 ptd->COG = -1.;
663 ptd->HDG = 511;
664 ptd->SOG = -1.;
665
666 parse_result = true;
667 b_posn_report = true;
668
669 break;
670 }
671 case 9: // Special Position Report (Standard SAR Aircraft Position Report)
672 {
673 ptd->SOG = bstr->GetInt(51, 10);
674
675 int lon = bstr->GetInt(62, 28);
676 if (lon & 0x08000000) // negative?
677 lon |= 0xf0000000;
678 double lon_tentative = lon / 600000.;
679
680 int lat = bstr->GetInt(90, 27);
681 if (lat & 0x04000000) // negative?
682 lat |= 0xf8000000;
683 double lat_tentative = lat / 600000.;
684
685 if ((lon_tentative <= 180.) &&
686 (lat_tentative <=
687 90.)) // Ship does not report Lat or Lon "unavailable"
688 {
689 ptd->Lon = lon_tentative;
690 ptd->Lat = lat_tentative;
691 ptd->b_positionDoubtful = false;
692 ptd->b_positionOnceValid = true; // Got the position at least once
693 ptd->LastPositionReportTicks = ptd->PositionReportTicks;
694 ptd->PositionReportTicks = now.GetTicks();
695 } else
696 ptd->b_positionDoubtful = true;
697
698 // decode balance of message....
699 ptd->COG = 0.1 * (bstr->GetInt(117, 12));
700
701 int alt_tent = bstr->GetInt(39, 12);
702 ptd->altitude = alt_tent;
703
704 ptd->b_SarAircraftPosnReport = true;
705
706 parse_result = true;
707 b_posn_report = true;
708
709 break;
710 }
711 case 21: // Test Message (Aid to Navigation)
712 {
713 ptd->ShipType = (unsigned char)bstr->GetInt(39, 5);
714 ptd->IMO = 0;
715 ptd->SOG = 0;
716 ptd->HDG = 0;
717 ptd->COG = 0;
718 ptd->ROTAIS = -128; // i.e. not available
719 ptd->DimA = bstr->GetInt(220, 9);
720 ptd->DimB = bstr->GetInt(229, 9);
721 ptd->DimC = bstr->GetInt(238, 6);
722 ptd->DimD = bstr->GetInt(244, 6);
723 ptd->Draft = 0;
724
725 ptd->m_utc_sec = bstr->GetInt(254, 6);
726
727 int offpos = bstr->GetInt(260, 1); // off position flag
728 int virt = bstr->GetInt(270, 1); // virtual flag
729
730 if (virt)
731 ptd->NavStatus = ATON_VIRTUAL;
732 else
733 ptd->NavStatus = ATON_REAL;
734 if (ptd->m_utc_sec <= 59 /*&& !virt*/) {
735 ptd->NavStatus += 1;
736 if (offpos) ptd->NavStatus += 1;
737 }
738
739 bstr->GetStr(44, 120, &ptd->ShipName[0], SHIP_NAME_LEN);
740 // short name only, extension wont fit in Ship structure
741
742 if (bstr->GetBitCount() > 276) {
743 int nx = ((bstr->GetBitCount() - 272) / 6) * 6;
744 bstr->GetStr(273, nx, &ptd->ShipNameExtension[0], 14);
745 ptd->ShipNameExtension[14] = 0;
746 } else {
747 ptd->ShipNameExtension[0] = 0;
748 }
749
750 ptd->b_nameValid = true;
751
752 parse_result = true; // so far so good
753
754 ptd->Class = AIS_ATON;
755
756 int lon = bstr->GetInt(165, 28);
757
758 if (lon & 0x08000000) // negative?
759 lon |= 0xf0000000;
760 double lon_tentative = lon / 600000.;
761
762 int lat = bstr->GetInt(193, 27);
763
764 if (lat & 0x04000000) // negative?
765 lat |= 0xf8000000;
766 double lat_tentative = lat / 600000.;
767
768 if ((lon_tentative <= 180.) &&
769 (lat_tentative <=
770 90.)) // Ship does not report Lat or Lon "unavailable"
771 {
772 ptd->Lon = lon_tentative;
773 ptd->Lat = lat_tentative;
774 ptd->b_positionDoubtful = false;
775 ptd->b_positionOnceValid = true; // Got the position at least once
776 ptd->LastPositionReportTicks = ptd->PositionReportTicks;
777 ptd->PositionReportTicks = now.GetTicks();
778 } else
779 ptd->b_positionDoubtful = true;
780
781 b_posn_report = true;
782 break;
783 }
784 case 8: // Binary Broadcast
785 {
786 int dac = bstr->GetInt(41, 10);
787 int fi = bstr->GetInt(51, 6);
788
789 if (dac == 200) // European inland
790 {
791 if (fi == 10) // "Inland ship static and voyage related data"
792 {
793 ptd->b_isEuroInland = true;
794
795 bstr->GetStr(57, 48, &ptd->Euro_VIN[0], 8);
796 ptd->Euro_Length = ((double)bstr->GetInt(105, 13)) / 10.0;
797 ptd->Euro_Beam = ((double)bstr->GetInt(118, 10)) / 10.0;
798 ptd->UN_shiptype = bstr->GetInt(128, 14);
799 ptd->Euro_Draft = ((double)bstr->GetInt(145, 11)) / 100.0;
800 parse_result = true;
801 }
802 }
803 if (dac == 1 || dac == 366) // IMO or US
804 {
805 if (fi == 22) // Area Notice
806 {
807 if (bstr->GetBitCount() >= 111) {
808 Ais8_001_22 an;
809 an.link_id = bstr->GetInt(57, 10);
810 an.notice_type = bstr->GetInt(67, 7);
811 an.month = bstr->GetInt(74, 4);
812 an.day = bstr->GetInt(78, 5);
813 an.hour = bstr->GetInt(83, 5);
814 an.minute = bstr->GetInt(88, 6);
815 an.duration_minutes = bstr->GetInt(94, 18);
816
817 wxDateTime now_ = wxDateTime::Now();
818 now_.MakeGMT();
819
820 an.start_time.Set(an.day, wxDateTime::Month(an.month - 1),
821 now_.GetYear(), an.hour, an.minute);
822
823 // msg is not supposed to be transmitted more than a day before it
824 // comes into effect, so a start_time less than a day or two away
825 // might indicate a month rollover
826 if (an.start_time > now_ + wxTimeSpan::Hours(48))
827 an.start_time.Set(an.day, wxDateTime::Month(an.month - 1),
828 now_.GetYear() - 1, an.hour, an.minute);
829
830 an.expiry_time =
831 an.start_time + wxTimeSpan::Minutes(an.duration_minutes);
832
833 // msg is not supposed to be transmitted beyond expiration, so
834 // taking into account a fudge factor for clock issues, assume an
835 // expiry date in the past indicates incorrect year
836 if (an.expiry_time < now_ - wxTimeSpan::Hours(24)) {
837 an.start_time.Set(an.day, wxDateTime::Month(an.month - 1),
838 now_.GetYear() + 1, an.hour, an.minute);
839 an.expiry_time =
840 an.start_time + wxTimeSpan::Minutes(an.duration_minutes);
841 }
842
843 // Default case, the IMO format
844 // https://www.e-navigation.nl/content/area-notice-0
845 int subarea_len = 87;
846 int lon_len = 25;
847 int lat_len = 24;
848 float pos_scale = 60000.0;
849 int prec_size = 3;
850 if (dac ==
851 366) { // Deprecated US format
852 // https://www.e-navigation.nl/content/area-notice-1
853 subarea_len = 90;
854 lon_len = 28;
855 lat_len = 27;
856 pos_scale = 600000.0;
857 prec_size = 0; // Not present in the in US format between
858 // coordinates and radius for some shapes
859 }
860
861 int subarea_count = (bstr->GetBitCount() - 111) / subarea_len;
862 for (int i = 0; i < subarea_count; ++i) {
863 int base = 111 + i * subarea_len;
865 sa.shape = bstr->GetInt(base + 1, 3);
866 int scale_factor = 1;
867 if (sa.shape == AIS8_001_22_SHAPE_TEXT) {
868 char t[15];
869 t[14] = 0;
870 bstr->GetStr(base + 4, subarea_len - 3, t, 14);
871 sa.text = wxString(t, wxConvUTF8);
872 } else {
873 int scale_multipliers[4] = {1, 10, 100, 1000};
874 scale_factor = scale_multipliers[bstr->GetInt(base + 4, 2)];
875 switch (sa.shape) {
876 case AIS8_001_22_SHAPE_SECTOR:
877 sa.left_bound_deg = bstr->GetInt(
878 base + 6 + lon_len + lat_len + prec_size + 12, 9);
879 sa.right_bound_deg = bstr->GetInt(
880 base + 6 + lon_len + lat_len + prec_size + 12 + 9, 9);
881 case AIS8_001_22_SHAPE_CIRCLE:
882 sa.radius_m =
883 bstr->GetInt(base + 6 + lon_len + lat_len + 3, 12) *
884 scale_factor;
885 // FALL THROUGH
886 case AIS8_001_22_SHAPE_RECT:
887 sa.longitude =
888 bstr->GetInt(base + 6, lon_len, true) / pos_scale;
889 sa.latitude =
890 bstr->GetInt(base + 6 + lon_len, lat_len, true) /
891 pos_scale;
892 sa.e_dim_m =
893 bstr->GetInt(base + 6 + lon_len + lat_len + prec_size,
894 8) *
895 scale_factor;
896 sa.n_dim_m =
897 bstr->GetInt(
898 base + 6 + lon_len + lat_len + prec_size + 8, 8) *
899 scale_factor;
900 sa.orient_deg = bstr->GetInt(
901 base + 6 + lon_len + lat_len + prec_size + 8 + 8, 9);
902 break;
903 case AIS8_001_22_SHAPE_POLYLINE:
904 case AIS8_001_22_SHAPE_POLYGON:
905 for (int j = 0; j < 4; ++j) {
906 sa.angles[j] = bstr->GetInt(base + 6 + j * 20, 10) * 0.5;
907 sa.dists_m[j] =
908 bstr->GetInt(base + 16 + j * 20, 10) * scale_factor;
909 }
910 }
911 }
912 an.sub_areas.push_back(sa);
913 }
914 ptd->area_notices[an.link_id] = an;
915 parse_result = true;
916 }
917 }
918
919 // Meteorological and Hydrographic data ref: IMO SN.1/Circ.289
920 if (fi == 31) {
921 if (bstr->GetBitCount() >= 360) {
922 // Ais8_001_31 mmsi can have been changed.
923 if (met_mmsi != 666) ptd->MMSI = met_mmsi;
924
925 // Default out of bounce values.
926 double lon_tentative = 181.;
927 double lat_tentative = 91.;
928
929 int lon = bstr->GetInt(57, 25);
930 int lat = bstr->GetInt(82, 24);
931
932 if (lon & 0x01000000) // negative?
933 lon |= 0xFE000000;
934 lon_tentative = lon / 60000.;
935
936 if (lat & 0x00800000) // negative?
937 lat |= 0xFF000000;
938 lat_tentative = lat / 60000.;
939
940 ptd->Lon = lon_tentative;
941 ptd->Lat = lat_tentative;
942
943 // Try to make unique name for each station based on position
944 wxString x = ptd->ShipName;
945 if (x.Find("METEO") == wxNOT_FOUND) {
946 double id1, id2;
947 wxString slat = wxString::Format("%0.3f", lat_tentative);
948 wxString slon = wxString::Format("%0.3f", lon_tentative);
949 slat.ToDouble(&id1);
950 slon.ToDouble(&id2);
951 wxString nameID = "METEO ";
952 nameID << wxString::Format("%0.3f", abs(id1) + abs(id2)).Right(3);
953 strncpy(ptd->ShipName, nameID, SHIP_NAME_LEN - 1);
954 }
955
956 ptd->met_data.pos_acc = bstr->GetInt(106, 1);
957 ptd->met_data.day = bstr->GetInt(107, 5);
958 ptd->met_data.hour = bstr->GetInt(112, 5);
959 ptd->met_data.minute = bstr->GetInt(117, 6);
960 ptd->met_data.wind_kn = bstr->GetInt(123, 7);
961 ptd->met_data.wind_gust_kn = bstr->GetInt(130, 7);
962 ptd->met_data.wind_dir = bstr->GetInt(137, 9);
963 ptd->met_data.wind_gust_dir = bstr->GetInt(146, 9);
964
965 int tmp = bstr->GetInt(155, 11);
966 if (tmp & 0x00000400) // negative?
967 tmp |= 0xFFFFF800;
968 ptd->met_data.air_temp = tmp / 10.;
969 ptd->met_data.rel_humid = bstr->GetInt(166, 7);
970 int dew = bstr->GetInt(173, 10);
971 if (dew & 0x00000200) // negative? (bit 9 = 1)
972 dew |= 0xFFFFFC00;
973 ptd->met_data.dew_point = dew / 10.;
974
975 /*Air pressure, defined as pressure reduced to sea level,
976 in 1 hPa steps.0 = pressure 799 hPa or less
977 1 - 401 = 800 - 1200 hPa*/
978 ptd->met_data.airpress = bstr->GetInt(183, 9) + 799;
979 ptd->met_data.airpress_tend = bstr->GetInt(192, 2);
980
981 int horVis = bstr->GetInt(194, 8);
982 if (horVis & 0x80u) { // if MSB = 1
983 horVis &= 0x7F; // We print >x.x
984 ptd->met_data.hor_vis_GT = true;
985 } else
986 ptd->met_data.hor_vis_GT = false;
987
988 ptd->met_data.hor_vis = horVis / 10.0;
989
990 ptd->met_data.water_lev_dev = (bstr->GetInt(202, 12) / 100.) - 10.;
991 ptd->met_data.water_lev_trend = bstr->GetInt(214, 2);
992 ptd->met_data.current = bstr->GetInt(216, 8) / 10.;
993 ptd->met_data.curr_dir = bstr->GetInt(224, 9);
994 ptd->met_data.wave_height = bstr->GetInt(277, 8) / 10.;
995 ptd->met_data.wave_period = bstr->GetInt(285, 6);
996 ptd->met_data.wave_dir = bstr->GetInt(291, 9);
997 ptd->met_data.swell_height = bstr->GetInt(300, 8) / 10;
998 ptd->met_data.swell_per = bstr->GetInt(308, 6);
999 ptd->met_data.swell_dir = bstr->GetInt(314, 9);
1000 ptd->met_data.seastate = bstr->GetInt(323, 4);
1001
1002 int wt = bstr->GetInt(327, 10);
1003 if (wt & 0x00000200) // negative? (bit 9 = 1)
1004 wt |= 0xFFFFFC00;
1005 ptd->met_data.water_temp = wt / 10.;
1006
1007 ptd->met_data.precipitation = bstr->GetInt(337, 3);
1008 ptd->met_data.salinity = bstr->GetInt(340, 9) / 10.;
1009 ptd->met_data.ice = bstr->GetInt(349, 2);
1010
1011 ptd->Class = AIS_METEO;
1012 ptd->COG = -1.;
1013 ptd->HDG = 511;
1014 ptd->SOG = -1.;
1015 ptd->b_NoTrack = true;
1016 ptd->b_show_track = false;
1017 ptd->b_positionDoubtful = false;
1018 ptd->b_positionOnceValid = true;
1019 b_posn_report = true;
1020 ptd->LastPositionReportTicks = ptd->PositionReportTicks;
1021 ptd->PositionReportTicks = now.GetTicks();
1022 ptd->b_nameValid = true;
1023
1024 parse_result = true;
1025 }
1026 }
1027 break;
1028 }
1029
1030 if (dac == 367 && fi == 33) { // ais8_367_33
1031 // US data acording to DAC367_FI33_em_version_release_3-23mar15_0
1032 // We use only the same kind of data as of ais8_001_31 from these
1033 // reports.
1034 const int size = bstr->GetBitCount();
1035 if (size >= 168) {
1036 // Change to meteo mmsi-ID
1037 if (met_mmsi != 666) ptd->MMSI = met_mmsi;
1038 const int startbits = 56;
1039 const int slotsize = 112;
1040 const int slots_count = (size - startbits) / slotsize;
1041 int slotbit;
1042 for (int slot = 0; slot < slots_count; slot++) {
1043 slotbit = slot * slotsize;
1044 int type = bstr->GetInt(slotbit + 57, 4);
1045 ptd->met_data.hour = bstr->GetInt(slotbit + 66, 5);
1046 ptd->met_data.minute = bstr->GetInt(slotbit + 71, 6);
1047 int Site_ID = bstr->GetInt(slotbit + 77, 7);
1048
1049 // Name the station acc to site ID until message type 1
1050 if (!ptd->b_nameValid) {
1051 wxString nameID = "METEO Site: ";
1052 nameID << Site_ID;
1053 strncpy(ptd->ShipName, nameID, SHIP_NAME_LEN - 1);
1054 ptd->b_nameValid = true;
1055 }
1056
1057 if (type == 0) { // Location
1058 int lon = bstr->GetInt(slotbit + 90, 28);
1059 if (lon & 0x08000000) // negative?
1060 lon |= 0xf0000000;
1061 ptd->Lon = lon / 600000.;
1062
1063 int lat = bstr->GetInt(slotbit + 118, 27);
1064 if (lat & 0x04000000) // negative?
1065 lat |= 0xf8000000;
1066 ptd->Lat = lat / 600000.;
1067 ptd->b_positionOnceValid = true;
1068
1069 } else if (type == 1) { // Name
1070 bstr->GetStr(slotbit + 84, 84, &ptd->ShipName[0], SHIP_NAME_LEN);
1071 ptd->b_nameValid = true;
1072
1073 } else if (type == 2) { // Wind
1074 // Description 1 and 2 are real time values.
1075 int descr = bstr->GetInt(slotbit + 116, 3);
1076 if (descr == 1 || descr == 2) {
1077 ptd->met_data.wind_kn = bstr->GetInt(slotbit + 84, 7);
1078 ptd->met_data.wind_gust_kn = bstr->GetInt(slotbit + 91, 7);
1079 ptd->met_data.wind_dir = bstr->GetInt(slotbit + 98, 9);
1080 ptd->met_data.wind_gust_dir = bstr->GetInt(slotbit + 107, 9);
1081 }
1082
1083 } else if (type == 3) { // Water level
1084 // Description 1 and 2 are real time values.
1085 int descr = bstr->GetInt(slotbit + 108, 3);
1086 if (descr == 1 || descr == 2) {
1087 int wltype = bstr->GetInt(slotbit + 84, 1);
1088 int wl = bstr->GetInt(slotbit + 85, 16); // cm
1089 if (wl & 0x00004000) // negative?
1090 wl |= 0xffff0000;
1091
1092 if (wltype == 1) // 0 = deviation from datum; 1 = water depth
1093 ptd->met_data.water_level = wl / 100.; // m
1094 else
1095 ptd->met_data.water_lev_dev = wl / 100.; // m
1096 }
1097 ptd->met_data.water_lev_trend = bstr->GetInt(slotbit + 101, 2);
1098 ptd->met_data.vertical_ref = bstr->GetInt(slotbit + 103, 5);
1099
1100 } else if (type == 6) { // Horizontal Current Profil
1101 ptd->met_data.current = bstr->GetInt(slotbit + 102, 8) / 10.0;
1102 ptd->met_data.curr_dir = bstr->GetInt(slotbit + 110, 9);
1103 } else if (type == 7) { // Sea state
1104 int swell_descr =
1105 bstr->GetInt(slotbit + 111, 3); // Use 1 || 2 real data
1106 if (swell_descr == 1 || swell_descr == 2) {
1107 ptd->met_data.swell_height =
1108 bstr->GetInt(slotbit + 84, 8) / 10.0;
1109 ptd->met_data.swell_per = bstr->GetInt(slotbit + 92, 6);
1110 ptd->met_data.swell_dir = bstr->GetInt(slotbit + 98, 9);
1111 }
1112 ptd->met_data.seastate = bstr->GetInt(slotbit + 107, 4); // Bf
1113 int wt_descr = bstr->GetInt(slotbit + 131, 3);
1114 if (wt_descr == 1 || wt_descr == 2)
1115 ptd->met_data.water_temp =
1116 bstr->GetInt(slotbit + 114, 10) / 10. - 10.;
1117
1118 int wawe_descr = bstr->GetInt(slotbit + 157, 3);
1119 if (wawe_descr == 1 || wawe_descr == 2) { // Only real data
1120 ptd->met_data.wave_height =
1121 bstr->GetInt(slotbit + 134, 8) / 10.0;
1122 ptd->met_data.wave_period = bstr->GetInt(slotbit + 142, 6);
1123 ptd->met_data.wave_dir = bstr->GetInt(slotbit + 148, 9);
1124 }
1125 ptd->met_data.salinity = bstr->GetInt(slotbit + 160, 9 / 10.0);
1126
1127 } else if (type == 8) { // Salinity
1128 ptd->met_data.water_temp =
1129 bstr->GetInt(slotbit + 84, 10) / 10.0 - 10.0;
1130 ptd->met_data.salinity = bstr->GetInt(slotbit + 120, 9) / 10.0;
1131
1132 } else if (type == 9) { // Weather
1133 int tmp = bstr->GetInt(slotbit + 84, 11);
1134 if (tmp & 0x00000400) // negative?
1135 tmp |= 0xFFFFF800;
1136 ptd->met_data.air_temp = tmp / 10.;
1137 int pp, precip = bstr->GetInt(slotbit + 98, 2);
1138 switch (precip) { // Adapt to IMO precipitation
1139 case 0:
1140 pp = 1;
1141 case 1:
1142 pp = 5;
1143 case 2:
1144 pp = 4;
1145 case 3:
1146 pp = 7;
1147 }
1148 ptd->met_data.precipitation = pp;
1149 ptd->met_data.hor_vis = bstr->GetInt(slotbit + 100, 8) / 10.0;
1150 ptd->met_data.dew_point =
1151 bstr->GetInt(slotbit + 108, 10) / 10.0 - 20.0;
1152 ptd->met_data.airpress = bstr->GetInt(slotbit + 121, 9) + 799;
1153 ptd->met_data.airpress_tend = bstr->GetInt(slotbit + 130, 2);
1154 ptd->met_data.salinity = bstr->GetInt(slotbit + 135, 9) / 10.0;
1155
1156 } else if (type == 11) { // Wind V2
1157 // Description 1 and 2 are real time values.
1158 int descr = bstr->GetInt(slotbit + 113, 3);
1159 if (descr == 1 || descr == 2) {
1160 ptd->met_data.wind_kn = bstr->GetInt(slotbit + 84, 7);
1161 ptd->met_data.wind_gust_kn = bstr->GetInt(slotbit + 91, 7);
1162 ptd->met_data.wind_dir = bstr->GetInt(slotbit + 98, 9);
1163 }
1164 }
1165 }
1166
1167 if (ptd->b_positionOnceValid) {
1168 ptd->Class = AIS_METEO;
1169 ptd->COG = -1.;
1170 ptd->HDG = 511;
1171 ptd->SOG = -1.;
1172 ptd->b_NoTrack = true;
1173 ptd->b_show_track = false;
1174 ptd->b_positionDoubtful = false;
1175 b_posn_report = true;
1176 ptd->LastPositionReportTicks = ptd->PositionReportTicks;
1177 ptd->PositionReportTicks = now.GetTicks();
1178 ptd->b_nameValid = true;
1179
1180 parse_result = true;
1181 }
1182 }
1183 break;
1184 }
1185 break;
1186 }
1187 case 14: // Safety Related Broadcast
1188 {
1189 // Always capture the MSG_14 text
1190 char msg_14_text[968];
1191 if (bstr->GetBitCount() > 40) {
1192 int nx = ((bstr->GetBitCount() - 40) / 6) * 6;
1193 int nd = bstr->GetStr(41, nx, msg_14_text, 968);
1194 nd = wxMax(0, nd);
1195 nd = wxMin(nd, 967);
1196 msg_14_text[nd] = 0;
1197 ptd->MSG_14_text = wxString(msg_14_text, wxConvUTF8);
1198 }
1199 parse_result = true; // so far so good
1200
1201 break;
1202 }
1203
1204 case 6: // Addressed Binary Message
1205 [[fallthrough]];
1206 case 7: // Binary Ack
1207 [[fallthrough]];
1208 default:
1209 break;
1210 }
1211
1212 if (b_posn_report) ptd->b_lost = false;
1213
1214 if (true == parse_result) {
1215 // Revalidate the target under some conditions
1216 if (!ptd->b_active && !ptd->b_positionDoubtful && b_posn_report)
1217 ptd->b_active = true;
1218 }
1219
1220 return parse_result;
1221}
1222
1223AisDecoder::AisDecoder(const AisDecoderCallbacks &callbacks)
1224 : m_signalk_selfid(""), m_callbacks(callbacks) {
1225 // Load cached AIS target names from a file
1226 AISTargetNamesC = new AIS_Target_Name_Hash;
1227 AISTargetNamesNC = new AIS_Target_Name_Hash;
1228
1229 if (g_benableAISNameCache) {
1230 if (wxFileName::FileExists(AISTargetNameFileName)) {
1231 wxTextFile infile;
1232 if (infile.Open(AISTargetNameFileName)) {
1233 AIS_Target_Name_Hash *HashFile = AISTargetNamesNC;
1234 wxString line = infile.GetFirstLine();
1235 while (!infile.Eof()) {
1236 if (line.IsSameAs(wxT("+++==Confirmed Entry's==+++")))
1237 HashFile = AISTargetNamesC;
1238 else {
1239 if (line.IsSameAs(wxT("+++==Non Confirmed Entry's==+++")))
1240 HashFile = AISTargetNamesNC;
1241 else {
1242 wxStringTokenizer tokenizer(line, _T(","));
1243 int mmsi = wxAtoi(tokenizer.GetNextToken());
1244 wxString name = tokenizer.GetNextToken().Trim();
1245 (*HashFile)[mmsi] = name;
1246 }
1247 }
1248 line = infile.GetNextLine();
1249 }
1250 }
1251 infile.Close();
1252 }
1253 }
1254
1255 BuildERIShipTypeHash();
1256
1257 g_pais_alert_dialog_active = nullptr;
1258 m_bAIS_Audio_Alert_On = false;
1259
1260 m_n_targets = 0;
1261
1262 m_bAIS_AlertPlaying = false;
1263
1264 TimerAIS.SetOwner(this, TIMER_AIS1);
1265 TimerAIS.Start(TIMER_AIS_MSEC, wxTIMER_CONTINUOUS);
1266
1267 m_ptentative_dsctarget = nullptr;
1268 m_dsc_timer.SetOwner(this, TIMER_DSC);
1269
1270 // Create/connect a dynamic event handler slot for wxEVT_OCPN_DATASTREAM(s)
1271 // FIXME delete Connect(wxEVT_OCPN_DATASTREAM,
1272 // (wxObjectEventFunction)(wxEventFunction)&AisDecoder::OnEvtAIS);
1273 // Connect(EVT_OCPN_SIGNALKSTREAM,
1274 // (wxObjectEventFunction)(wxEventFunction)&AisDecoder::OnEvtSignalK);
1275 InitCommListeners();
1276}
1277
1278AisDecoder::~AisDecoder() {
1279 // for (const auto &it : GetTargetList()) {
1280 // AisTargetData *td = it.second;
1281 //
1282 // delete td;
1283 // }
1284
1285 // Write mmsi-shipsname to file in a safe way
1286 wxTempFile outfile;
1287 if (outfile.Open(AISTargetNameFileName)) {
1288 wxString content = wxT("+++==Confirmed Entry's==+++");
1289 AIS_Target_Name_Hash::iterator it;
1290 for (it = AISTargetNamesC->begin(); it != AISTargetNamesC->end(); ++it) {
1291 content.append(_T("\r\n"));
1292 content.append(wxString::Format(wxT("%i"), it->first));
1293 content.append(_T(",")).append(it->second);
1294 }
1295 content.append(_T("\r\n"));
1296 content.append(_T("+++==Non Confirmed Entry's==+++"));
1297 for (it = AISTargetNamesNC->begin(); it != AISTargetNamesNC->end(); ++it) {
1298 content.append(_T("\r\n"));
1299 content.append(wxString::Format(wxT("%i"), it->first));
1300 content.append(_T(",")).append(it->second);
1301 }
1302 outfile.Write(content);
1303 outfile.Commit();
1304 }
1305
1306 AISTargetNamesC->clear();
1307 delete AISTargetNamesC;
1308 AISTargetNamesNC->clear();
1309 delete AISTargetNamesNC;
1310
1311 clear_hash_ERI();
1312
1313 m_dsc_timer.Stop();
1314 m_AIS_Audio_Alert_Timer.Stop();
1315 TimerAIS.Stop();
1316
1317#ifdef AIS_DEBUG
1318 printf(
1319 "First message[1, 2] ticks: %d Last Message [1,2]ticks %d Difference: "
1320 "%d\n",
1321 first_rx_ticks, rx_ticks, rx_ticks - first_rx_ticks);
1322#endif
1323}
1324
1325bool IsTargetOnTheIgnoreList(const int &mmsi) {
1326 // Check the MMSI-Prop list if the target shall be ignored
1327 for (unsigned int i = 0; i < g_MMSI_Props_Array.GetCount(); i++) {
1328 if (mmsi == g_MMSI_Props_Array[i]->MMSI) {
1329 MmsiProperties *props = g_MMSI_Props_Array[i];
1330 if (props->m_bignore) {
1331 return true;
1332 }
1333 }
1334 }
1335 return false;
1336}
1337
1338void AisDecoder::InitCommListeners() {
1339 // Initialize the comm listeners
1340
1341 // NMEA0183
1342 // VDM
1343 Nmea0183Msg n0183_msg_VDM("VDM");
1344 listener_N0183_VDM.Listen(n0183_msg_VDM, this, EVT_N0183_VDM);
1345 Bind(EVT_N0183_VDM, [&](const ObservedEvt &ev) {
1346 auto ptr = ev.GetSharedPtr();
1347 auto n0183_msg = std::static_pointer_cast<const Nmea0183Msg>(ptr);
1348 HandleN0183_AIS(n0183_msg);
1349 });
1350
1351 // FRPOS
1352 Nmea0183Msg n0183_msg_FRPOS("FRPOS");
1353 listener_N0183_FRPOS.Listen(n0183_msg_FRPOS, this, EVT_N0183_FRPOS);
1354
1355 Bind(EVT_N0183_FRPOS, [&](const ObservedEvt &ev) {
1356 auto ptr = ev.GetSharedPtr();
1357 auto n0183_msg = std::static_pointer_cast<const Nmea0183Msg>(ptr);
1358 HandleN0183_AIS(n0183_msg);
1359 });
1360
1361 // CDDSC
1362 Nmea0183Msg n0183_msg_CDDSC("CDDSC");
1363 listener_N0183_CDDSC.Listen(n0183_msg_CDDSC, this, EVT_N0183_CDDSC);
1364 Bind(EVT_N0183_CDDSC, [&](const ObservedEvt &ev) {
1365 auto ptr = ev.GetSharedPtr();
1366 auto n0183_msg = std::static_pointer_cast<const Nmea0183Msg>(ptr);
1367 HandleN0183_AIS(n0183_msg);
1368 });
1369
1370 // CDDSE
1371 Nmea0183Msg n0183_msg_CDDSE("CDDSE");
1372 listener_N0183_CDDSE.Listen(n0183_msg_CDDSE, this, EVT_N0183_CDDSE);
1373 Bind(EVT_N0183_CDDSE, [&](const ObservedEvt &ev) {
1374 auto ptr = ev.GetSharedPtr();
1375 auto n0183_msg = std::static_pointer_cast<const Nmea0183Msg>(ptr);
1376 HandleN0183_AIS(n0183_msg);
1377 });
1378
1379 // TLL
1380 Nmea0183Msg n0183_msg_TLL("TLL");
1381 listener_N0183_TLL.Listen(n0183_msg_TLL, this, EVT_N0183_TLL);
1382
1383 Bind(EVT_N0183_TLL, [&](const ObservedEvt &ev) {
1384 auto ptr = ev.GetSharedPtr();
1385 auto n0183_msg = std::static_pointer_cast<const Nmea0183Msg>(ptr);
1386 HandleN0183_AIS(n0183_msg);
1387 });
1388
1389 // TTM
1390 Nmea0183Msg n0183_msg_ttm("TTM");
1391 listener_N0183_TTM.Listen(n0183_msg_ttm, this, EVT_N0183_TTM);
1392 Bind(EVT_N0183_TTM, [&](const ObservedEvt &ev) {
1393 auto ptr = ev.GetSharedPtr();
1394 auto n0183_msg = std::static_pointer_cast<const Nmea0183Msg>(ptr);
1395 HandleN0183_AIS(n0183_msg);
1396 });
1397
1398 // OSD
1399 Nmea0183Msg n0183_msg_OSD("OSD");
1400 listener_N0183_OSD.Listen(n0183_msg_OSD, this, EVT_N0183_OSD);
1401 Bind(EVT_N0183_OSD, [&](const ObservedEvt &ev) {
1402 auto ptr = ev.GetSharedPtr();
1403 auto n0183_msg = std::static_pointer_cast<const Nmea0183Msg>(ptr);
1404 HandleN0183_AIS(n0183_msg);
1405 });
1406
1407 // WPL
1408 Nmea0183Msg n0183_msg_WPL("WPL");
1409 listener_N0183_WPL.Listen(n0183_msg_WPL, this, EVT_N0183_WPL);
1410 Bind(EVT_N0183_WPL, [&](const ObservedEvt &ev) {
1411 auto ptr = ev.GetSharedPtr();
1412 auto n0183_msg = std::static_pointer_cast<const Nmea0183Msg>(ptr);
1413 HandleN0183_AIS(n0183_msg);
1414 });
1415
1416 // SignalK
1417 SignalkMsg sk_msg;
1418 listener_SignalK.Listen(sk_msg, this, EVT_SIGNALK);
1419 Bind(EVT_SIGNALK, [&](const ObservedEvt &ev) {
1420 HandleSignalK(UnpackEvtPointer<SignalkMsg>(ev));
1421 });
1422
1423 // AIS Class A PGN 129038
1424 //-----------------------------
1425 Nmea2000Msg n2k_msg_129038(static_cast<uint64_t>(129038));
1426 listener_N2K_129038.Listen(n2k_msg_129038, this, EVT_N2K_129038);
1427 Bind(EVT_N2K_129038, [&](const ObservedEvt &ev) {
1428 HandleN2K_129038(UnpackEvtPointer<Nmea2000Msg>(ev));
1429 });
1430
1431 // AIS Class B PGN 129039
1432 //-----------------------------
1433 Nmea2000Msg n2k_msg_129039(static_cast<uint64_t>(129039));
1434 listener_N2K_129039.Listen(n2k_msg_129039, this, EVT_N2K_129039);
1435 Bind(EVT_N2K_129039, [&](const ObservedEvt &ev) {
1436 HandleN2K_129039(UnpackEvtPointer<Nmea2000Msg>(ev));
1437 });
1438
1439 // AIS ATON PGN 129041
1440 //-----------------------------
1441 Nmea2000Msg n2k_msg_129041(static_cast<uint64_t>(129041));
1442 listener_N2K_129041.Listen(n2k_msg_129041, this, EVT_N2K_129041);
1443 Bind(EVT_N2K_129041, [&](const ObservedEvt &ev) {
1444 HandleN2K_129041(UnpackEvtPointer<Nmea2000Msg>(ev));
1445 });
1446
1447 // AIS static data class A PGN 129794
1448 //-----------------------------
1449 Nmea2000Msg n2k_msg_129794(static_cast<uint64_t>(129794));
1450 listener_N2K_129794.Listen(n2k_msg_129794, this, EVT_N2K_129794);
1451 Bind(EVT_N2K_129794, [&](const ObservedEvt &ev) {
1452 HandleN2K_129794(UnpackEvtPointer<Nmea2000Msg>(ev));
1453 });
1454
1455 // AIS static data class B part A PGN 129809
1456 //-----------------------------
1457 Nmea2000Msg n2k_msg_129809(static_cast<uint64_t>(129809));
1458 listener_N2K_129809.Listen(n2k_msg_129809, this, EVT_N2K_129809);
1459 Bind(EVT_N2K_129809, [&](const ObservedEvt &ev) {
1460 HandleN2K_129809(UnpackEvtPointer<Nmea2000Msg>(ev));
1461 });
1462
1463 // AIS static data class B part B PGN 129810
1464 //-----------------------------
1465 Nmea2000Msg n2k_msg_129810(static_cast<uint64_t>(129810));
1466 listener_N2K_129810.Listen(n2k_msg_129810, this, EVT_N2K_129810);
1467 Bind(EVT_N2K_129810, [&](const ObservedEvt &ev) {
1468 HandleN2K_129810(UnpackEvtPointer<Nmea2000Msg>(ev));
1469 });
1470
1471 // AIS Base Station report PGN 129793
1472 //-----------------------------
1473 Nmea2000Msg n2k_msg_129793(static_cast<uint64_t>(129793));
1474 listener_N2K_129793.Listen(n2k_msg_129793, this, EVT_N2K_129793);
1475 Bind(EVT_N2K_129793, [&](const ObservedEvt &ev) {
1476 HandleN2K_129793(UnpackEvtPointer<Nmea2000Msg>(ev));
1477 });
1478}
1479
1480bool AisDecoder::HandleN0183_AIS(const N0183MsgPtr &n0183_msg) {
1481 std::string str = n0183_msg->payload;
1482 wxString sentence(str.c_str());
1483 DecodeN0183(sentence);
1485 return true;
1486}
1487
1488bool AisDecoder::HandleN2K_129038(const N2000MsgPtr &n2k_msg) {
1489 std::vector<unsigned char> v = n2k_msg->payload;
1490
1491 uint8_t MessageID;
1492 tN2kAISRepeat Repeat;
1493 uint32_t UserID;
1494 double Latitude;
1495 double Longitude;
1496 bool Accuracy;
1497 bool RAIM;
1498 uint8_t Seconds;
1499 double COG;
1500 double SOG;
1501 double Heading;
1502 double ROT;
1503 tN2kAISNavStatus NavStat = N2kaisns_Under_Way_Motoring;
1504 tN2kAISTransceiverInformation AISTransceiverInformation;
1505
1506 if (ParseN2kPGN129038(v, MessageID, Repeat, UserID, Latitude, Longitude,
1507 Accuracy, RAIM, Seconds, COG, SOG, Heading, ROT,
1508 NavStat, AISTransceiverInformation)) {
1509 unsigned mmsi = UserID;
1510 // Stop here if the target shall be ignored
1511 if (mmsi == g_OwnShipmmsi || IsTargetOnTheIgnoreList(mmsi)) return false;
1512
1513 // Is this target already in the global target list?
1514 // Search the current AISTargetList for an MMSI match
1515 std::shared_ptr<AisTargetData> pTargetData = nullptr;
1516 bool bnewtarget = false;
1517
1518 auto it = AISTargetList.find(mmsi);
1519 if (it == AISTargetList.end()) // not found
1520 {
1521 pTargetData = AisTargetDataMaker::GetInstance().GetTargetData();
1522 bnewtarget = true;
1523 m_n_targets++;
1524 } else {
1525 pTargetData = it->second; // find current entry
1526 }
1527
1528 wxDateTime now = wxDateTime::Now();
1529 now.MakeUTC();
1530
1531 // Populate the target_data
1532 pTargetData->MMSI = mmsi;
1533 pTargetData->MID = MessageID;
1534 pTargetData->MMSI = mmsi;
1535 pTargetData->Class = AIS_CLASS_A;
1536 // Check for SART and friends by looking at first two digits of MMSI
1537 if (97 == pTargetData->MMSI / 10000000) {
1538 pTargetData->Class = AIS_SART;
1539 // won't get a static report, so fake it here
1540 pTargetData->StaticReportTicks = now.GetTicks();
1541 }
1542 pTargetData->NavStatus = static_cast<ais_nav_status>(NavStat);
1543 if (!N2kIsNA(SOG)) pTargetData->SOG = MS2KNOTS(SOG);
1544 if (!N2kIsNA(COG)) pTargetData->COG = GeodesicRadToDeg(COG);
1545 if (!N2kIsNA(Heading)) pTargetData->HDG = GeodesicRadToDeg(Heading);
1546 if (!N2kIsNA(Longitude)) pTargetData->Lon = Longitude;
1547 if (!N2kIsNA(Latitude)) pTargetData->Lat = Latitude;
1548
1549 pTargetData->ROTAIS = ROT;
1550
1551 // FIXME (dave)
1552 // if (ROT == 128)
1553 // pTargetData->ROTAIS = -128; // not available codes as -128
1554 // else if ((ROT & 0x80) == 0x80) {
1555 // pTargetData->ROTAIS = ROT - 256; // convert to twos complement
1556 // rot_dir = -1.0;
1557 // }
1558 //
1559 // pTargetData->ROTIND = round(rot_dir * pow((ROT / 4.733), 2));
1560
1561 pTargetData->b_OwnShip =
1562 AISTransceiverInformation ==
1563 tN2kAISTransceiverInformation::N2kaisown_information_not_broadcast;
1564 pTargetData->b_active = true;
1565 pTargetData->b_lost = false;
1566 pTargetData->b_positionOnceValid = true;
1567 pTargetData->LastPositionReportTicks = pTargetData->PositionReportTicks;
1568 pTargetData->PositionReportTicks = now.GetTicks();
1569
1570 pSelectAIS->DeleteSelectablePoint((void *)(long)mmsi, SELTYPE_AISTARGET);
1571 CommitAISTarget(pTargetData, "", true, bnewtarget);
1572
1574 return true;
1575 } else
1576 return false;
1577}
1578
1579// AIS position reports for Class B
1580bool AisDecoder::HandleN2K_129039(const N2000MsgPtr &n2k_msg) {
1581 std::vector<unsigned char> v = n2k_msg->payload;
1582
1583 // Input:
1584 // - N2kMsg NMEA2000 message to decode
1585 // bool ParseN2kPGN129039(std::vector<unsigned char> &v, uint8_t &MessageID,
1586 // tN2kAISRepeat &Repeat, uint32_t &UserID,
1587 // double &Latitude, double &Longitude, bool
1588 // &Accuracy, bool &RAIM, uint8_t &Seconds, double
1589 // &COG, double &SOG, tN2kAISTransceiverInformation
1590 // &AISTransceiverInformation, double &Heading,
1591 // tN2kAISUnit &Unit, bool &Display, bool &DSC, bool
1592 // &Band, bool &Msg22, tN2kAISMode &Mode, bool
1593 // &State);
1594
1595 uint8_t MessageID;
1596 tN2kAISRepeat Repeat;
1597 uint32_t UserID;
1598 double Latitude;
1599 double Longitude;
1600 bool Accuracy;
1601 bool RAIM;
1602 uint8_t Seconds;
1603 double COG;
1604 double SOG;
1605 double Heading;
1606 tN2kAISTransceiverInformation AISTransceiverInformation;
1607 tN2kAISUnit Unit;
1608 bool DSC, Band, Msg22, State, Display;
1609 tN2kAISMode Mode;
1610
1611 if (ParseN2kPGN129039(v, MessageID, Repeat, UserID, Latitude, Longitude,
1612 Accuracy, RAIM, Seconds, COG, SOG,
1613 AISTransceiverInformation, Heading, Unit, Display, DSC,
1614 Band, Msg22, Mode, State)) {
1615 uint32_t mmsi = UserID;
1616 // Stop here if the target shall be ignored
1617 if (mmsi == g_OwnShipmmsi || IsTargetOnTheIgnoreList(mmsi)) return false;
1618
1619 // Is this target already in the global target list?
1620 // Search the current AISTargetList for an MMSI match
1621 std::shared_ptr<AisTargetData> pTargetData = nullptr;
1622 bool bnewtarget = false;
1623
1624 auto it = AISTargetList.find(mmsi);
1625 if (it == AISTargetList.end()) // not found
1626 {
1627 pTargetData = AisTargetDataMaker::GetInstance().GetTargetData();
1628 bnewtarget = true;
1629 m_n_targets++;
1630 } else {
1631 pTargetData = it->second; // find current entry
1632 }
1633
1634 wxDateTime now = wxDateTime::Now();
1635 now.MakeUTC();
1636
1637 // Populate the target_data
1638 pTargetData->MMSI = mmsi;
1639 pTargetData->MID = MessageID;
1640 if (!isBuoyMmsi(mmsi))
1641 pTargetData->Class = AIS_CLASS_B;
1642 else
1643 pTargetData->Class = AIS_BUOY;
1644
1645 pTargetData->NavStatus = UNDEFINED; // Class B targets have no status.
1646 if (!N2kIsNA(SOG)) pTargetData->SOG = MS2KNOTS(SOG);
1647 if (!N2kIsNA(COG)) pTargetData->COG = GeodesicRadToDeg(COG);
1648 if (!N2kIsNA(Heading)) pTargetData->HDG = GeodesicRadToDeg(Heading);
1649 if (!N2kIsNA(Longitude)) pTargetData->Lon = Longitude;
1650 if (!N2kIsNA(Latitude)) pTargetData->Lat = Latitude;
1651
1652 pTargetData->b_positionOnceValid = true;
1653 pTargetData->b_active = true;
1654 pTargetData->b_lost = false;
1655 pTargetData->LastPositionReportTicks = pTargetData->PositionReportTicks;
1656 pTargetData->PositionReportTicks = now.GetTicks();
1657 pTargetData->b_OwnShip =
1658 AISTransceiverInformation ==
1659 tN2kAISTransceiverInformation::N2kaisown_information_not_broadcast;
1660
1661 pSelectAIS->DeleteSelectablePoint((void *)(long)mmsi, SELTYPE_AISTARGET);
1662 CommitAISTarget(pTargetData, "", true, bnewtarget);
1663
1665 return true;
1666 } else
1667 return false;
1668
1669 return true;
1670}
1671
1672bool AisDecoder::HandleN2K_129041(const N2000MsgPtr &n2k_msg) {
1673 std::vector<unsigned char> v = n2k_msg->payload;
1674
1675 tN2kAISAtoNReportData data;
1676
1677#if 0
1678 struct tN2kAISAtoNReportData {
1679 uint8_t MessageID;
1680 tN2kAISRepeat Repeat;
1681 uint32_t UserID;
1682 double Longitude;
1683 double Latitude;
1684 bool Accuracy;
1685 bool RAIM;
1686 uint8_t Seconds;
1687 double Length;
1688 double Beam;
1689 double PositionReferenceStarboard ;
1690 double PositionReferenceTrueNorth;
1691 tN2kAISAtoNType AtoNType;
1692 bool OffPositionIndicator;
1693 bool VirtualAtoNFlag;
1694 bool AssignedModeFlag;
1695 tN2kGNSStype GNSSType;
1696 uint8_t AtoNStatus;
1697 tN2kAISTransceiverInformation AISTransceiverInformation;
1698 char AtoNName[34 + 1];
1699#endif
1700
1701 if (ParseN2kPGN129041(v, data)) {
1702 uint32_t mmsi = data.UserID;
1703 // Stop here if the target shall be ignored
1704 if (mmsi == g_OwnShipmmsi || IsTargetOnTheIgnoreList(mmsi)) return false;
1705
1706 // Is this target already in the global target list?
1707 // Search the current AISTargetList for an MMSI match
1708 std::shared_ptr<AisTargetData> pTargetData = nullptr;
1709 bool bnewtarget = false;
1710
1711 auto it = AISTargetList.find(mmsi);
1712 if (it == AISTargetList.end()) // not found
1713 {
1714 pTargetData = AisTargetDataMaker::GetInstance().GetTargetData();
1715 bnewtarget = true;
1716 m_n_targets++;
1717 } else {
1718 pTargetData = it->second; // find current entry
1719 }
1720
1721 // Populate the target_data
1722 pTargetData->MMSI = mmsi;
1723
1724 wxDateTime now = wxDateTime::Now();
1725 now.MakeUTC();
1726
1727 int offpos = data.OffPositionIndicator; // off position flag
1728 int virt = data.VirtualAtoNFlag; // virtual flag
1729
1730 if (virt)
1731 pTargetData->NavStatus = ATON_VIRTUAL;
1732 else
1733 pTargetData->NavStatus = ATON_REAL;
1734
1735 pTargetData->m_utc_sec = data.Seconds;
1736
1737 if (pTargetData->m_utc_sec <= 59) {
1738 pTargetData->NavStatus += 1;
1739 if (offpos) pTargetData->NavStatus += 1;
1740 }
1741
1742 data.AtoNName[34] = 0;
1743 strncpy(pTargetData->ShipName, data.AtoNName, SHIP_NAME_LEN - 1);
1744 pTargetData->ShipName[sizeof(pTargetData->ShipName) - 1] = '\0';
1745 pTargetData->b_nameValid = true;
1746 pTargetData->MID = 124; // Indicates a name from n2k
1747
1748 pTargetData->ShipType = data.AtoNType;
1749 pTargetData->Class = AIS_ATON;
1750
1751 if (!N2kIsNA(data.Longitude)) pTargetData->Lon = data.Longitude;
1752 if (!N2kIsNA(data.Latitude)) pTargetData->Lat = data.Latitude;
1753 pTargetData->b_positionDoubtful = false;
1754 pTargetData->b_positionOnceValid = true; // Got the position at least once
1755 pTargetData->LastPositionReportTicks = pTargetData->PositionReportTicks;
1756 pTargetData->PositionReportTicks = now.GetTicks();
1757
1758 // FIXME (dave) Populate more fiddly static data
1759
1760 pSelectAIS->DeleteSelectablePoint((void *)(long)mmsi, SELTYPE_AISTARGET);
1761 CommitAISTarget(pTargetData, "", true, bnewtarget);
1762
1763 touch_state.Notify();
1764 return true;
1765 } else
1766 return false;
1767}
1768
1769// AIS static data class A
1770bool AisDecoder::HandleN2K_129794(const N2000MsgPtr &n2k_msg) {
1771 std::vector<unsigned char> v = n2k_msg->payload;
1772
1773 uint8_t MessageID;
1774 tN2kAISRepeat Repeat;
1775 uint32_t UserID;
1776 uint32_t IMOnumber;
1777 char Callsign[21];
1778 char Name[SHIP_NAME_LEN];
1779 uint8_t VesselType;
1780 double Length;
1781 double Beam;
1782 double PosRefStbd;
1783 double PosRefBow;
1784 uint16_t ETAdate;
1785 double ETAtime;
1786 double Draught;
1787 char Destination[DESTINATION_LEN];
1788 tN2kAISVersion AISversion;
1789 tN2kGNSStype GNSStype;
1790 tN2kAISDTE DTE;
1791 tN2kAISTranceiverInfo AISinfo;
1792
1793 if (ParseN2kPGN129794(v, MessageID, Repeat, UserID, IMOnumber, Callsign, Name,
1794 VesselType, Length, Beam, PosRefStbd, PosRefBow,
1795 ETAdate, ETAtime, Draught, Destination, AISversion,
1796 GNSStype, DTE, AISinfo)) {
1797 uint32_t mmsi = UserID;
1798 // Stop here if the target shall be ignored
1799 if (mmsi == g_OwnShipmmsi || IsTargetOnTheIgnoreList(mmsi)) return false;
1800
1801 // Is this target already in the global target list?
1802 // Search the current AISTargetList for an MMSI match
1803 std::shared_ptr<AisTargetData> pTargetData = nullptr;
1804 bool bnewtarget = false;
1805
1806 auto it = AISTargetList.find(mmsi);
1807 if (it == AISTargetList.end()) // not found
1808 {
1809 pTargetData = AisTargetDataMaker::GetInstance().GetTargetData();
1810 bnewtarget = true;
1811 m_n_targets++;
1812 } else {
1813 pTargetData = it->second; // find current entry
1814 }
1815
1816 // Populate the target_data
1817 pTargetData->MMSI = mmsi;
1818 strncpy(pTargetData->ShipName, Name, SHIP_NAME_LEN - 1);
1819 pTargetData->ShipName[sizeof(pTargetData->ShipName) - 1] = '\0';
1820 Name[sizeof(Name) - 1] = 0;
1821 pTargetData->b_nameValid = true;
1822 pTargetData->MID = 124; // Indicates a name from n2k
1823
1824 pTargetData->b_OwnShip =
1825 AISinfo ==
1826 tN2kAISTranceiverInfo::N2kaisti_Own_information_not_broadcast;
1827
1828 pTargetData->DimA = PosRefBow;
1829 pTargetData->DimB = Length - PosRefBow;
1830 pTargetData->DimC = Beam - PosRefStbd;
1831 pTargetData->DimD = PosRefStbd;
1832 pTargetData->Draft = Draught;
1833 pTargetData->IMO = IMOnumber;
1834 strncpy(pTargetData->CallSign, Callsign, CALL_SIGN_LEN - 1);
1835 pTargetData->CallSign[sizeof(pTargetData->CallSign) - 1] = '\0';
1836 pTargetData->ShipType = (unsigned char)VesselType;
1837 strncpy(pTargetData->Destination, Destination, DESTINATION_LEN - 1);
1838 pTargetData->Destination[sizeof(pTargetData->Destination) - 1] = '\0';
1839 Destination[sizeof(Destination) - 1] = 0;
1840
1841 if (!N2kIsNA(ETAdate) && !N2kIsNA(ETAtime)) {
1842 long secs = (ETAdate * 24 * 3600) + wxRound(ETAtime);
1843 wxDateTime t((time_t)secs);
1844 if (t.IsValid()) {
1845 wxDateTime tz = t.ToUTC();
1846 pTargetData->ETA_Mo = tz.GetMonth() + 1;
1847 pTargetData->ETA_Day = tz.GetDay();
1848 pTargetData->ETA_Hr = tz.GetHour();
1849 pTargetData->ETA_Min = tz.GetMinute();
1850 }
1851 }
1852
1853 pSelectAIS->DeleteSelectablePoint((void *)(long)mmsi, SELTYPE_AISTARGET);
1854 CommitAISTarget(pTargetData, "", true, bnewtarget);
1855
1857 return true;
1858 } else
1859 return false;
1860}
1861// AIS static data class B part A
1862bool AisDecoder::HandleN2K_129809(const N2000MsgPtr &n2k_msg) {
1863 std::vector<unsigned char> v = n2k_msg->payload;
1864
1865 uint8_t MessageID;
1866 tN2kAISRepeat Repeat;
1867 uint32_t UserID;
1868 char Name[21];
1869
1870 if (ParseN2kPGN129809(v, MessageID, Repeat, UserID, Name)) {
1871 unsigned mmsi = UserID;
1872 // Stop here if the target shall be ignored
1873 if (mmsi == g_OwnShipmmsi || IsTargetOnTheIgnoreList(mmsi)) return false;
1874
1875 // Is this target already in the global target list?
1876 // Search the current AISTargetList for an MMSI match
1877 std::shared_ptr<AisTargetData> pTargetData = nullptr;
1878 bool bnewtarget = false;
1879
1880 auto it = AISTargetList.find(mmsi);
1881 if (it == AISTargetList.end()) // not found
1882 {
1883 pTargetData = AisTargetDataMaker::GetInstance().GetTargetData();
1884 bnewtarget = true;
1885 m_n_targets++;
1886 } else {
1887 pTargetData = it->second; // find current entry
1888 }
1889
1890 // Populate the target_data
1891 pTargetData->MMSI = mmsi;
1892 Name[sizeof(Name) - 1] = 0;
1893 strncpy(pTargetData->ShipName, Name, SHIP_NAME_LEN - 1);
1894 pTargetData->b_nameValid = true;
1895 pTargetData->MID = 124; // Indicates a name from n2k
1896
1897 pSelectAIS->DeleteSelectablePoint((void *)(long)mmsi, SELTYPE_AISTARGET);
1898 CommitAISTarget(pTargetData, "", true, bnewtarget);
1899
1901 return true;
1902
1903 } else
1904 return false;
1905}
1906
1907// AIS static data class B part B
1908bool AisDecoder::HandleN2K_129810(const N2000MsgPtr &n2k_msg) {
1909 std::vector<unsigned char> v = n2k_msg->payload;
1910
1911 uint8_t MessageID;
1912 tN2kAISRepeat Repeat;
1913 uint32_t UserID;
1914 uint8_t VesselType;
1915 char Vendor[20];
1916 char Callsign[20];
1917 double Length;
1918 double Beam;
1919 double PosRefStbd;
1920 double PosRefBow;
1921 uint32_t MothershipID;
1922
1923 if (ParseN2kPGN129810(v, MessageID, Repeat, UserID, VesselType, Vendor,
1924 Callsign, Length, Beam, PosRefStbd, PosRefBow,
1925 MothershipID)) {
1926 unsigned mmsi = UserID;
1927 // Stop here if the target shall be ignored
1928 if (mmsi == g_OwnShipmmsi || IsTargetOnTheIgnoreList(mmsi)) return false;
1929
1930 // Is this target already in the global target list?
1931 // Search the current AISTargetList for an MMSI match
1932 std::shared_ptr<AisTargetData> pTargetData = nullptr;
1933 bool bnewtarget = false;
1934
1935 auto it = AISTargetList.find(mmsi);
1936 if (it == AISTargetList.end()) // not found
1937 {
1938 pTargetData = AisTargetDataMaker::GetInstance().GetTargetData();
1939 bnewtarget = true;
1940 m_n_targets++;
1941 } else {
1942 pTargetData = it->second; // find current entry
1943 }
1944
1945 // Populate the target_data
1946 pTargetData->MMSI = mmsi;
1947 pTargetData->DimA = PosRefBow;
1948 pTargetData->DimB = Length - PosRefBow;
1949 pTargetData->DimC = Beam - PosRefStbd;
1950 pTargetData->DimD = PosRefStbd;
1951 strncpy(pTargetData->CallSign, Callsign, CALL_SIGN_LEN - 1);
1952 pTargetData->CallSign[sizeof(pTargetData->CallSign) - 1] = '\0';
1953 pTargetData->ShipType = (unsigned char)VesselType;
1954
1955 pSelectAIS->DeleteSelectablePoint((void *)(long)mmsi, SELTYPE_AISTARGET);
1956 CommitAISTarget(pTargetData, "", true, bnewtarget);
1957
1959 return true;
1960 } else
1961 return false;
1962}
1963
1964// AIS Base Station Report
1965bool AisDecoder::HandleN2K_129793(const N2000MsgPtr &n2k_msg) {
1966 std::vector<unsigned char> v = n2k_msg->payload;
1967
1968 uint8_t MessageID;
1969 tN2kAISRepeat Repeat;
1970 uint32_t UserID;
1971 double Longitude;
1972 double Latitude;
1973 unsigned int SecondsSinceMidnight;
1974 unsigned int DaysSinceEpoch;
1975
1976 if (ParseN2kPGN129793(v, MessageID, Repeat, UserID, Longitude, Latitude,
1977 SecondsSinceMidnight, DaysSinceEpoch)) {
1978 wxDateTime now = wxDateTime::Now();
1979 now.MakeUTC();
1980
1981 // Is this target already in the global target list?
1982 // Search the current AISTargetList for an MMSI match
1983 int mmsi = UserID;
1984 std::shared_ptr<AisTargetData> pTargetData = nullptr;
1985 bool bnewtarget = false;
1986
1987 auto it = AISTargetList.find(mmsi);
1988 if (it == AISTargetList.end()) // not found
1989 {
1990 pTargetData = AisTargetDataMaker::GetInstance().GetTargetData();
1991 bnewtarget = true;
1992 m_n_targets++;
1993 } else {
1994 pTargetData = it->second; // find current entry
1995 }
1996
1997 // Populate the target_data
1998 pTargetData->MMSI = mmsi;
1999 pTargetData->Class = AIS_BASE;
2000
2001 if (!N2kIsNA(Longitude)) pTargetData->Lon = Longitude;
2002 if (!N2kIsNA(Latitude)) pTargetData->Lat = Latitude;
2003 pTargetData->b_positionDoubtful = false;
2004 pTargetData->b_positionOnceValid = true; // Got the position at least once
2005 pTargetData->LastPositionReportTicks = pTargetData->PositionReportTicks;
2006 pTargetData->PositionReportTicks = now.GetTicks();
2007
2008 // FIXME (dave) Populate more fiddly static data
2009
2010 pSelectAIS->DeleteSelectablePoint((void *)(long)mmsi, SELTYPE_AISTARGET);
2011 CommitAISTarget(pTargetData, "", true, bnewtarget);
2012
2014 return true;
2015 } else
2016 return false;
2017}
2018
2019// void AisDecoder::HandleSignalK(std::shared_ptr<const SignalkMsg> sK_msg){
2020// std::string msgTerminated = sK_msg->raw_message;;
2021// wxString sentence(msgTerminated.c_str());
2022// Decode(sentence);
2023// touch_state.notify();
2024// return true;
2025// }
2026
2027//----------------------------------------------------------------------------------
2028// Handle events from SignalK
2029//----------------------------------------------------------------------------------
2030void AisDecoder::HandleSignalK(const SignalKMsgPtr &sK_msg) {
2031 rapidjson::Document root;
2032
2033 root.Parse(sK_msg->raw_message);
2034
2035 if (root.HasParseError()) return;
2036
2037 if (root.HasMember("self")) {
2038 // m_signalk_selfid = _T("vessels.") + (root["self"].AsString());
2039 m_signalk_selfid =
2040 (root["self"]
2041 .GetString()); // Verified for OpenPlotter node.js server 1.20
2042 }
2043 if (m_signalk_selfid.IsEmpty()) {
2044 return; // Don't handle any messages (with out self) until we know how we
2045 // are
2046 }
2047 long mmsi = 0;
2048 int meteo_SiteID = 0;
2049 if (root.HasMember("context") && root["context"].IsString()) {
2050 wxString context = root["context"].GetString();
2051 if (context == m_signalk_selfid) {
2052#if 0
2053 wxLogMessage(_T("** Ignore context own ship.."));
2054#endif
2055 return;
2056 }
2057 wxString mmsi_string;
2058 if (context.StartsWith(_T("vessels.urn:mrn:imo:mmsi:"), &mmsi_string) ||
2059 context.StartsWith(_T("atons.urn:mrn:imo:mmsi:"), &mmsi_string) ||
2060 context.StartsWith(_T("aircraft.urn:mrn:imo:mmsi:"), &mmsi_string)) {
2061 // wxLogMessage(wxString::Format(_T("Context: %s, %s"),
2062 // context.c_str(), mmsi_string));
2063 if (mmsi_string.ToLong(&mmsi)) {
2064 // wxLogMessage(_T("Got MMSI from context."));
2065 } else {
2066 mmsi = 0;
2067 }
2068 } else if (context.StartsWith(_T("meteo.urn:mrn:imo:mmsi:"),
2069 &mmsi_string)) {
2070 // mmsi_string for a Meteo is like: 002655619:672707
2071 origin_mmsi = wxAtoi(wxString(mmsi_string).BeforeFirst(':'));
2072 meteo_SiteID = wxAtoi('1' + wxString(mmsi_string).AfterFirst(':'));
2073 // Preface "1" to distinguish e.g. "012345" from "12345"
2074 // Get a meteo mmsi_ID
2075 int meteo_mmsi = AisMeteoNewMmsi(origin_mmsi, 0, 0, 999, meteo_SiteID);
2076 if (meteo_mmsi)
2077 mmsi = meteo_mmsi;
2078 else
2079 mmsi = 0;
2080 }
2081 }
2082 if (mmsi == 0) {
2083 return; // Only handle ships with MMSI for now
2084 }
2085
2086 if (g_pMUX && g_pMUX->IsLogActive()) {
2087 wxString logmsg;
2088 logmsg.Printf("AIS :MMSI: %ld", mmsi);
2089 g_pMUX->LogInputMessage(sK_msg, false, false);
2090 }
2091
2092 // Stop here if the target shall be ignored
2093 if (IsTargetOnTheIgnoreList(mmsi)) return;
2094 // If self data is not set on the SK server, own ship mmsi
2095 // could fall through here but being set in options Own ship.
2096 if (mmsi == g_OwnShipmmsi) return;
2097
2098#if 0
2099 wxString dbg;
2100 wxJSONWriter writer;
2101 writer.Write(root, dbg);
2102
2103 wxString msg( _T("AisDecoder::OnEvtSignalK: ") );
2104 msg.append(dbg);
2105 wxLogMessage(msg);
2106#endif
2107 std::shared_ptr<AisTargetData> pTargetData = nullptr;
2108 std::shared_ptr<AisTargetData> pStaleTarget = nullptr;
2109 bool bnewtarget = false;
2110 int last_report_ticks;
2111 wxDateTime now;
2112 getAISTarget(mmsi, pTargetData, pStaleTarget, bnewtarget, last_report_ticks,
2113 now);
2114 if (pTargetData) {
2115 pTargetData->MMSI = mmsi;
2116 getMmsiProperties(pTargetData);
2117 if (root.HasMember("updates") && root["updates"].IsArray()) {
2118 for (rapidjson::Value::ConstValueIterator itr = root["updates"].Begin();
2119 itr != root["updates"].End(); ++itr) {
2120 handleUpdate(pTargetData, bnewtarget, *itr);
2121 }
2122 }
2123
2124 // A SART can send wo any values first transmits. Detect class already here.
2125 if (97 == mmsi / 10000000) {
2126 pTargetData->Class = AIS_SART;
2127 } else if (1994 == mmsi / 100000) {
2128 // SignalK meteo data
2129 pTargetData->Class = AIS_METEO;
2130 pTargetData->met_data.original_mmsi = origin_mmsi;
2131 pTargetData->met_data.stationID = meteo_SiteID;
2132 /* Make a unique "shipname" for each station
2133 based on position inherited from meteo_SiteID */
2134 wxString met_name = pTargetData->ShipName;
2135 if (met_name.Find("METEO") == wxNOT_FOUND) {
2136 wxString s_id;
2137 int id1, id2;
2138 s_id << meteo_SiteID;
2139 id1 = wxAtoi(s_id.Mid(1, 3));
2140 id2 = wxAtoi(s_id.Mid(4, 3));
2141 met_name = "METEO ";
2142 met_name << wxString::Format("%03d", (id1 + id2)).Right(3);
2143 strncpy(pTargetData->ShipName, met_name, SHIP_NAME_LEN - 1);
2144 }
2145 pTargetData->b_nameValid = true;
2146 pTargetData->MID = 123; // Indicates a name from SignalK
2147 pTargetData->COG = -1.;
2148 pTargetData->HDG = 511;
2149 pTargetData->SOG = -1.;
2150 pTargetData->b_NoTrack = true;
2151 pTargetData->b_show_track = false;
2152 }
2153 pTargetData->b_OwnShip = false;
2154 AISTargetList[pTargetData->MMSI] = pTargetData;
2155 }
2156}
2157
2158void AisDecoder::handleUpdate(const std::shared_ptr<AisTargetData> &pTargetData,
2159 bool bnewtarget, const rapidjson::Value &update) {
2160 wxString sfixtime = "";
2161
2162 if (update.HasMember("timestamp")) {
2163 sfixtime = update["timestamp"].GetString();
2164 }
2165 if (update.HasMember("values") && update["values"].IsArray()) {
2166 for (rapidjson::Value::ConstValueIterator itr = update["values"].Begin();
2167 itr != update["values"].End(); ++itr) {
2168 updateItem(pTargetData, bnewtarget, *itr, sfixtime);
2169 }
2170 }
2171 wxDateTime now = wxDateTime::Now();
2172 pTargetData->m_utc_hour = now.ToUTC().GetHour();
2173 pTargetData->m_utc_min = now.ToUTC().GetMinute();
2174 pTargetData->m_utc_sec = now.ToUTC().GetSecond();
2175 // pTargetData->NavStatus = 15; // undefined
2176 pTargetData->b_active = true;
2177 pTargetData->b_lost = false;
2178
2179 if (pTargetData->b_positionOnceValid) {
2180 long mmsi_long = pTargetData->MMSI;
2181 SelectItem *pSel =
2182 pSelectAIS->AddSelectablePoint(pTargetData->Lat, pTargetData->Lon,
2183 (void *)mmsi_long, SELTYPE_AISTARGET);
2184 pSel->SetUserData(pTargetData->MMSI);
2185 }
2186 UpdateOneCPA(pTargetData.get());
2187 if (pTargetData->b_show_track) UpdateOneTrack(pTargetData.get());
2188}
2189
2190void AisDecoder::updateItem(const std::shared_ptr<AisTargetData> &pTargetData,
2191 bool bnewtarget, const rapidjson::Value &item,
2192 wxString &sfixtime) const {
2193 if (item.HasMember("path") && item.HasMember("value")) {
2194 const wxString &update_path = item["path"].GetString();
2195 if (update_path == _T("navigation.position")) {
2196 if (item["value"].HasMember("latitude") &&
2197 item["value"].HasMember("longitude")) {
2198 wxDateTime now = wxDateTime::Now();
2199 now.MakeUTC();
2200 double lat = item["value"]["latitude"].GetDouble();
2201 double lon = item["value"]["longitude"].GetDouble();
2202 pTargetData->LastPositionReportTicks = pTargetData->PositionReportTicks;
2203 pTargetData->PositionReportTicks = now.GetTicks();
2204 pTargetData->StaticReportTicks = now.GetTicks();
2205 pTargetData->Lat = lat;
2206 pTargetData->Lon = lon;
2207 pTargetData->b_positionOnceValid = true;
2208 pTargetData->b_positionDoubtful = false;
2209 }
2210
2211 /* Not implemented in SK server (2024-01)
2212 if (item["value"].HasMember("altitude")) {
2213 pTargetData->altitude = item["value"]["altitude "].GetInt();
2214 }*/
2215 } else if (update_path == _T("navigation.speedOverGround") &&
2216 item["value"].IsNumber()) {
2217 pTargetData->SOG = item["value"].GetDouble() * ms_to_knot_factor;
2218 } else if (update_path == _T("navigation.courseOverGroundTrue") &&
2219 item["value"].IsNumber()) {
2220 pTargetData->COG = GEODESIC_RAD2DEG(item["value"].GetDouble());
2221 } else if (update_path == _T("navigation.headingTrue") &&
2222 item["value"].IsNumber()) {
2223 pTargetData->HDG = GEODESIC_RAD2DEG(item["value"].GetDouble());
2224 } else if (update_path == _T("navigation.rateOfTurn") &&
2225 item["value"].IsNumber()) {
2226 pTargetData->ROTAIS = 4.733 * sqrt(item["value"].GetDouble());
2227 } else if (update_path == _T("design.aisShipType")) {
2228 if (item["value"].HasMember("id")) {
2229 if (!pTargetData->b_isDSCtarget) {
2230 pTargetData->ShipType = item["value"]["id"].GetUint();
2231 }
2232 }
2233 } else if (update_path == _T("atonType")) {
2234 if (item["value"].HasMember("id")) {
2235 pTargetData->ShipType = item["value"]["id"].GetUint();
2236 }
2237 } else if (update_path == _T("virtual")) {
2238 if (item["value"].GetBool()) {
2239 pTargetData->NavStatus = ATON_VIRTUAL;
2240 } else {
2241 pTargetData->NavStatus = ATON_REAL;
2242 }
2243 } else if (update_path == _T("offPosition")) {
2244 if (item["value"].GetBool()) {
2245 if (ATON_REAL == pTargetData->NavStatus) {
2246 pTargetData->NavStatus = ATON_REAL_OFFPOSITION;
2247 } else if (ATON_VIRTUAL == pTargetData->NavStatus) {
2248 pTargetData->NavStatus = ATON_VIRTUAL_OFFPOSITION;
2249 }
2250 }
2251 } else if (update_path == _T("design.draft")) {
2252 if (item["value"].HasMember("maximum") &&
2253 item["value"]["maximum"].IsNumber()) {
2254 pTargetData->Draft = item["value"]["maximum"].GetDouble();
2255 pTargetData->Euro_Draft = item["value"]["maximum"].GetDouble();
2256 }
2257 if (item["value"].HasMember("current") &&
2258 item["value"]["current"].IsNumber()) {
2259 double draft = item["value"]["current"].GetDouble();
2260 if (draft > 0) {
2261 pTargetData->Draft = draft;
2262 pTargetData->Euro_Draft = draft;
2263 }
2264 }
2265 } else if (update_path == _T("design.length")) {
2266 if (pTargetData->DimB == 0) {
2267 if (item["value"].HasMember("overall")) {
2268 if (item["value"]["overall"].IsNumber()) {
2269 pTargetData->Euro_Length = item["value"]["overall"].GetDouble();
2270 pTargetData->DimA = item["value"]["overall"].GetDouble();
2271 }
2272 pTargetData->DimB = 0;
2273 }
2274 }
2275 } else if (update_path == _T("sensors.ais.class")) {
2276 wxString aisclass = item["value"].GetString();
2277 if (aisclass == _T("A")) {
2278 if (!pTargetData->b_isDSCtarget) pTargetData->Class = AIS_CLASS_A;
2279 } else if (aisclass == _T("B")) {
2280 if (!pTargetData->b_isDSCtarget) {
2281 if (!isBuoyMmsi(pTargetData->MMSI))
2282 pTargetData->Class = AIS_CLASS_B;
2283 else
2284 pTargetData->Class = AIS_BUOY;
2285
2286 // Class B targets have no status. Enforce this...
2287 pTargetData->NavStatus = UNDEFINED;
2288 }
2289 } else if (aisclass == _T("BASE")) {
2290 pTargetData->Class = AIS_BASE;
2291 } else if (aisclass == _T("ATON")) {
2292 pTargetData->Class = AIS_ATON;
2293 }
2294 } else if (update_path == _T("sensors.ais.fromBow")) {
2295 if (pTargetData->DimB == 0 && pTargetData->DimA != 0) {
2296 int length = pTargetData->DimA;
2297 if (item["value"].IsNumber()) {
2298 pTargetData->DimA = item["value"].GetDouble();
2299 pTargetData->DimB = length - item["value"].GetDouble();
2300 }
2301 }
2302 } else if (update_path == _T("design.beam")) {
2303 if (pTargetData->DimD == 0) {
2304 if (item["value"].IsNumber()) {
2305 pTargetData->Euro_Beam = item["value"].GetDouble();
2306 pTargetData->DimC = item["value"].GetDouble();
2307 }
2308 pTargetData->DimD = 0;
2309 }
2310 } else if (update_path == _T("sensors.ais.fromCenter")) {
2311 if (pTargetData->DimD == 0 && pTargetData->DimC != 0) {
2312 int beam = pTargetData->DimC;
2313 int center = beam / 2;
2314 if (item["value"].IsNumber()) {
2315 // FIXME (nohal): Dim* are int, but we have seen data streams with
2316 // doubles in them...
2317 pTargetData->DimC = center + item["value"].GetDouble();
2318 pTargetData->DimD = beam - pTargetData->DimC;
2319 }
2320 }
2321 } else if (update_path == _T("navigation.state")) {
2322 wxString state = item["value"].GetString();
2323 if (state == _T("motoring")) {
2324 pTargetData->NavStatus = UNDERWAY_USING_ENGINE;
2325 } else if (state == _T("anchored")) {
2326 pTargetData->NavStatus = AT_ANCHOR;
2327 } else if (state == _T("not under command")) {
2328 pTargetData->NavStatus = NOT_UNDER_COMMAND;
2329 } else if (state == _T("restricted manouverability")) {
2330 pTargetData->NavStatus = RESTRICTED_MANOEUVRABILITY;
2331 } else if (state == _T("constrained by draft")) {
2332 pTargetData->NavStatus = CONSTRAINED_BY_DRAFT;
2333 } else if (state == _T("moored")) {
2334 pTargetData->NavStatus = MOORED;
2335 } else if (state == _T("aground")) {
2336 pTargetData->NavStatus = AGROUND;
2337 } else if (state == _T("fishing")) {
2338 pTargetData->NavStatus = FISHING;
2339 } else if (state == _T("sailing")) {
2340 pTargetData->NavStatus = UNDERWAY_SAILING;
2341 } else if (state == _T("hazardous material high speed")) {
2342 pTargetData->NavStatus = HSC;
2343 } else if (state == _T("hazardous material wing in ground")) {
2344 pTargetData->NavStatus = WIG;
2345 } else if (state == _T("ais-sart")) {
2346 pTargetData->NavStatus = RESERVED_14;
2347 } else {
2348 pTargetData->NavStatus = UNDEFINED;
2349 }
2350 } else if (update_path == _T("navigation.destination.commonName")) {
2351 const wxString &destination = item["value"].GetString();
2352 pTargetData->Destination[0] = '\0';
2353 strncpy(pTargetData->Destination, destination.c_str(),
2354 DESTINATION_LEN - 1);
2355 } else if (update_path == _T("navigation.specialManeuver")) {
2356 if (strcmp("not available", item["value"].GetString()) != 0 &&
2357 pTargetData->IMO < 1) {
2358 const wxString &bluesign = item["value"].GetString();
2359 if (_T("not engaged") == bluesign) {
2360 pTargetData->blue_paddle = 1;
2361 }
2362 if (_T("engaged") == bluesign) {
2363 pTargetData->blue_paddle = 2;
2364 }
2365 pTargetData->b_blue_paddle =
2366 pTargetData->blue_paddle == 2 ? true : false;
2367 }
2368 } else if (update_path == _T("sensors.ais.designatedAreaCode")) {
2369 if (item["value"].GetInt() == 200) { // European inland
2370 pTargetData->b_hasInlandDac = true;
2371 }
2372 } else if (update_path == _T("sensors.ais.functionalId")) {
2373 if (item["value"].GetInt() == 10 && pTargetData->b_hasInlandDac) {
2374 // "Inland ship static and voyage related data"
2375 pTargetData->b_isEuroInland = true;
2376 }
2377
2378 // METEO Data
2379 } else if (update_path == "environment.date") {
2380 wxString issued = item["value"].GetString();
2381 if (issued.Len()) {
2382 // Parse ISO 8601 date/time
2383 wxDateTime tz;
2384 ParseGPXDateTime(tz, issued);
2385 pTargetData->met_data.day = tz.GetDay();
2386 pTargetData->met_data.hour = tz.GetHour();
2387 pTargetData->met_data.minute = tz.GetMinute();
2388 }
2389 } else if (update_path == "environment.wind.averageSpeed" &&
2390 item["value"].IsNumber()) {
2391 pTargetData->met_data.wind_kn = MS2KNOTS(item["value"].GetDouble());
2392 } else if (update_path == "environment.wind.gust" &&
2393 item["value"].IsNumber()) {
2394 pTargetData->met_data.wind_gust_kn = MS2KNOTS(item["value"].GetDouble());
2395 } else if (update_path == "environment.wind.directionTrue" &&
2396 item["value"].IsNumber()) {
2397 pTargetData->met_data.wind_dir =
2398 GEODESIC_RAD2DEG(item["value"].GetDouble());
2399 } else if (update_path == "environment.wind.gustDirectionTrue" &&
2400 item["value"].IsNumber()) {
2401 pTargetData->met_data.wind_gust_dir =
2402 GEODESIC_RAD2DEG(item["value"].GetDouble());
2403 } else if (update_path == "environment.outside.temperature" &&
2404 item["value"].IsNumber()) {
2405 pTargetData->met_data.air_temp = KelvinToC(item["value"].GetDouble());
2406 } else if (update_path == "environment.outside.relativeHumidity" &&
2407 item["value"].IsNumber()) {
2408 pTargetData->met_data.rel_humid = item["value"].GetDouble();
2409 } else if (update_path == "environment.outside.dewPointTemperature" &&
2410 item["value"].IsNumber()) {
2411 pTargetData->met_data.dew_point = KelvinToC(item["value"].GetDouble());
2412 } else if (update_path == "environment.outside.pressure" &&
2413 item["value"].IsNumber()) {
2414 pTargetData->met_data.airpress =
2415 static_cast<int>(item["value"].GetDouble() / 100);
2416 } else if (update_path == "environment.water.level" &&
2417 item["value"].IsNumber()) {
2418 pTargetData->met_data.water_lev_dev = item["value"].GetDouble();
2419 } else if (update_path == "environment.water.current.drift" &&
2420 item["value"].IsNumber()) { // surfcurrspd
2421 pTargetData->met_data.current = MS2KNOTS(item["value"].GetDouble());
2422 } else if (update_path == "environment.water.current.set" &&
2423 item["value"].IsNumber()) { // surfcurrdir
2424 pTargetData->met_data.curr_dir =
2425 GEODESIC_RAD2DEG(item["value"].GetDouble());
2426 } else if (update_path == "environment.water.levelTendencyValue" &&
2427 item["value"].IsNumber()) {
2428 pTargetData->met_data.water_lev_trend =
2429 static_cast<int>(item["value"].GetDouble());
2430 } else if (update_path == "environment.water.levelTendency") {
2431 // Don't use this text we parse it ourself.
2432 } else if (update_path == "environment.water.waves.significantHeight" &&
2433 item["value"].IsNumber()) {
2434 pTargetData->met_data.wave_height = item["value"].GetDouble();
2435 } else if (update_path == "environment.water.waves.period" &&
2436 item["value"].IsNumber()) {
2437 pTargetData->met_data.wave_period =
2438 static_cast<int>(item["value"].GetDouble());
2439 } else if (update_path == "environment.water.waves.directionTrue" &&
2440 item["value"].IsNumber()) {
2441 pTargetData->met_data.wave_dir =
2442 GEODESIC_RAD2DEG(item["value"].GetDouble());
2443 } else if (update_path == "environment.water.swell.height" &&
2444 item["value"].IsNumber()) {
2445 pTargetData->met_data.swell_height = item["value"].GetDouble();
2446 } else if (update_path == "environment.water.swell.period" &&
2447 item["value"].IsNumber()) {
2448 pTargetData->met_data.swell_per =
2449 static_cast<int>(item["value"].GetDouble());
2450 } else if (update_path == "environment.water.swell.directionTrue" &&
2451 item["value"].IsNumber()) {
2452 pTargetData->met_data.swell_dir =
2453 GEODESIC_RAD2DEG(item["value"].GetDouble());
2454 } else if (update_path == "environment.water.temperature" &&
2455 item["value"].IsNumber()) {
2456 pTargetData->met_data.water_temp = KelvinToC(item["value"].GetDouble());
2457 } else if (update_path == "environment.water.salinity" &&
2458 item["value"].IsNumber()) {
2459 pTargetData->met_data.salinity = item["value"].GetDouble();
2460 } else if (update_path == "environment.water.ice") {
2461 // Don't use. We parse it ourself
2462 } else if (update_path == "environment.water.iceValue" &&
2463 item["value"].IsNumber()) {
2464 pTargetData->met_data.ice = static_cast<int>(item["value"].GetDouble());
2465 } else if (update_path == "environment.water.seaStateValue" &&
2466 item["value"].IsNumber()) {
2467 pTargetData->met_data.seastate =
2468 static_cast<int>(item["value"].GetDouble());
2469 } else if (update_path == "environment.water.seaState") {
2470 // This is the parsed (air!) Bf-scale. Don't use
2471 } else if (update_path == "environment.outside.precipitation") {
2472 // Don't use. We parse it ourself
2473 } else if (update_path == "environment.outside.precipitationValue" &&
2474 item["value"].IsNumber()) {
2475 pTargetData->met_data.precipitation =
2476 static_cast<int>(item["value"].GetDouble());
2477 } else if (update_path == "environment.outside.pressureTendencyValue" &&
2478 item["value"].IsNumber()) {
2479 pTargetData->met_data.airpress_tend =
2480 static_cast<int>(item["value"].GetDouble());
2481 } else if (update_path == "environment.outside.pressureTendency") {
2482 // Parsed value, don't use, we do it ourself
2483 } else if (update_path == "environment.outside.horizontalVisibility" &&
2484 item["value"].IsNumber()) {
2485 pTargetData->met_data.hor_vis =
2486 GEODESIC_METERS2NM(item["value"].GetDouble());
2487 } else if (update_path ==
2488 "environment.outside.horizontalVisibility.overRange") {
2489 pTargetData->met_data.hor_vis_GT = item["value"].GetBool();
2490 } else if (update_path.empty()) {
2491 if (item["value"].HasMember("name")) {
2492 const wxString &name = item["value"]["name"].GetString();
2493 strncpy(pTargetData->ShipName, name.c_str(), SHIP_NAME_LEN - 1);
2494 pTargetData->b_nameValid = true;
2495 pTargetData->MID = 123; // Indicates a name from SignalK
2496 } else if (item["value"].HasMember("registrations")) {
2497 const wxString &imo = item["value"]["registrations"]["imo"].GetString();
2498 pTargetData->IMO = wxAtoi(imo.Right(7));
2499 } else if (item["value"].HasMember("communication")) {
2500 const wxString &callsign =
2501 item["value"]["communication"]["callsignVhf"].GetString();
2502 strncpy(pTargetData->CallSign, callsign.c_str(), 7);
2503 }
2504 if (item["value"].HasMember("mmsi") &&
2505 1994 != (pTargetData->MMSI) / 100000) { // Meteo
2506 long mmsi;
2507 wxString tmp = item["value"]["mmsi"].GetString();
2508 if (tmp.ToLong(&mmsi)) {
2509 pTargetData->MMSI = mmsi;
2510
2511 if (97 == mmsi / 10000000) {
2512 pTargetData->Class = AIS_SART;
2513 }
2514 if (111 == mmsi / 1000000) {
2515 pTargetData->b_SarAircraftPosnReport = true;
2516 }
2517
2518 AISshipNameCache(pTargetData.get(), AISTargetNamesC, AISTargetNamesNC,
2519 mmsi);
2520 }
2521 }
2522 } else {
2523 wxLogMessage(wxString::Format(
2524 _T("** AisDecoder::updateItem: unhandled path %s"), update_path));
2525#if 1
2526 rapidjson::StringBuffer buffer;
2527 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
2528 item.Accept(writer);
2529 wxString msg(_T("update: "));
2530 msg.append(buffer.GetString());
2531 wxLogMessage(msg);
2532#endif
2533 }
2534 }
2535}
2536
2537//----------------------------------------------------------------------------------
2538// Decode a single AIVDO sentence to a Generic Position Report
2539//----------------------------------------------------------------------------------
2540AisError AisDecoder::DecodeSingleVDO(const wxString &str, GenericPosDatEx *pos,
2541 wxString *accumulator) {
2542 // Make some simple tests for validity
2543 if (str.Len() > 128) return AIS_NMEAVDX_TOO_LONG;
2544
2545 if (!NMEACheckSumOK(str)) return AIS_NMEAVDX_CHECKSUM_BAD;
2546
2547 if (!pos) return AIS_GENERIC_ERROR;
2548
2549 if (!accumulator) return AIS_GENERIC_ERROR;
2550
2551 // We only process AIVDO messages
2552 if (!str.Mid(1, 5).IsSameAs(_T("AIVDO"))) return AIS_GENERIC_ERROR;
2553
2554 // Use a tokenizer to pull out the first 4 fields
2555 wxStringTokenizer tkz(str, _T(","));
2556
2557 wxString token;
2558 token = tkz.GetNextToken(); // !xxVDx
2559
2560 token = tkz.GetNextToken();
2561 int nsentences = atoi(token.mb_str());
2562
2563 token = tkz.GetNextToken();
2564 int isentence = atoi(token.mb_str());
2565
2566 token = tkz.GetNextToken(); // skip 2 fields
2567 token = tkz.GetNextToken();
2568
2569 wxString string_to_parse;
2570 string_to_parse.Clear();
2571
2572 // Fill the output structure with all NANs
2573 pos->kLat = NAN;
2574 pos->kLon = NAN;
2575 pos->kCog = NAN;
2576 pos->kSog = NAN;
2577 pos->kHdt = NAN;
2578 pos->kVar = NAN;
2579 pos->kHdm = NAN;
2580
2581 // Simple case first
2582 // First and only part of a one-part sentence
2583 if ((1 == nsentences) && (1 == isentence)) {
2584 string_to_parse = tkz.GetNextToken(); // the encapsulated data
2585 }
2586
2587 else if (nsentences > 1) {
2588 if (1 == isentence) {
2589 *accumulator = tkz.GetNextToken(); // the encapsulated data
2590 }
2591
2592 else {
2593 accumulator->Append(tkz.GetNextToken());
2594 }
2595
2596 if (isentence == nsentences) { // ready to parse
2597 string_to_parse = *accumulator;
2598 }
2599 }
2600
2601 if (string_to_parse.IsEmpty() &&
2602 (nsentences > 1)) { // not ready, so return with NAN
2603 return AIS_INCOMPLETE_MULTIPART; // and non-zero return
2604 }
2605
2606 // Create the bit accessible string
2607 AisBitstring strbit(string_to_parse.mb_str());
2608
2609 // auto TargetData = std::make_unique<AisTargetData>(
2610 // *AisTargetDataMaker::GetInstance().GetTargetData());
2611
2612 auto TargetData = AisTargetDataMaker::GetInstance().GetTargetData();
2613
2614 bool bdecode_result = Parse_VDXBitstring(&strbit, TargetData);
2615
2616 if (bdecode_result) {
2617 switch (TargetData->MID) {
2618 case 1:
2619 case 2:
2620 case 3:
2621 case 18: {
2622 if (!TargetData->b_positionDoubtful) {
2623 pos->kLat = TargetData->Lat;
2624 pos->kLon = TargetData->Lon;
2625 } else {
2626 pos->kLat = NAN;
2627 pos->kLon = NAN;
2628 }
2629
2630 if (TargetData->COG == 360.0)
2631 pos->kCog = NAN;
2632 else
2633 pos->kCog = TargetData->COG;
2634
2635 if (TargetData->SOG > 102.2)
2636 pos->kSog = NAN;
2637 else
2638 pos->kSog = TargetData->SOG;
2639
2640 if ((int)TargetData->HDG == 511)
2641 pos->kHdt = NAN;
2642 else
2643 pos->kHdt = TargetData->HDG;
2644
2645 // VDO messages do not contain variation or magnetic heading
2646 pos->kVar = NAN;
2647 pos->kHdm = NAN;
2648 break;
2649 }
2650 default:
2651 return AIS_GENERIC_ERROR; // unrecognised sentence
2652 }
2653
2654 return AIS_NoError;
2655 } else
2656 return AIS_GENERIC_ERROR;
2657}
2658
2659//----------------------------------------------------------------------------------------
2660// Decode NMEA VDM/VDO/FRPOS/DSCDSE/TTM/TLL/OSD/RSD/TLB/WPL sentence to AIS
2661// Target(s)
2662//----------------------------------------------------------------------------------------
2663
2664AisError AisDecoder::DecodeN0183(const wxString &str) {
2665 AisError ret = AIS_GENERIC_ERROR;
2666 wxString string_to_parse;
2667
2668 double gpsg_lat, gpsg_lon, gpsg_mins, gpsg_degs;
2669 double gpsg_cog, gpsg_sog, gpsg_utc_time;
2670 int gpsg_utc_hour = 0;
2671 int gpsg_utc_min = 0;
2672 int gpsg_utc_sec = 0;
2673 char gpsg_name_str[21];
2674 wxString gpsg_date;
2675
2676 bool bdecode_result = false;
2677
2678 int gpsg_mmsi = 0;
2679 int arpa_mmsi = 0;
2680 int aprs_mmsi = 0;
2681 unsigned mmsi = 0;
2682
2683 long arpa_tgt_num = 0;
2684 double arpa_sog = 0.;
2685 double arpa_cog = 0.;
2686 double arpa_lat = 0.;
2687 double arpa_lon = 0.;
2688 double arpa_dist = 0.;
2689 double arpa_brg = 0.;
2690 wxString arpa_brgunit;
2691 wxString arpa_status;
2692 wxString arpa_distunit;
2693 wxString arpa_cogunit;
2694 double arpa_mins, arpa_degs;
2695 double arpa_utc_time;
2696 int arpa_utc_hour = 0;
2697 int arpa_utc_min = 0;
2698 int arpa_utc_sec = 0;
2699 char arpa_name_str[21];
2700 bool arpa_lost = true;
2701 bool arpa_nottracked = false;
2702
2703 double aprs_lat = 0.;
2704 double aprs_lon = 0.;
2705 char aprs_name_str[21];
2706 double aprs_mins, aprs_degs;
2707
2708 std::shared_ptr<AisTargetData> pTargetData = nullptr;
2709 std::shared_ptr<AisTargetData> pStaleTarget = nullptr;
2710 bool bnewtarget = false;
2711 int last_report_ticks;
2712
2713 // Make some simple tests for validity
2714
2715 if (str.Len() > 128) return AIS_NMEAVDX_TOO_LONG;
2716
2717 if (!NMEACheckSumOK(str)) {
2718 return AIS_NMEAVDX_CHECKSUM_BAD;
2719 }
2720 if (str.Mid(1, 2).IsSameAs(_T("CD"))) {
2721 ProcessDSx(str);
2722 return AIS_NoError;
2723 } else if (str.Mid(3, 3).IsSameAs(_T("TTM"))) {
2724 //$--TTM,xx,x.x,x.x,a,x.x,x.x,a,x.x,x.x,a,c--c,a,a*hh <CR><LF>
2725 // or
2726 //$--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>
2727 wxStringTokenizer tkz(str, ",*");
2728
2729 wxString token;
2730 token = tkz.GetNextToken(); // Sentence (xxTTM)
2731 token = tkz.GetNextToken(); // 1) Target Number
2732 token.ToLong(&arpa_tgt_num);
2733 token = tkz.GetNextToken(); // 2)Target Distance
2734 token.ToDouble(&arpa_dist);
2735 token = tkz.GetNextToken(); // 3) Bearing from own ship
2736 token.ToDouble(&arpa_brg);
2737 arpa_brgunit = tkz.GetNextToken(); // 4) Bearing Units
2738 if (arpa_brgunit == _T("R")) {
2739 if (std::isnan(arpa_ref_hdg)) {
2740 if (!std::isnan(gHdt))
2741 arpa_brg += gHdt;
2742 else
2743 arpa_brg += gCog;
2744 } else
2745 arpa_brg += arpa_ref_hdg;
2746 if (arpa_brg >= 360.) arpa_brg -= 360.;
2747 }
2748 token = tkz.GetNextToken(); // 5) Target speed
2749 token.ToDouble(&arpa_sog);
2750 token = tkz.GetNextToken(); // 6) Target Course
2751 token.ToDouble(&arpa_cog);
2752 arpa_cogunit = tkz.GetNextToken(); // 7) Course Units
2753 if (arpa_cogunit == _T("R")) {
2754 if (std::isnan(arpa_ref_hdg)) {
2755 if (!std::isnan(gHdt))
2756 arpa_cog += gHdt;
2757 else
2758 arpa_cog += gCog;
2759 } else
2760 arpa_cog += arpa_ref_hdg;
2761 if (arpa_cog >= 360.) arpa_cog -= 360.;
2762 }
2763 token = tkz.GetNextToken(); // 8) Distance of closest-point-of-approach
2764 token = tkz.GetNextToken(); // 9) Time until closest-point-of-approach "-"
2765 // means increasing
2766 arpa_distunit = tkz.GetNextToken(); // 10)Speed/ dist unit
2767 token = tkz.GetNextToken(); // 11) Target name
2768 if (token == wxEmptyString)
2769 token = wxString::Format(_T("ARPA %ld"), arpa_tgt_num);
2770 int len = wxMin(token.Length(), 20);
2771 strncpy(arpa_name_str, token.mb_str(), len);
2772 arpa_name_str[len] = 0;
2773 arpa_status = tkz.GetNextToken(); // 12) Target Status
2774 if (arpa_status != _T( "L" )) {
2775 arpa_lost = false;
2776 } else if (arpa_status != wxEmptyString)
2777 arpa_nottracked = true;
2778 wxString arpa_reftarget = tkz.GetNextToken(); // 13) Reference Target
2779 if (tkz.HasMoreTokens()) {
2780 token = tkz.GetNextToken();
2781 token.ToDouble(&arpa_utc_time);
2782 arpa_utc_hour = (int)(arpa_utc_time / 10000.0);
2783 arpa_utc_min = (int)(arpa_utc_time / 100.0) - arpa_utc_hour * 100;
2784 arpa_utc_sec =
2785 (int)arpa_utc_time - arpa_utc_hour * 10000 - arpa_utc_min * 100;
2786 } else {
2787 arpa_utc_hour = wxDateTime::Now().ToUTC().GetHour();
2788 arpa_utc_min = wxDateTime::Now().ToUTC().GetMinute();
2789 arpa_utc_sec = wxDateTime::Now().ToUTC().GetSecond();
2790 }
2791
2792 if (arpa_distunit == _T("K")) {
2793 arpa_dist = fromUsrDistance(arpa_dist, DISTANCE_KM, g_iDistanceFormat);
2794 arpa_sog = fromUsrSpeed(arpa_sog, SPEED_KMH, g_iSpeedFormat);
2795 } else if (arpa_distunit == _T("S")) {
2796 arpa_dist = fromUsrDistance(arpa_dist, DISTANCE_MI, g_iDistanceFormat);
2797 arpa_sog = fromUsrSpeed(arpa_sog, SPEED_MPH, g_iSpeedFormat);
2798 }
2799
2800 mmsi = arpa_mmsi = 199200000 + arpa_tgt_num;
2801 // 199 is INMARSAT-A MID, should not occur ever in AIS
2802 // stream + we make sure we are out of the hashes for
2803 // GPSGate buddies by being above 1992*
2804 } else if (str.Mid(3, 3).IsSameAs(_T("TLL"))) {
2805 //$--TLL,xx,llll.lll,a,yyyyy.yyy,a,c--c,hhmmss.ss,a,a*hh<CR><LF>
2806 //"$RATLL,01,5603.370,N,01859.976,E,ALPHA,015200.36,T,*75\r\n"
2807 wxStringTokenizer tkz(str, ",*");
2808
2809 wxString token;
2810 wxString aprs_tll_str = tkz.GetNextToken(); // Sentence (xxTLL)
2811 token = tkz.GetNextToken(); // 1) Target number 00 - 99
2812 token.ToLong(&arpa_tgt_num);
2813 token = tkz.GetNextToken(); // 2) Latitude, N/S
2814 token.ToDouble(&arpa_lat);
2815 arpa_degs = (int)(arpa_lat / 100.0);
2816 arpa_mins = arpa_lat - arpa_degs * 100.0;
2817 arpa_lat = arpa_degs + arpa_mins / 60.0;
2818 token = tkz.GetNextToken(); // hemisphere N or S
2819 if (token.Mid(0, 1).Contains(_T("S")) == true ||
2820 token.Mid(0, 1).Contains(_T("s")) == true)
2821 arpa_lat = 0. - arpa_lat;
2822 token = tkz.GetNextToken(); // 3) Longitude, E/W
2823 token.ToDouble(&arpa_lon);
2824 arpa_degs = (int)(arpa_lon / 100.0);
2825 arpa_mins = arpa_lon - arpa_degs * 100.0;
2826 arpa_lon = arpa_degs + arpa_mins / 60.0;
2827 token = tkz.GetNextToken(); // hemisphere E or W
2828 if (token.Mid(0, 1).Contains(_T("W")) == true ||
2829 token.Mid(0, 1).Contains(_T("w")) == true)
2830 arpa_lon = 0. - arpa_lon;
2831 token = tkz.GetNextToken(); // 4) Target name
2832 if (token == wxEmptyString)
2833 token = wxString::Format(_T("ARPA %d"), arpa_tgt_num);
2834 int len = wxMin(token.Length(), 20);
2835 strncpy(arpa_name_str, token.mb_str(), len);
2836 arpa_name_str[len] = 0;
2837 token = tkz.GetNextToken(); // 5) UTC of data
2838 token.ToDouble(&arpa_utc_time);
2839 arpa_utc_hour = (int)(arpa_utc_time / 10000.0);
2840 arpa_utc_min = (int)(arpa_utc_time / 100.0) - arpa_utc_hour * 100;
2841 arpa_utc_sec =
2842 (int)arpa_utc_time - arpa_utc_hour * 10000 - arpa_utc_min * 100;
2843 arpa_status =
2844 tkz.GetNextToken(); // 6) Target status: L = lost,tracked
2845 // target has beenlost Q = query,target in
2846 // the process of acquisition T = tracking
2847 if (arpa_status != _T("L"))
2848 arpa_lost = false;
2849 else if (arpa_status != wxEmptyString)
2850 arpa_nottracked = true;
2851 wxString arpa_reftarget = tkz.GetNextToken(); // 7) Reference target=R,null
2852 mmsi = arpa_mmsi = 199200000 + arpa_tgt_num;
2853 // 199 is INMARSAT-A MID, should not occur ever in AIS
2854 // stream + we make sure we are out of the hashes for
2855 // GPSGate buddies by being above 1992*
2856 } else if (str.Mid(3, 3).IsSameAs(_T("OSD"))) {
2857 //$--OSD,x.x,A,x.x,a,x.x,a,x.x,x.x,a*hh <CR><LF>
2858 wxStringTokenizer tkz(str, _T(",*"));
2859
2860 wxString token;
2861 token = tkz.GetNextToken(); // Sentence (xxOSD)
2862 token = tkz.GetNextToken(); // 1) Heading (true)
2863 token.ToDouble(&arpa_ref_hdg);
2864 // 2) speed
2865 // 3) Vessel Course, degrees True
2866 // 4) Course Reference, B/M/W/R/P (see note)
2867 // 5) Vessel Speed
2868 // 6) Speed Reference, B/M/W/R/P (see note)
2869 // 7) Vessel Set, degrees True - Manually entered
2870 // 8) Vessel drift (speed) - Manually entered
2871 // 9) Speed Units K = km/h; N = Knots; S = statute miles/h
2872
2873 } else if (g_bWplUsePosition && str.Mid(3, 3).IsSameAs(_T("WPL"))) {
2874 //** $--WPL,llll.ll,a,yyyyy.yy,a,c--c*hh<CR><LF>
2875 wxStringTokenizer tkz(str, ",*");
2876
2877 wxString token;
2878 token = tkz.GetNextToken(); // Sentence (xxWPL)
2879 token = tkz.GetNextToken(); // 1) Latitude, N/S
2880 token.ToDouble(&aprs_lat);
2881 aprs_degs = (int)(aprs_lat / 100.0);
2882 aprs_mins = aprs_lat - aprs_degs * 100.0;
2883 aprs_lat = aprs_degs + aprs_mins / 60.0;
2884 token = tkz.GetNextToken(); // 2) hemisphere N or S
2885 if (token.Mid(0, 1).Contains(_T("S")) == true ||
2886 token.Mid(0, 1).Contains(_T("s")) == true)
2887 aprs_lat = 0. - aprs_lat;
2888 token = tkz.GetNextToken(); // 3) Longitude, E/W
2889 token.ToDouble(&aprs_lon);
2890 aprs_degs = (int)(aprs_lon / 100.0);
2891 aprs_mins = aprs_lon - aprs_degs * 100.0;
2892 aprs_lon = aprs_degs + aprs_mins / 60.0;
2893 token = tkz.GetNextToken(); // 4) hemisphere E or W
2894 if (token.Mid(0, 1).Contains(_T("W")) == true ||
2895 token.Mid(0, 1).Contains(_T("w")) == true)
2896 aprs_lon = 0. - aprs_lon;
2897 token = tkz.GetNextToken(); // 5) Target name
2898 int len = wxMin(token.Length(), 20);
2899 strncpy(aprs_name_str, token.mb_str(), len + 1);
2900 if (0 == g_WplAction) { // APRS position reports
2901 int i, hash = 0;
2902 aprs_name_str[len] = 0;
2903 for (i = 0; i < len; i++) {
2904 hash = hash * 10;
2905 hash += (int)(aprs_name_str[i]);
2906 while (hash >= 100000) hash = hash / 100000;
2907 }
2908 mmsi = aprs_mmsi = 199300000 + hash;
2909 // 199 is INMARSAT-A MID, should not occur ever in AIS stream +
2910 // we make sure we are out of the hashes for GPSGate buddies
2911 // and ARPA by being above 1993*
2912 } else if (1 == g_WplAction) { // Create mark
2913 auto *pWP = new RoutePoint(aprs_lat, aprs_lon, wxEmptyString,
2914 aprs_name_str, wxEmptyString, false);
2915 pWP->m_bIsolatedMark = true;
2916 InsertWpt(pWP, true);
2917 }
2918 } else if (str.Mid(1, 5).IsSameAs(_T("FRPOS"))) {
2919 // parse a GpsGate Position message $FRPOS,.....
2920
2921 // Use a tokenizer to pull out the first 9 fields
2922 wxStringTokenizer tkz(str, ",*");
2923
2924 wxString token;
2925 token = tkz.GetNextToken(); // !$FRPOS
2926
2927 token = tkz.GetNextToken(); // latitude DDMM.MMMM
2928 token.ToDouble(&gpsg_lat);
2929 gpsg_degs = (int)(gpsg_lat / 100.0);
2930 gpsg_mins = gpsg_lat - gpsg_degs * 100.0;
2931 gpsg_lat = gpsg_degs + gpsg_mins / 60.0;
2932
2933 token = tkz.GetNextToken(); // hemisphere N or S
2934 if (token.Mid(0, 1).Contains(_T("S")) == true ||
2935 token.Mid(0, 1).Contains(_T("s")) == true)
2936 gpsg_lat = 0. - gpsg_lat;
2937
2938 token = tkz.GetNextToken(); // longitude DDDMM.MMMM
2939 token.ToDouble(&gpsg_lon);
2940 gpsg_degs = (int)(gpsg_lon / 100.0);
2941 gpsg_mins = gpsg_lon - gpsg_degs * 100.0;
2942 gpsg_lon = gpsg_degs + gpsg_mins / 60.0;
2943
2944 token = tkz.GetNextToken(); // hemisphere E or W
2945 if (token.Mid(0, 1).Contains(_T("W")) == true ||
2946 token.Mid(0, 1).Contains(_T("w")) == true)
2947 gpsg_lon = 0. - gpsg_lon;
2948
2949 token = tkz.GetNextToken(); // altitude AA.a
2950 // token.toDouble(&gpsg_alt);
2951
2952 token = tkz.GetNextToken(); // speed over ground SSS.SS knots
2953 token.ToDouble(&gpsg_sog);
2954
2955 token = tkz.GetNextToken(); // heading over ground HHH.hh degrees
2956 token.ToDouble(&gpsg_cog);
2957
2958 token = tkz.GetNextToken(); // date DDMMYY
2959 gpsg_date = token;
2960
2961 token = tkz.GetNextToken(); // time UTC hhmmss.dd
2962 token.ToDouble(&gpsg_utc_time);
2963 gpsg_utc_hour = (int)(gpsg_utc_time / 10000.0);
2964 gpsg_utc_min = (int)(gpsg_utc_time / 100.0) - gpsg_utc_hour * 100;
2965 gpsg_utc_sec =
2966 (int)gpsg_utc_time - gpsg_utc_hour * 10000 - gpsg_utc_min * 100;
2967
2968 // now comes the name, followed by in * and NMEA checksum
2969
2970 token = tkz.GetNextToken();
2971 int i, len, hash = 0;
2972 len = wxMin(wxStrlen(token), 20);
2973 strncpy(gpsg_name_str, token.mb_str(), len);
2974 gpsg_name_str[len] = 0;
2975 for (i = 0; i < len; i++) {
2976 hash = hash * 10;
2977 hash += (int)(token[i]);
2978 while (hash >= 100000) hash = hash / 100000;
2979 }
2980 // 199 is INMARSAT-A MID, should not occur ever in AIS stream
2981 gpsg_mmsi = 199000000 + hash;
2982 mmsi = gpsg_mmsi;
2983 } else if (!str.Mid(3, 2).IsSameAs(_T("VD"))) {
2984 return AIS_NMEAVDX_BAD;
2985 }
2986
2987 // OK, looks like the sentence is OK
2988
2989 // Use a tokenizer to pull out the first 4 fields
2990 string_to_parse.Clear();
2991
2992 if (str.Mid(3, 2).IsSameAs(_T("VD"))) {
2993 wxStringTokenizer tkz(str, ",");
2994
2995 wxString token;
2996 token = tkz.GetNextToken(); // !xxVDx
2997
2998 token = tkz.GetNextToken();
2999 nsentences = atoi(token.mb_str());
3000
3001 token = tkz.GetNextToken();
3002 isentence = atoi(token.mb_str());
3003
3004 token = tkz.GetNextToken();
3005 long lsequence_id = 0;
3006 token.ToLong(&lsequence_id);
3007
3008 token = tkz.GetNextToken();
3009 long lchannel;
3010 token.ToLong(&lchannel);
3011 // Now, some decisions
3012
3013 // Simple case first
3014 // First and only part of a one-part sentence
3015 if ((1 == nsentences) && (1 == isentence)) {
3016 string_to_parse = tkz.GetNextToken(); // the encapsulated data
3017 }
3018
3019 else if (nsentences > 1) {
3020 if (1 == isentence) {
3021 sentence_accumulator = tkz.GetNextToken(); // the encapsulated data
3022 }
3023
3024 else {
3025 sentence_accumulator += tkz.GetNextToken();
3026 }
3027
3028 if (isentence == nsentences) {
3029 string_to_parse = sentence_accumulator;
3030 }
3031 }
3032 }
3033
3034 if (mmsi || (!string_to_parse.IsEmpty() &&
3035 (string_to_parse.Len() < AIS_MAX_MESSAGE_LEN))) {
3036 // Create the bit accessible string
3037 wxCharBuffer abuf = string_to_parse.ToUTF8();
3038 if (!abuf.data()) // badly formed sentence?
3039 return AIS_GENERIC_ERROR;
3040
3041 AisBitstring strbit(abuf.data());
3042
3043 // Extract the MMSI
3044 if (!mmsi) mmsi = strbit.GetInt(9, 30);
3045 long mmsi_long = mmsi;
3046
3047 // Ais8_001_31 || ais8_367_33 (class AIS_METEO) test for a new mmsi ID
3048 int origin_mmsi = 0;
3049 int messID = strbit.GetInt(1, 6);
3050 int dac = strbit.GetInt(41, 10);
3051 int fi = strbit.GetInt(51, 6);
3052 if (messID == 8) {
3053 int met_lon, met_lat;
3054 if (dac == 001 && fi == 31) {
3055 origin_mmsi = mmsi;
3056 met_lon = strbit.GetInt(57, 25);
3057 met_lat = strbit.GetInt(82, 24);
3058 mmsi = AisMeteoNewMmsi(mmsi, met_lat, met_lon, 25, 0);
3059 mmsi_long = mmsi;
3060
3061 } else if (dac == 367 && fi == 33) { // ais8_367_33
3062 // Check for a valid message size before further handling
3063 const int size = strbit.GetBitCount();
3064 if (size < 168) return AIS_GENERIC_ERROR;
3065 const int startb = 56;
3066 const int slot_size = 112;
3067 const int extra_bits = (size - startb) % slot_size;
3068 if (extra_bits > 0) return AIS_GENERIC_ERROR;
3069
3070 int mes_type = strbit.GetInt(57, 4);
3071 int site_ID = strbit.GetInt(77, 7);
3072 if (mes_type == 0) { // Location
3073 origin_mmsi = mmsi;
3074 met_lon = strbit.GetInt(90, 28);
3075 met_lat = strbit.GetInt(118, 27);
3076 mmsi = AisMeteoNewMmsi(mmsi, met_lat, met_lon, 28, site_ID);
3077 mmsi_long = mmsi;
3078 } else { // Other messsage types without position.
3079 // We need a previously received type 0, position message
3080 // to get use of any sensor report.
3081 int x_mmsi = AisMeteoNewMmsi(mmsi, 91, 181, 0, site_ID);
3082 if (x_mmsi) {
3083 origin_mmsi = mmsi;
3084 mmsi = x_mmsi;
3085 mmsi_long = mmsi;
3086 } else // So far no use for this report.
3087 return AIS_GENERIC_ERROR;
3088 }
3089 }
3090 }
3091
3092 // Check for own ship mmsi. It's not a valid AIS target.
3093 if (mmsi == g_OwnShipmmsi) return AIS_GENERIC_ERROR;
3094
3095 // Search the current AISTargetList for an MMSI match
3096 auto it = AISTargetList.find(mmsi);
3097 if (it == AISTargetList.end()) // not found
3098 {
3099 pTargetData = AisTargetDataMaker::GetInstance().GetTargetData();
3100 bnewtarget = true;
3101 m_n_targets++;
3102
3103 if (origin_mmsi) { // New mmsi allocated for a Meteo station
3104 pTargetData->MMSI = mmsi;
3105 pTargetData->met_data.original_mmsi = origin_mmsi;
3106 }
3107 } else {
3108 pTargetData = it->second; // find current entry
3109
3110 if (!bnewtarget)
3111 pStaleTarget = pTargetData; // save a pointer to stale data
3112 if (origin_mmsi) { // Meteo point
3113 pTargetData->MMSI = mmsi;
3114 pTargetData->met_data.original_mmsi = origin_mmsi;
3115 }
3116 }
3117 for (unsigned int i = 0; i < g_MMSI_Props_Array.GetCount(); i++) {
3118 MmsiProperties *props = g_MMSI_Props_Array[i];
3119 if (mmsi == static_cast<unsigned>(props->MMSI)) {
3120 // Check if this target has a dedicated tracktype
3121 if (TRACKTYPE_NEVER == props->TrackType) {
3122 pTargetData->b_show_track = false;
3123 } else if (TRACKTYPE_ALWAYS == props->TrackType) {
3124 pTargetData->b_show_track = true;
3125 }
3126
3127 // Check to see if this MMSI has been configured to be ignored
3128 // completely...
3129 if (props->m_bignore) return AIS_NoError;
3130 // Check to see if this MMSI wants VDM translated to VDO or whether we
3131 // want to persist it's track...
3132 else if (props->m_bVDM) {
3133 // Only single line VDM messages to be translated
3134 if (str.Mid(3, 9).IsSameAs(wxT("VDM,1,1,,"))) {
3135 int message_ID = strbit.GetInt(1, 6); // Parse on message ID
3136 // Only translate the dynamic positionreport messages (1, 2, 3 or
3137 // 18)
3138 if ((message_ID <= 3) || (message_ID == 18)) {
3139 // set OwnShip to prevent target from being drawn
3140 pTargetData->b_OwnShip = true;
3141 // Rename nmea sentence to AIVDO and calc a new checksum
3142 wxString aivdostr = str;
3143 aivdostr.replace(1, 5, "AIVDO");
3144 unsigned char calculated_checksum = 0;
3145 wxString::iterator j;
3146 for (j = aivdostr.begin() + 1; j != aivdostr.end() && *j != '*';
3147 ++j)
3148 calculated_checksum ^= static_cast<unsigned char>(*j);
3149 // if i is not at least 3 positons befoere end, there is no
3150 // checksum added so also no need to add one now.
3151 if (j <= aivdostr.end() - 3)
3152 aivdostr.replace(
3153 j + 1, j + 3,
3154 wxString::Format(_("%02X"), calculated_checksum));
3155
3156 gps_watchdog_timeout_ticks =
3157 60; // increase watchdog time up to 1 minute
3158 // add the changed sentence into nmea message system
3159 std::string full_sentence = aivdostr.ToStdString();
3160 auto address = std::make_shared<NavAddr0183>("virtual");
3161 // We notify based on full message, including the Talker ID
3162 // Notify message listener
3163 auto msg = std::make_shared<const Nmea0183Msg>(
3164 "AIVDO", full_sentence, address);
3165 NavMsgBus::GetInstance().Notify(std::move(msg));
3166 }
3167 }
3168 return AIS_NoError;
3169 } else
3170 break;
3171 }
3172 }
3173
3174 // Grab the stale targets's last report time
3175 wxDateTime now = wxDateTime::Now();
3176 now.MakeGMT();
3177
3178 if (pStaleTarget)
3179 last_report_ticks = pStaleTarget->PositionReportTicks;
3180 else
3181 last_report_ticks = now.GetTicks();
3182
3183 // Delete the stale AIS Target selectable point
3184 if (pStaleTarget)
3185 pSelectAIS->DeleteSelectablePoint((void *)mmsi_long, SELTYPE_AISTARGET);
3186
3187 if (pTargetData) {
3188 if (gpsg_mmsi) {
3189 pTargetData->LastPositionReportTicks = pTargetData->PositionReportTicks;
3190 pTargetData->PositionReportTicks = now.GetTicks();
3191 pTargetData->StaticReportTicks = now.GetTicks();
3192 pTargetData->m_utc_hour = gpsg_utc_hour;
3193 pTargetData->m_utc_min = gpsg_utc_min;
3194 pTargetData->m_utc_sec = gpsg_utc_sec;
3195 pTargetData->m_date_string = gpsg_date;
3196 pTargetData->MMSI = gpsg_mmsi;
3197 pTargetData->NavStatus = 0; // underway
3198 pTargetData->Lat = gpsg_lat;
3199 pTargetData->Lon = gpsg_lon;
3200 pTargetData->b_positionOnceValid = true;
3201 pTargetData->COG = gpsg_cog;
3202 pTargetData->SOG = gpsg_sog;
3203 pTargetData->ShipType = 52; // buddy
3204 pTargetData->Class = AIS_GPSG_BUDDY;
3205 memcpy(pTargetData->ShipName, gpsg_name_str, sizeof(gpsg_name_str));
3206 pTargetData->b_nameValid = true;
3207 pTargetData->b_active = true;
3208 pTargetData->b_lost = false;
3209
3210 bdecode_result = true;
3211 } else if (arpa_mmsi) {
3212 pTargetData->m_utc_hour = arpa_utc_hour;
3213 pTargetData->m_utc_min = arpa_utc_min;
3214 pTargetData->m_utc_sec = arpa_utc_sec;
3215 pTargetData->MMSI = arpa_mmsi;
3216 pTargetData->NavStatus = 15; // undefined
3217 if (str.Mid(3, 3).IsSameAs(_T("TLL"))) {
3218 if (!bnewtarget) {
3219 int age_of_last =
3220 (now.GetTicks() - pTargetData->PositionReportTicks);
3221 if (age_of_last > 0) {
3222 ll_gc_ll_reverse(pTargetData->Lat, pTargetData->Lon, arpa_lat,
3223 arpa_lon, &pTargetData->COG, &pTargetData->SOG);
3224 pTargetData->SOG = pTargetData->SOG * 3600 / age_of_last;
3225 }
3226 }
3227 pTargetData->Lat = arpa_lat;
3228 pTargetData->Lon = arpa_lon;
3229 } else if (str.Mid(3, 3).IsSameAs(_T("TTM"))) {
3230 if (arpa_dist != 0.) // Not a new or turned off target
3231 ll_gc_ll(gLat, gLon, arpa_brg, arpa_dist, &pTargetData->Lat,
3232 &pTargetData->Lon);
3233 else
3234 arpa_lost = true;
3235 pTargetData->COG = arpa_cog;
3236 pTargetData->SOG = arpa_sog;
3237 }
3238 pTargetData->LastPositionReportTicks = pTargetData->PositionReportTicks;
3239 pTargetData->PositionReportTicks = now.GetTicks();
3240 pTargetData->StaticReportTicks = now.GetTicks();
3241 pTargetData->b_positionOnceValid = true;
3242 pTargetData->ShipType = 55; // arpa
3243 pTargetData->Class = AIS_ARPA;
3244
3245 memcpy(pTargetData->ShipName, arpa_name_str, sizeof(arpa_name_str));
3246 if (arpa_status != _T("Q"))
3247 pTargetData->b_nameValid = true;
3248 else
3249 pTargetData->b_nameValid = false;
3250 pTargetData->b_active = !arpa_lost;
3251 pTargetData->b_lost = arpa_nottracked;
3252
3253 bdecode_result = true;
3254 } else if (aprs_mmsi) {
3255 pTargetData->m_utc_hour = now.GetHour();
3256 pTargetData->m_utc_min = now.GetMinute();
3257 pTargetData->m_utc_sec = now.GetSecond();
3258 pTargetData->MMSI = aprs_mmsi;
3259 pTargetData->NavStatus = 15; // undefined
3260 if (!bnewtarget) {
3261 int age_of_last = (now.GetTicks() - pTargetData->PositionReportTicks);
3262 if (age_of_last > 0) {
3263 ll_gc_ll_reverse(pTargetData->Lat, pTargetData->Lon, aprs_lat,
3264 aprs_lon, &pTargetData->COG, &pTargetData->SOG);
3265 pTargetData->SOG = pTargetData->SOG * 3600 / age_of_last;
3266 }
3267 }
3268 pTargetData->LastPositionReportTicks = pTargetData->PositionReportTicks;
3269 pTargetData->PositionReportTicks = now.GetTicks();
3270 pTargetData->StaticReportTicks = now.GetTicks();
3271 pTargetData->Lat = aprs_lat;
3272 pTargetData->Lon = aprs_lon;
3273 pTargetData->b_positionOnceValid = true;
3274 pTargetData->ShipType = 56; // aprs
3275 pTargetData->Class = AIS_APRS;
3276 memcpy(pTargetData->ShipName, aprs_name_str, sizeof(aprs_name_str));
3277 pTargetData->b_nameValid = true;
3278 pTargetData->b_active = true;
3279 pTargetData->b_lost = false;
3280
3281 bdecode_result = true;
3282 } else {
3283 // The normal Plain-Old AIS target code path....
3284 bdecode_result =
3285 Parse_VDXBitstring(&strbit, pTargetData); // Parse the new data
3286 }
3287
3288 // Catch mmsi properties like track, persistent track, follower.
3289 getMmsiProperties(pTargetData);
3290
3291 // Update the most recent report period
3292 pTargetData->RecentPeriod =
3293 pTargetData->PositionReportTicks - last_report_ticks;
3294 }
3295 ret = AIS_NoError;
3296 } else {
3297 ret = AIS_Partial; // accumulating parts of a multi-sentence message
3298 pTargetData = nullptr;
3299 }
3300
3301 if (pTargetData) {
3302 // pTargetData is valid, either new or existing. Commit to GUI
3303 CommitAISTarget(pTargetData, str, bdecode_result, bnewtarget);
3304 }
3305
3306 n_msgs++;
3307#ifdef AIS_DEBUG
3308 if ((n_msgs % 10000) == 0)
3309 printf("n_msgs %10d m_n_targets: %6d n_msg1: %10d n_msg5+24: %10d \n",
3310 n_msgs, m_n_targets, n_msg1, n_msg5 + n_msg24);
3311#endif
3312
3313 return ret;
3314}
3315
3316void AisDecoder::CommitAISTarget(
3317 const std::shared_ptr<AisTargetData> &pTargetData, const wxString &str,
3318 bool message_valid, bool new_target) {
3319 m_pLatestTargetData = pTargetData;
3320
3321 if (!str.IsEmpty()) { // NMEA0183 message
3322 if (str.Mid(3, 3).IsSameAs(_T("VDO")))
3323 pTargetData->b_OwnShip = true;
3324 else
3325 pTargetData->b_OwnShip = false;
3326 }
3327
3328 if (!pTargetData->b_OwnShip) {
3329 // set mmsi-props to default values
3330 if (0 == m_persistent_tracks.count(pTargetData->MMSI)) {
3331 // Normal target
3332 pTargetData->b_PersistTrack = false;
3333 // Or first decode for this target
3334 for (unsigned int i = 0; i < g_MMSI_Props_Array.GetCount(); i++) {
3335 if (pTargetData->MMSI == g_MMSI_Props_Array[i]->MMSI) {
3336 MmsiProperties *props = g_MMSI_Props_Array[i];
3337 pTargetData->b_mPropPersistTrack = props->m_bPersistentTrack;
3338 break;
3339 }
3340 }
3341 } else {
3342 // The track persistency enabled in the query window or mmsi-props
3343 }
3344 pTargetData->b_NoTrack = false;
3345 }
3346
3347 // If the message was decoded correctly
3348 // Update the AIS Target information
3349 if (message_valid) {
3350 // Print to name cache only if not mmsi = 0
3351 if (pTargetData->MMSI) {
3352 AISshipNameCache(pTargetData.get(), AISTargetNamesC, AISTargetNamesNC,
3353 pTargetData->MMSI);
3354 }
3355 AISTargetList[pTargetData->MMSI] =
3356 pTargetData; // update the hash table entry
3357
3358 if (!pTargetData->area_notices.empty()) {
3359 auto it = AIS_AreaNotice_Sources.find(pTargetData->MMSI);
3360 if (it == AIS_AreaNotice_Sources.end())
3361 AIS_AreaNotice_Sources[pTargetData->MMSI] = pTargetData;
3362 }
3363
3364 // If this is not an ownship message, update the AIS Target in the
3365 // Selectable list, and update the CPA info
3366 if (!pTargetData->b_OwnShip) {
3367 if (pTargetData->b_positionOnceValid) {
3368 long mmsi_long = pTargetData->MMSI;
3369 SelectItem *pSel = pSelectAIS->AddSelectablePoint(
3370 pTargetData->Lat, pTargetData->Lon, (void *)mmsi_long,
3371 SELTYPE_AISTARGET);
3372 pSel->SetUserData(pTargetData->MMSI);
3373 }
3374
3375 // Calculate CPA info for this target immediately
3376 UpdateOneCPA(pTargetData.get());
3377
3378 // Update this target's track
3379 if (pTargetData->b_show_track) UpdateOneTrack(pTargetData.get());
3380 }
3381 // TODO add ais message call
3382 plugin_msg.Notify(std::make_shared<AisTargetData>(*pTargetData), "");
3383 } else {
3384 // printf("Unrecognised AIS message ID: %d\n",
3385 // pTargetData->MID);
3386 if (new_target) {
3387 // delete pTargetData; // this target is not going to be used
3388 m_n_targets--;
3389 } else {
3390 // If this is not an ownship message, update the AIS Target in the
3391 // Selectable list even if the message type was not recognized
3392 if (!pTargetData->b_OwnShip) {
3393 if (pTargetData->b_positionOnceValid) {
3394 long mmsi_long = pTargetData->MMSI;
3395 SelectItem *pSel = pSelectAIS->AddSelectablePoint(
3396 pTargetData->Lat, pTargetData->Lon, (void *)mmsi_long,
3397 SELTYPE_AISTARGET);
3398 pSel->SetUserData(pTargetData->MMSI);
3399 }
3400 }
3401 }
3402 }
3403}
3404
3405void AisDecoder::getAISTarget(long mmsi,
3406 std::shared_ptr<AisTargetData> &pTargetData,
3407 std::shared_ptr<AisTargetData> &pStaleTarget,
3408 bool &bnewtarget, int &last_report_ticks,
3409 wxDateTime &now) {
3410 now = wxDateTime::Now();
3411 auto it = AISTargetList.find(mmsi);
3412 if (it == AISTargetList.end()) // not found
3413 {
3414 pTargetData = AisTargetDataMaker::GetInstance().GetTargetData();
3415 bnewtarget = true;
3416 m_n_targets++;
3417 } else {
3418 pTargetData = it->second; // find current entry
3419 pStaleTarget = pTargetData; // save a pointer to stale data
3420 }
3421
3422 // Grab the stale targets's last report time
3423 now.MakeGMT();
3424
3425 if (pStaleTarget)
3426 last_report_ticks = pStaleTarget->PositionReportTicks;
3427 else
3428 last_report_ticks = now.GetTicks();
3429
3430 // Delete the stale AIS Target selectable point
3431 if (pStaleTarget)
3432 pSelectAIS->DeleteSelectablePoint((void *)mmsi, SELTYPE_AISTARGET);
3433}
3434
3435std::shared_ptr<AisTargetData> AisDecoder::ProcessDSx(const wxString &str,
3436 bool b_take_dsc) {
3437 double dsc_lat = 0.;
3438 double dsc_lon = 0.;
3439 double dsc_mins, dsc_degs, dsc_tmp, dsc_addr;
3440 double dse_tmp;
3441 double dse_lat = 0.;
3442 double dse_lon = 0.;
3443 long dsc_fmt, dsc_quadrant, dsc_cat, dsc_nature;
3444
3445 int dsc_mmsi = 0;
3446 int dsc_tx_mmsi = 0;
3447 int dse_mmsi = 0;
3448 double dse_cog = 0.;
3449 double dse_sog = 0.;
3450 wxString dse_shipName = wxEmptyString;
3451 wxString dseSymbol;
3452
3453 unsigned mmsi = 0;
3454
3455 std::shared_ptr<AisTargetData> pTargetData = nullptr;
3456
3457 // parse a DSC Position message $CDDSx,.....
3458 // Use a tokenizer to pull out the first 9 fields
3459 wxStringTokenizer tkz(str, ",*");
3460
3461 wxString token;
3462 token = tkz.GetNextToken(); // !$CDDS
3463
3464 if (str.Mid(3, 3).IsSameAs(_T("DSC"))) {
3465 m_dsc_last_string = str;
3466
3467 token = tkz.GetNextToken(); // format specifier
3468 token.ToLong(
3469 &dsc_fmt); // (02-area,12-distress,16-allships,20-individual,...)
3470
3471 token = tkz.GetNextToken(); // address i.e. mmsi*10 for received msg, or
3472 // area spec or sender mmsi for (12) and (16)
3473 if (dsc_fmt == 12 || dsc_fmt == 16) {
3474 dsc_mmsi = wxAtoi(token.Mid(0, 9));
3475 } else {
3476 token.ToDouble(&dsc_addr);
3477 dsc_mmsi = 0 - (int)(dsc_addr / 10); // as per NMEA 0183 3.01
3478 }
3479
3480 token = tkz.GetNextToken(); // category
3481 token.ToLong(&dsc_cat); // 12 - Distress (relayed)
3482
3483 token = tkz.GetNextToken(); // nature of distress or telecommand1
3484 if (!token.IsSameAs(wxEmptyString)) { // 00-12 = nature of distress
3485 token.ToLong(&dsc_nature);
3486 } else
3487 dsc_nature = 99;
3488
3489 token = tkz.GetNextToken(); // comm type or telecommand2
3490
3491 token = tkz.GetNextToken(); // position or channel/freq
3492 token.ToDouble(&dsc_tmp);
3493
3494 token = tkz.GetNextToken(); // time or tel. no.
3495 token = tkz.GetNextToken(); // mmsi of ship in distress, relay
3496 if (dsc_fmt == 16 && dsc_cat == 12 && !token.IsSameAs(wxEmptyString)) {
3497 // wxString dmmsi = token.Mid(0,9);
3498 dsc_tx_mmsi = dsc_mmsi; // mmsi of relay issuer
3499 dsc_mmsi = wxAtoi(token.Mid(0, 9));
3500 }
3501 token = tkz.GetNextToken(); // nature of distress, relay
3502 if (dsc_fmt == 16 && dsc_cat == 12) {
3503 if (!token.IsSameAs(wxEmptyString)) { // 00-12 = nature of distress
3504 token.ToLong(&dsc_nature);
3505 } else
3506 dsc_nature = 99;
3507 }
3508 token = tkz.GetNextToken(); // acknowledgement
3509 token = tkz.GetNextToken(); // expansion indicator
3510
3511 dsc_quadrant = (int)(dsc_tmp / 1000000000.0);
3512
3513 if (dsc_quadrant > 3) // Position is "Unspecified", or 9999999999
3514 return nullptr;
3515
3516 dsc_lat = (int)(dsc_tmp / 100000.0);
3517 dsc_lon = dsc_tmp - dsc_lat * 100000.0;
3518 dsc_lat = dsc_lat - dsc_quadrant * 10000;
3519 dsc_degs = (int)(dsc_lat / 100.0);
3520 dsc_mins = dsc_lat - dsc_degs * 100.0;
3521 dsc_lat = dsc_degs + dsc_mins / 60.0;
3522
3523 dsc_degs = (int)(dsc_lon / 100.0);
3524 dsc_mins = dsc_lon - dsc_degs * 100.0;
3525 dsc_lon = dsc_degs + dsc_mins / 60.0;
3526 switch (dsc_quadrant) {
3527 case 0:
3528 break; // NE
3529 case 1:
3530 dsc_lon = -dsc_lon;
3531 break; // NW
3532 case 2:
3533 dsc_lat = -dsc_lat;
3534 break; // SE
3535 case 3:
3536 dsc_lon = -dsc_lon;
3537 dsc_lat = -dsc_lat;
3538 break; // SW
3539 default:
3540 break;
3541 }
3542 if (dsc_fmt != 02) mmsi = (int)dsc_mmsi;
3543
3544 } else if (str.Mid(3, 3).IsSameAs(_T("DSE"))) {
3545 token = tkz.GetNextToken(); // total number of sentences
3546 token = tkz.GetNextToken(); // sentence number
3547 token = tkz.GetNextToken(); // query/rely flag
3548 token = tkz.GetNextToken(); // vessel MMSI
3549 dse_mmsi = wxAtoi(token.Mid(
3550 0, 9)); // ITU-R M.493-10 �5.2
3551 // token.ToDouble(&dse_addr);
3552 // 0 - (int)(dse_addr / 10); // as per NMEA 0183 3.01
3553
3554#if 0
3555 token = tkz.GetNextToken(); // code field
3556 token =
3557 tkz.GetNextToken(); // data field - position - 2*4 digits latlon .mins
3558 token.ToDouble(&dse_tmp);
3559 dse_lat = (int)(dse_tmp / 10000.0);
3560 dse_lon = (int)(dse_tmp - dse_lat * 10000.0);
3561 dse_lat = dse_lat / 600000.0;
3562 dse_lon = dse_lon / 600000.0;
3563#endif
3564 // DSE Sentence may contain multiple dse expansion data items
3565 while (tkz.HasMoreTokens()) {
3566 dseSymbol = tkz.GetNextToken(); // dse expansion data symbol
3567 token = tkz.GetNextToken(); // dse expansion data
3568 if (dseSymbol.IsSameAs(_T("00"))) { // Position
3569 token.ToDouble(&dse_tmp);
3570 dse_lat = (int)(dse_tmp / 10000.0);
3571 dse_lon = (int)(dse_tmp - dse_lat * 10000.0);
3572 dse_lat = dse_lat / 600000.0;
3573 dse_lon = dse_lon / 600000.0;
3574 } else if (dseSymbol.IsSameAs(_T("01"))) { // Source & Datum
3575 } else if (dseSymbol.IsSameAs(_T("02"))) { // SOG
3576 token.ToDouble(&dse_tmp);
3577 dse_sog = dse_tmp / 10.0;
3578 } else if (dseSymbol.IsSameAs(_T("03"))) { // COG
3579 token.ToDouble(&dse_tmp);
3580 dse_cog = dse_tmp / 10.0;
3581 } else if (dseSymbol.IsSameAs(_T("04"))) { // Station Information
3582 dse_shipName = DecodeDSEExpansionCharacters(token);
3583 } else if (dseSymbol.IsSameAs(_T("05"))) { // Geographic Information
3584 } else if (dseSymbol.IsSameAs(_T("06"))) { // Persons On Board
3585 }
3586 }
3587 mmsi = abs((int)dse_mmsi);
3588 }
3589
3590 // Get the last report time for this target, if it exists
3591 wxDateTime now = wxDateTime::Now();
3592 now.MakeGMT();
3593 int last_report_ticks = now.GetTicks();
3594
3595 // Search the current AISTargetList for an MMSI match
3596 auto it = AISTargetList.find(mmsi);
3597 std::shared_ptr<AisTargetData> pStaleTarget = nullptr;
3598 if (it == AISTargetList.end()) { // not found
3599 } else {
3600 pStaleTarget = it->second; // find current entry
3601 last_report_ticks = pStaleTarget->PositionReportTicks;
3602 }
3603
3604 if (dsc_mmsi) {
3605 // Create a tentative target, but do not post it pending receipt of
3606 // extended data
3607 m_ptentative_dsctarget = AisTargetDataMaker::GetInstance().GetTargetData();
3608
3609 m_ptentative_dsctarget->PositionReportTicks = now.GetTicks();
3610 m_ptentative_dsctarget->StaticReportTicks = now.GetTicks();
3611
3612 m_ptentative_dsctarget->MMSI = mmsi;
3613 m_ptentative_dsctarget->NavStatus =
3614 99; // Undefind. "-" in the AIS target list
3615 m_ptentative_dsctarget->Lat = dsc_lat;
3616 m_ptentative_dsctarget->Lon = dsc_lon;
3617 m_ptentative_dsctarget->b_positionOnceValid = true;
3618 m_ptentative_dsctarget->COG = 0;
3619 m_ptentative_dsctarget->SOG = 0;
3620 m_ptentative_dsctarget->ShipType = dsc_fmt;
3621 m_ptentative_dsctarget->Class = AIS_DSC;
3622 m_ptentative_dsctarget->b_isDSCtarget = true;
3623 m_ptentative_dsctarget->b_nameValid = true;
3624 if (dsc_fmt == 12 || (dsc_fmt == 16 && dsc_cat == 12)) {
3625 snprintf(m_ptentative_dsctarget->ShipName, SHIP_NAME_LEN, "DISTRESS %u",
3626 mmsi);
3627 m_ptentative_dsctarget->m_dscNature = int(dsc_nature);
3628 m_ptentative_dsctarget->m_dscTXmmsi = dsc_tx_mmsi;
3629 } else {
3630 snprintf(m_ptentative_dsctarget->ShipName, SHIP_NAME_LEN, "POSITION %u",
3631 mmsi);
3632 }
3633
3634 m_ptentative_dsctarget->b_active = true;
3635 m_ptentative_dsctarget->b_lost = false;
3636 m_ptentative_dsctarget->RecentPeriod =
3637 m_ptentative_dsctarget->PositionReportTicks - last_report_ticks;
3638
3639 // Start a timer, looking for an expected DSE extension message
3640 if (!b_take_dsc) m_dsc_timer.Start(1000, wxTIMER_ONE_SHOT);
3641 }
3642
3643 // Got an extension message, or the timer expired and no extension is
3644 // expected
3645 if (dse_mmsi || b_take_dsc) {
3646 if (m_ptentative_dsctarget) {
3647 // stop the timer for sure
3648 m_dsc_timer.Stop();
3649
3650 // Update the extended information
3651 if (dse_mmsi) {
3652 m_ptentative_dsctarget->Lat =
3653 m_ptentative_dsctarget->Lat +
3654 ((m_ptentative_dsctarget->Lat) >= 0 ? dse_lat : -dse_lat);
3655 m_ptentative_dsctarget->Lon =
3656 m_ptentative_dsctarget->Lon +
3657 ((m_ptentative_dsctarget->Lon) >= 0 ? dse_lon : -dse_lon);
3658 if (!dse_shipName.empty()) {
3659 memset(m_ptentative_dsctarget->ShipName, '\0', SHIP_NAME_LEN);
3660 snprintf(m_ptentative_dsctarget->ShipName, dse_shipName.length(),
3661 "%s", dse_shipName.ToAscii().data());
3662 }
3663 m_ptentative_dsctarget->COG = dse_cog;
3664 m_ptentative_dsctarget->SOG = dse_sog;
3665 }
3666
3667 // Update the most recent report period
3668 m_ptentative_dsctarget->RecentPeriod =
3669 m_ptentative_dsctarget->PositionReportTicks - last_report_ticks;
3670
3671 // And post the target
3672
3673 // Search the current AISTargetList for an MMSI match
3674 auto found = AISTargetList.find(mmsi);
3675 if (found == AISTargetList.end()) { // not found
3676 pTargetData = m_ptentative_dsctarget;
3677 } else {
3678 pTargetData = found->second; // find current entry
3679 std::vector<AISTargetTrackPoint> ptrack =
3680 std::move(pTargetData->m_ptrack);
3681 pTargetData->CloneFrom(
3682 m_ptentative_dsctarget
3683 .get()); // this will make an empty track list
3684
3685 pTargetData->m_ptrack =
3686 std::move(ptrack); // and substitute the old track list
3687
3688 // delete m_ptentative_dsctarget;
3689 }
3690
3691 // Reset for next time
3692 m_ptentative_dsctarget = nullptr;
3693
3694 m_pLatestTargetData = pTargetData;
3695
3696 AISTargetList[pTargetData->MMSI] =
3697 pTargetData; // update the hash table entry
3698
3699 long mmsi_long = pTargetData->MMSI;
3700
3701 // Delete any stale Target selectable point
3702 if (pStaleTarget)
3703 pSelectAIS->DeleteSelectablePoint((void *)mmsi_long, SELTYPE_AISTARGET);
3704 // And add the updated target
3705 SelectItem *pSel =
3706 pSelectAIS->AddSelectablePoint(pTargetData->Lat, pTargetData->Lon,
3707 (void *)mmsi_long, SELTYPE_AISTARGET);
3708 pSel->SetUserData(pTargetData->MMSI);
3709
3710 // Calculate CPA info for this target immediately
3711 UpdateOneCPA(pTargetData.get());
3712
3713 // Update this target's track
3714 if (pTargetData->b_show_track) UpdateOneTrack(pTargetData.get());
3715 }
3716 }
3717
3718 return pTargetData;
3719}
3720
3721bool AisDecoder::NMEACheckSumOK(const wxString &str_in) {
3722 unsigned char checksum_value = 0;
3723 int sentence_hex_sum;
3724
3725 wxCharBuffer buf = str_in.ToUTF8();
3726 if (!buf.data()) return false; // cannot decode string
3727
3728 char str_ascii[AIS_MAX_MESSAGE_LEN + 1];
3729 strncpy(str_ascii, buf.data(), AIS_MAX_MESSAGE_LEN);
3730 str_ascii[AIS_MAX_MESSAGE_LEN] = '\0';
3731
3732 int string_length = strlen(str_ascii);
3733
3734 int payload_length = 0;
3735 while ((payload_length < string_length) &&
3736 (str_ascii[payload_length] != '*')) // look for '*'
3737 payload_length++;
3738
3739 if (payload_length == string_length)
3740 return false; // '*' not found at all, no checksum
3741
3742 int index = 1; // Skip over the $ at the begining of the sentence
3743
3744 while (index < payload_length) {
3745 checksum_value ^= str_ascii[index];
3746 index++;
3747 }
3748
3749 if (string_length > 4) {
3750 char scanstr[3];
3751 scanstr[0] = str_ascii[payload_length + 1];
3752 scanstr[1] = str_ascii[payload_length + 2];
3753 scanstr[2] = 0;
3754 sscanf(scanstr, "%2x", &sentence_hex_sum);
3755
3756 if (sentence_hex_sum == checksum_value) return true;
3757 }
3758
3759 return false;
3760}
3761
3762void AisDecoder::UpdateAllCPA() {
3763 // Iterate thru all the targets
3764 for (const auto &it : GetTargetList()) {
3765 std::shared_ptr<AisTargetData> td = it.second;
3766
3767 if (nullptr != td) UpdateOneCPA(td.get());
3768 }
3769}
3770
3771void AisDecoder::UpdateAllTracks() {
3772 for (const auto &it : GetTargetList()) {
3773 std::shared_ptr<AisTargetData> td = it.second;
3774 if (td) UpdateOneTrack(td.get());
3775 }
3776}
3777
3778int gdup;
3779void AisDecoder::UpdateOneTrack(AisTargetData *ptarget) {
3780 if (!ptarget->b_positionOnceValid) return;
3781 // Reject for unbelievable jumps (corrupted/bad data)
3782 if (!ptarget->m_ptrack.empty()) {
3783 const AISTargetTrackPoint &LastTrackpoint = ptarget->m_ptrack.back();
3784 if (fabs(LastTrackpoint.m_lat - ptarget->Lat) > .1 ||
3785 fabs(LastTrackpoint.m_lon - ptarget->Lon) > .1) {
3786 // after an unlikely jump in pos, the last trackpoint might also be wrong
3787 // just to be sure we do delete this one as well.
3788 ptarget->m_ptrack.pop_back();
3789 ptarget->b_positionDoubtful = true;
3790 return;
3791 }
3792 }
3793
3794 // Avoid duplicate track points
3795 // Do not add track point if time since last point is < 2 seconds.
3796 if ((ptarget->PositionReportTicks - ptarget->LastPositionReportTicks) > 2) {
3797 // Create the newest point
3798 AISTargetTrackPoint ptrackpoint;
3799 ptrackpoint.m_lat = ptarget->Lat;
3800 ptrackpoint.m_lon = ptarget->Lon;
3801 ptrackpoint.m_time = wxDateTime::Now().GetTicks();
3802
3803 ptarget->m_ptrack.push_back(ptrackpoint);
3804
3805 if (ptarget->b_PersistTrack || ptarget->b_mPropPersistTrack) {
3806 Track *t;
3807 if (0 == m_persistent_tracks.count(ptarget->MMSI)) {
3808 t = new Track();
3809 t->SetName(wxString::Format(
3810 _T("AIS %s (%u) %s %s"), ptarget->GetFullName().c_str(),
3811 ptarget->MMSI, wxDateTime::Now().FormatISODate().c_str(),
3812 wxDateTime::Now().FormatISOTime().c_str()));
3813 g_TrackList.push_back(t);
3814 new_track.Notify(t);
3815 m_persistent_tracks[ptarget->MMSI] = t;
3816 } else {
3817 t = m_persistent_tracks[ptarget->MMSI];
3818 }
3819 TrackPoint *tp = t->GetLastPoint();
3820 vector2D point(ptrackpoint.m_lon, ptrackpoint.m_lat);
3821 TrackPoint *tp1 =
3822 t->AddNewPoint(point, wxDateTime(ptrackpoint.m_time).ToUTC());
3823
3824 if (tp)
3825 pSelect->AddSelectableTrackSegment(tp->m_lat, tp->m_lon, tp1->m_lat,
3826 tp1->m_lon, tp, tp1, t);
3827
3828 // We do not want dependency on the GUI here, do we?
3829 // if( pRouteManagerDialog && pRouteManagerDialog->IsShown() )
3830 // pRouteManagerDialog->UpdateTrkListCtrl();
3831
3832 } else {
3833 // Walk the list, removing any track points that are older than the
3834 // stipulated time
3835 time_t test_time =
3836 wxDateTime::Now().GetTicks() - (time_t)(g_AISShowTracks_Mins * 60);
3837
3838 ptarget->m_ptrack.erase(
3839 std::remove_if(ptarget->m_ptrack.begin(), ptarget->m_ptrack.end(),
3840 [=](const AISTargetTrackPoint &track) {
3841 return track.m_time < test_time;
3842 }),
3843 ptarget->m_ptrack.end());
3844 }
3845 }
3846}
3847
3848void AisDecoder::DeletePersistentTrack(const Track *track) {
3849 for (auto it = m_persistent_tracks.begin(); it != m_persistent_tracks.end();
3850 it++) {
3851 if (it->second == track) {
3852 unsigned mmsi = it->first;
3853 m_persistent_tracks.erase(it);
3854 // Last tracks for this target?
3855 if (0 == m_persistent_tracks.count(mmsi)) {
3856 for (unsigned int i = 0; i < g_MMSI_Props_Array.GetCount(); i++) {
3857 if (mmsi == static_cast<unsigned>(g_MMSI_Props_Array[i]->MMSI)) {
3858 MmsiProperties *props = g_MMSI_Props_Array[i];
3859 if (props->m_bPersistentTrack) {
3860 // Ask if mmsi props should be changed.
3861 // Avoid creation of a new track while messaging
3862
3863 std::shared_ptr<AisTargetData> td =
3864 Get_Target_Data_From_MMSI(mmsi);
3865 if (td) {
3866 props->m_bPersistentTrack = false;
3867 td->b_mPropPersistTrack = false;
3868 }
3869 if (!m_callbacks.confirm_stop_track()) {
3870 props->m_bPersistentTrack = true;
3871 }
3872 }
3873 break;
3874 }
3875 }
3876 }
3877 break;
3878 }
3879 }
3880}
3881
3882void AisDecoder::UpdateAllAlarms() {
3883 m_bGeneralAlert = false; // no alerts yet
3884
3885 // Iterate thru all the targets
3886 for (const auto &it : GetTargetList()) {
3887 std::shared_ptr<AisTargetData> td = it.second;
3888
3889 if (td) {
3890 // Maintain General Alert
3891 if (!m_bGeneralAlert) {
3892 // Quick check on basic condition
3893 if ((td->CPA < g_CPAWarn_NM) && (td->TCPA > 0) &&
3894 (td->Class != AIS_ATON) && (td->Class != AIS_BASE))
3895 m_bGeneralAlert = true;
3896
3897 // Some options can suppress general alerts
3898 if (g_bAIS_CPA_Alert_Suppress_Moored && (td->SOG <= g_ShowMoored_Kts))
3899 m_bGeneralAlert = false;
3900
3901 // Skip distant targets if requested
3902 if ((g_bCPAMax) && (td->Range_NM > g_CPAMax_NM))
3903 m_bGeneralAlert = false;
3904
3905 // Skip if TCPA is too long
3906 if ((g_bTCPA_Max) && (td->TCPA > g_TCPA_Max)) m_bGeneralAlert = false;
3907
3908 // SART targets always alert if "Active"
3909 if (td->Class == AIS_SART && td->NavStatus == 14)
3910 m_bGeneralAlert = true;
3911
3912 // DSC Distress targets always alert
3913 if ((td->Class == AIS_DSC) &&
3914 ((td->ShipType == 12) || (td->ShipType == 16)))
3915 m_bGeneralAlert = true;
3916 }
3917
3918 ais_alert_type this_alarm = AIS_NO_ALERT;
3919
3920 // SART targets always alert if "Active"
3921 if (td->Class == AIS_SART && td->NavStatus == 14)
3922 this_alarm = AIS_ALERT_SET;
3923
3924 // DSC Distress targets always alert
3925 if ((td->Class == AIS_DSC) &&
3926 ((td->ShipType == 12) || (td->ShipType == 16)))
3927 this_alarm = AIS_ALERT_SET;
3928
3929 if (g_bCPAWarn && td->b_active && td->b_positionOnceValid &&
3930 (td->Class != AIS_SART) && (td->Class != AIS_DSC)) {
3931 // Skip anchored/moored(interpreted as low speed) targets if
3932 // requested
3933 if ((g_bHideMoored) && (td->SOG <= g_ShowMoored_Kts)) { // dsr
3934 td->n_alert_state = AIS_NO_ALERT;
3935 continue;
3936 }
3937
3938 // No Alert on moored(interpreted as low speed) targets if so
3939 // requested
3940 if (g_bAIS_CPA_Alert_Suppress_Moored &&
3941 (td->SOG <= g_ShowMoored_Kts)) { // dsr
3942 td->n_alert_state = AIS_NO_ALERT;
3943 continue;
3944 }
3945
3946 // Skip distant targets if requested
3947 if (g_bCPAMax) {
3948 if (td->Range_NM > g_CPAMax_NM) {
3949 td->n_alert_state = AIS_NO_ALERT;
3950 continue;
3951 }
3952 }
3953
3954 if ((td->CPA < g_CPAWarn_NM) && (td->TCPA > 0) &&
3955 (td->Class != AIS_ATON) && (td->Class != AIS_BASE) &&
3956 (td->Class != AIS_METEO)) {
3957 if (g_bTCPA_Max) {
3958 if (td->TCPA < g_TCPA_Max) {
3959 if (td->b_isFollower)
3960 this_alarm = AIS_ALERT_NO_DIALOG_SET;
3961 else
3962 this_alarm = AIS_ALERT_SET;
3963 }
3964 } else {
3965 if (td->b_isFollower)
3966 this_alarm = AIS_ALERT_NO_DIALOG_SET;
3967 else
3968 this_alarm = AIS_ALERT_SET;
3969 }
3970 }
3971 }
3972
3973 // Maintain the timer for in_ack flag
3974 // SART and DSC targets always maintain ack timeout
3975
3976 if (g_bAIS_ACK_Timeout || (td->Class == AIS_SART) ||
3977 ((td->Class == AIS_DSC) &&
3978 ((td->ShipType == 12) || (td->ShipType == 16)))) {
3979 if (td->b_in_ack_timeout) {
3980 wxTimeSpan delta = wxDateTime::Now() - td->m_ack_time;
3981 if (delta.GetMinutes() > g_AckTimeout_Mins)
3982 td->b_in_ack_timeout = false;
3983 }
3984 } else {
3985 // Not using ack timeouts.
3986 // If a target has been acknowledged, leave it ack'ed until it goes out
3987 // of AIS_ALARM_SET state
3988 if (td->b_in_ack_timeout) {
3989 if (this_alarm == AIS_NO_ALERT) td->b_in_ack_timeout = false;
3990 }
3991 }
3992
3993 td->n_alert_state = this_alarm;
3994 }
3995 }
3996}
3997
3998void AisDecoder::UpdateOneCPA(AisTargetData *ptarget) {
3999 ptarget->Range_NM = -1.; // Defaults
4000 ptarget->Brg = -1.;
4001
4002 // Compute the current Range/Brg to the target
4003 // This should always be possible even if GPS data is not valid
4004 // because O must always have a position for own-ship. Plugins need
4005 // AIS target range and bearing from own-ship position even if GPS is not
4006 // valid.
4007 double brg, dist;
4008 DistanceBearingMercator(ptarget->Lat, ptarget->Lon, gLat, gLon, &brg, &dist);
4009 ptarget->Range_NM = dist;
4010 ptarget->Brg = brg;
4011
4012 if (dist <= 1e-5) ptarget->Brg = -1.0; // Brg is undefined if Range == 0.
4013
4014 if (!ptarget->b_positionOnceValid || !bGPSValid) {
4015 ptarget->bCPA_Valid = false;
4016 return;
4017 }
4018 // Ais Meteo is not a hard target in danger for collision
4019 if (ptarget->Class == AIS_METEO) {
4020 ptarget->bCPA_Valid = false;
4021 return;
4022 }
4023
4024 // There can be no collision between ownship and itself....
4025 // This can happen if AIVDO messages are received, and there is another
4026 // source of ownship position, like NMEA GLL The two positions are always
4027 // temporally out of sync, and one will always be exactly in front of the
4028 // other one.
4029 if (ptarget->b_OwnShip) {
4030 ptarget->CPA = 100;
4031 ptarget->TCPA = -100;
4032 ptarget->bCPA_Valid = false;
4033 return;
4034 }
4035
4036 double cpa_calc_ownship_cog = gCog;
4037 double cpa_calc_target_cog = ptarget->COG;
4038
4039 // Ownship is not reporting valid SOG, so no way to calculate CPA
4040 if (std::isnan(gSog) || (gSog > 102.2)) {
4041 ptarget->bCPA_Valid = false;
4042 return;
4043 }
4044
4045 // Ownship is maybe anchored and not reporting COG
4046 if (std::isnan(gCog) || gCog == 360.0) {
4047 if (gSog < .01)
4048 cpa_calc_ownship_cog =
4049 0.; // substitute value
4050 // for the case where SOG ~= 0, and COG is unknown.
4051 else {
4052 ptarget->bCPA_Valid = false;
4053 return;
4054 }
4055 }
4056
4057 // Target is maybe anchored and not reporting COG
4058 if (ptarget->COG == 360.0) {
4059 if (ptarget->SOG > 102.2) {
4060 ptarget->bCPA_Valid = false;
4061 return;
4062 } else if (ptarget->SOG < .01) {
4063 cpa_calc_target_cog = 0.; // substitute value
4064 // for the case where SOG ~= 0, and COG is unknown.
4065 } else {
4066 ptarget->bCPA_Valid = false;
4067 return;
4068 }
4069 }
4070
4071 // Express the SOGs as meters per hour
4072 double v0 = gSog * 1852.;
4073 double v1 = ptarget->SOG * 1852.;
4074
4075 if ((v0 < 1e-6) && (v1 < 1e-6)) {
4076 ptarget->TCPA = 0.;
4077 ptarget->CPA = 0.;
4078
4079 ptarget->bCPA_Valid = false;
4080 } else {
4081 // Calculate the TCPA first
4082
4083 // Working on a Reduced Lat/Lon orthogonal plotting sheet....
4084 // Get easting/northing to target, in meters
4085
4086 double east1 = (ptarget->Lon - gLon) * 60 * 1852;
4087 double north1 = (ptarget->Lat - gLat) * 60 * 1852;
4088
4089 double east = east1 * (cos(gLat * PI / 180.));
4090
4091 double north = north1;
4092
4093 // Convert COGs trigonometry to standard unit circle
4094 double cosa = cos((90. - cpa_calc_ownship_cog) * PI / 180.);
4095 double sina = sin((90. - cpa_calc_ownship_cog) * PI / 180.);
4096 double cosb = cos((90. - cpa_calc_target_cog) * PI / 180.);
4097 double sinb = sin((90. - cpa_calc_target_cog) * PI / 180.);
4098
4099 // These will be useful
4100 double fc = (v0 * cosa) - (v1 * cosb);
4101 double fs = (v0 * sina) - (v1 * sinb);
4102
4103 double d = (fc * fc) + (fs * fs);
4104 double tcpa;
4105
4106 // the tracks are almost parallel
4107 if (fabs(d) < 1e-6)
4108 tcpa = 0.;
4109 else
4110 // Here is the equation for t, which will be in hours
4111 tcpa = ((fc * east) + (fs * north)) / d;
4112
4113 // Convert to minutes
4114 ptarget->TCPA = tcpa * 60.;
4115
4116 // Calculate CPA
4117 // Using TCPA, predict ownship and target positions
4118
4119 double OwnshipLatCPA, OwnshipLonCPA, TargetLatCPA, TargetLonCPA;
4120
4121 ll_gc_ll(gLat, gLon, cpa_calc_ownship_cog, gSog * tcpa, &OwnshipLatCPA,
4122 &OwnshipLonCPA);
4123 ll_gc_ll(ptarget->Lat, ptarget->Lon, cpa_calc_target_cog,
4124 ptarget->SOG * tcpa, &TargetLatCPA, &TargetLonCPA);
4125
4126 // And compute the distance
4127 ptarget->CPA = DistGreatCircle(OwnshipLatCPA, OwnshipLonCPA, TargetLatCPA,
4128 TargetLonCPA);
4129
4130 ptarget->bCPA_Valid = true;
4131
4132 if (ptarget->TCPA < 0) ptarget->bCPA_Valid = false;
4133 }
4134}
4135
4136void AisDecoder::OnTimerDSC(wxTimerEvent &event) {
4137 // Timer expired, no CDDSE message was received, so accept the latest CDDSC
4138 // message
4139 if (m_ptentative_dsctarget) {
4140 ProcessDSx(m_dsc_last_string, true);
4141 }
4142}
4143
4144void AisDecoder::OnTimerAIS(wxTimerEvent &event) {
4145 TimerAIS.Stop();
4146 // Scrub the target hash list
4147 // removing any targets older than stipulated age
4148
4149 wxDateTime now = wxDateTime::Now();
4150 now.MakeGMT();
4151
4152 std::unordered_map<int, std::shared_ptr<AisTargetData>> &current_targets =
4153 GetTargetList();
4154
4155 auto it = current_targets.begin();
4156 std::vector<int> remove_array; // collector for MMSI of targets to be removed
4157
4158 while (it != current_targets.end()) {
4159 if (it->second ==
4160 nullptr) // This should never happen, but I saw it once....
4161 {
4162 current_targets.erase(it);
4163 break; // leave the loop
4164 }
4165 // std::shared_ptr<AisTargetData>
4166 // xtd(std::make_shared<AisTargetData>(*it->second));
4167 std::shared_ptr<AisTargetData> xtd = it->second;
4168
4169 int target_posn_age = now.GetTicks() - xtd->PositionReportTicks;
4170 int target_static_age = now.GetTicks() - xtd->StaticReportTicks;
4171
4172 // Global variables controlling lost target handling
4173 // g_bMarkLost
4174 // g_MarkLost_Mins // Minutes until black "cross out
4175 // g_bRemoveLost
4176 // g_RemoveLost_Mins); // minutes until target is removed from screen and
4177 // internal lists
4178
4179 // g_bInlandEcdis
4180
4181 // Mark lost targets if specified
4182 double removelost_Mins = fmax(g_RemoveLost_Mins, g_MarkLost_Mins);
4183
4184 if (g_bInlandEcdis && (xtd->Class != AIS_ARPA)) {
4185 double iECD_LostTimeOut = 0.0;
4186 // special rules apply for europe inland ecdis timeout settings. overrule
4187 // option settings Won't apply for ARPA targets where the radar has all
4188 // control
4189 if (xtd->Class == AIS_CLASS_B) {
4190 if ((xtd->NavStatus == MOORED) || (xtd->NavStatus == AT_ANCHOR))
4191 iECD_LostTimeOut = 18 * 60;
4192 else
4193 iECD_LostTimeOut = 180;
4194 }
4195 if (xtd->Class == AIS_CLASS_A) {
4196 if ((xtd->NavStatus == MOORED) || (xtd->NavStatus == AT_ANCHOR)) {
4197 if (xtd->SOG < 3.)
4198 iECD_LostTimeOut = 18 * 60;
4199 else
4200 iECD_LostTimeOut = 60;
4201 } else
4202 iECD_LostTimeOut = 60;
4203 }
4204
4205 if ((target_posn_age > iECD_LostTimeOut) &&
4206 (xtd->Class != AIS_GPSG_BUDDY))
4207 xtd->b_active = false;
4208
4209 removelost_Mins = (2 * iECD_LostTimeOut) / 60.;
4210 } else if (g_bMarkLost) {
4211 if ((target_posn_age > g_MarkLost_Mins * 60) &&
4212 (xtd->Class != AIS_GPSG_BUDDY))
4213 xtd->b_active = false;
4214 }
4215
4216 if (xtd->Class == AIS_SART || xtd->Class == AIS_METEO)
4217 removelost_Mins = 18.0;
4218
4219 // Remove lost targets if specified
4220
4221 if (g_bRemoveLost || g_bInlandEcdis) {
4222 bool b_arpalost =
4223 (xtd->Class == AIS_ARPA &&
4224 xtd->b_lost); // A lost ARPA target would be deleted at once
4225 if (((target_posn_age > removelost_Mins * 60) &&
4226 (xtd->Class != AIS_GPSG_BUDDY)) ||
4227 b_arpalost) {
4228 // So mark the target as lost, with unknown position, and make it
4229 // not selectable
4230 xtd->b_lost = true;
4231 xtd->b_positionOnceValid = false;
4232 xtd->COG = 360.0;
4233 xtd->SOG = 103.0;
4234 xtd->HDG = 511.0;
4235 xtd->ROTAIS = -128;
4236
4237 plugin_msg.Notify(xtd, "");
4238
4239 long mmsi_long = xtd->MMSI;
4240 pSelectAIS->DeleteSelectablePoint((void *)mmsi_long, SELTYPE_AISTARGET);
4241
4242 // If we have not seen a static report in 3 times the removal spec,
4243 // then remove the target from all lists
4244 // or a lost ARPA target.
4245 if (target_static_age > removelost_Mins * 60 * 3 || b_arpalost) {
4246 xtd->b_removed = true;
4247 plugin_msg.Notify(xtd, "");
4248 remove_array.push_back(xtd->MMSI); // Add this target to removal list
4249 }
4250 }
4251 }
4252
4253 // Remove any targets specified as to be "ignored", so that they won't
4254 // trigger phantom alerts (e.g. SARTs)
4255 for (unsigned int i = 0; i < g_MMSI_Props_Array.GetCount(); i++) {
4256 MmsiProperties *props = g_MMSI_Props_Array[i];
4257 if (xtd->MMSI == props->MMSI) {
4258 if (props->m_bignore) {
4259 remove_array.push_back(xtd->MMSI); // Add this target to removal list
4260 xtd->b_removed = true;
4261 plugin_msg.Notify(xtd, "");
4262 }
4263 break;
4264 }
4265 }
4266
4267 // Check if the target has recently been set as own MMSI
4268 if (static_cast<unsigned>(xtd->MMSI) == g_OwnShipmmsi) {
4269 remove_array.push_back(xtd->MMSI); // Add this target to removal list
4270 xtd->b_removed = true;
4271 plugin_msg.Notify(xtd, "");
4272 }
4273
4274 ++it;
4275 }
4276
4277 // Remove all the targets collected in remove_array in one pass
4278 for (unsigned int i = 0; i < remove_array.size(); i++) {
4279 auto itd = current_targets.find(remove_array[i]);
4280 if (itd != current_targets.end()) {
4281 std::shared_ptr<AisTargetData> td = itd->second;
4282 current_targets.erase(itd);
4283 // delete td;
4284 }
4285 }
4286
4287 UpdateAllCPA();
4288 UpdateAllAlarms();
4289
4290 // Update the general suppression flag
4291 m_bSuppressed = false;
4292 if (g_bAIS_CPA_Alert_Suppress_Moored || g_bHideMoored ||
4293 (g_bShowScaled && g_bAllowShowScaled))
4294 m_bSuppressed = true;
4295
4296 m_bAIS_Audio_Alert_On = false; // default, may be set on
4297
4298 // Process any Alarms
4299
4300 // If the AIS Alert Dialog is not currently shown....
4301
4302 // Scan all targets, looking for SART, DSC Distress, and CPA incursions
4303 // In the case of multiple targets of the same type, select the shortest
4304 // range or shortest TCPA
4305 std::shared_ptr<AisTargetData> palert_target = nullptr;
4306
4307 if (!g_pais_alert_dialog_active) {
4308 pAISMOBRoute = nullptr; // Reset the AISMOB auto route.
4309 double tcpa_min = 1e6; // really long
4310 double sart_range = 1e6;
4311 std::shared_ptr<AisTargetData> palert_target_cpa = nullptr;
4312 std::shared_ptr<AisTargetData> palert_target_sart = nullptr;
4313 std::shared_ptr<AisTargetData> palert_target_dsc = nullptr;
4314
4315 for (it = current_targets.begin(); it != current_targets.end(); ++it) {
4316 std::shared_ptr<AisTargetData> td = it->second;
4317 if (td) {
4318 if ((td->Class != AIS_SART) && (td->Class != AIS_DSC)) {
4319 if (g_bAIS_CPA_Alert && td->b_active) {
4320 if ((AIS_ALERT_SET == td->n_alert_state) && !td->b_in_ack_timeout) {
4321 if (td->TCPA < tcpa_min) {
4322 tcpa_min = td->TCPA;
4323 palert_target_cpa = td;
4324 }
4325 }
4326 }
4327 } else if ((td->Class == AIS_DSC) &&
4328 ((td->ShipType == 12) || (td->ShipType == 16))) {
4329 if (td->b_active) {
4330 if ((AIS_ALERT_SET == td->n_alert_state) && !td->b_in_ack_timeout) {
4331 palert_target_dsc = td;
4332 } else { // Reset DCS flag to open for a real AIS for the same
4333 // target
4334 td->b_isDSCtarget = false;
4335 }
4336 }
4337 }
4338
4339 else if (td->Class == AIS_SART) {
4340 if (td->b_active) {
4341 if ((AIS_ALERT_SET == td->n_alert_state) && !td->b_in_ack_timeout) {
4342 if (td->Range_NM < sart_range) {
4343 tcpa_min = sart_range;
4344 palert_target_sart = td;
4345 }
4346 }
4347 }
4348 }
4349 }
4350 }
4351
4352 // Which of multiple targets?
4353 // Give priority to SART targets, then DSC Distress, then CPA incursion
4354 palert_target = palert_target_cpa;
4355
4356 if (palert_target_sart) {
4357 palert_target = palert_target_sart;
4358 }
4359
4360 if (palert_target_dsc) {
4361 palert_target = palert_target_dsc;
4362 }
4363 } else {
4364 // Alert is currently shown, get target from from knowable GUI
4365 palert_target = Get_Target_Data_From_MMSI(m_callbacks.get_target_mmsi());
4366 }
4367 // Show or update the alert
4368 if (palert_target) info_update.Notify(palert_target, "");
4369
4370 TimerAIS.Start(TIMER_AIS_MSEC, wxTIMER_CONTINUOUS);
4371}
4372
4373std::shared_ptr<AisTargetData> AisDecoder::Get_Target_Data_From_MMSI(
4374 unsigned mmsi) {
4375 if (AISTargetList.find(mmsi) == AISTargetList.end()) return nullptr;
4376 return AISTargetList[mmsi];
4377}
4378
4379ArrayOfMmsiProperties g_MMSI_Props_Array;
4380
4381// MmsiProperties Implementation
4382
4383MmsiProperties::MmsiProperties(wxString &spec) {
4384 Init();
4385 wxStringTokenizer tkz(spec, _T(";"));
4386 wxString s;
4387
4388 s = tkz.GetNextToken();
4389 long mmsil;
4390 s.ToLong(&mmsil);
4391 MMSI = (int)mmsil;
4392
4393 s = tkz.GetNextToken();
4394 if (s.Len()) {
4395 if (s.Upper() == _T("ALWAYS"))
4396 TrackType = TRACKTYPE_ALWAYS;
4397 else if (s.Upper() == _T("NEVER"))
4398 TrackType = TRACKTYPE_NEVER;
4399 }
4400
4401 s = tkz.GetNextToken();
4402 if (s.Len()) {
4403 if (s.Upper() == _T("IGNORE")) m_bignore = true;
4404 }
4405
4406 s = tkz.GetNextToken();
4407 if (s.Len()) {
4408 if (s.Upper() == _T("MOB")) m_bMOB = true;
4409 }
4410
4411 s = tkz.GetNextToken();
4412 if (s.Len()) {
4413 if (s.Upper() == _T("VDM")) m_bVDM = true;
4414 }
4415
4416 s = tkz.GetNextToken();
4417 if (s.Len()) {
4418 if (s.Upper() == _T("FOLLOWER")) m_bFollower = true;
4419 }
4420
4421 s = tkz.GetNextToken();
4422 if (s.Len()) {
4423 if (s.Upper() == _T("PERSIST")) m_bPersistentTrack = true;
4424 }
4425
4426 s = tkz.GetNextToken();
4427 if (s.Len()) {
4428 m_ShipName = s.Upper();
4429 }
4430}
4431
4432MmsiProperties::~MmsiProperties() = default;
4433
4434void MmsiProperties::Init() {
4435 MMSI = -1;
4436 TrackType = TRACKTYPE_DEFAULT;
4437 m_bignore = false;
4438 m_bMOB = false;
4439 m_bVDM = false;
4440 m_bFollower = false;
4441 m_bPersistentTrack = false;
4442 m_ShipName = wxEmptyString;
4443}
4444
4445wxString MmsiProperties::Serialize() {
4446 wxString sMMSI;
4447 wxString s;
4448
4449 sMMSI.Printf(_T("%d"), MMSI);
4450 sMMSI << _T(";");
4451
4452 if (TrackType) {
4453 if (TRACKTYPE_ALWAYS == TrackType)
4454 sMMSI << _T("always");
4455 else if (TRACKTYPE_NEVER == TrackType)
4456 sMMSI << _T("never");
4457 }
4458 sMMSI << _T(";");
4459
4460 if (m_bignore) {
4461 sMMSI << _T("ignore");
4462 }
4463 sMMSI << _T(";");
4464
4465 if (m_bMOB) {
4466 sMMSI << _T("MOB");
4467 }
4468 sMMSI << _T(";");
4469
4470 if (m_bVDM) {
4471 sMMSI << _T("VDM");
4472 }
4473 sMMSI << _T(";");
4474
4475 if (m_bFollower) {
4476 sMMSI << _T("Follower");
4477 }
4478 sMMSI << _T(";");
4479
4480 if (m_bPersistentTrack) {
4481 sMMSI << _T("PERSIST");
4482 }
4483 sMMSI << _T(";");
4484
4485 if (m_ShipName == wxEmptyString) {
4486 m_ShipName = GetShipNameFromFile(MMSI);
4487 }
4488 sMMSI << m_ShipName;
4489 return sMMSI;
4490}
4491
4492void AISshipNameCache(AisTargetData *pTargetData,
4493 AIS_Target_Name_Hash *AISTargetNamesC,
4494 AIS_Target_Name_Hash *AISTargetNamesNC, long mmsi) {
4495 if (g_benableAISNameCache) {
4496 wxString ship_name = wxEmptyString;
4497
4498 // Check for valid name data
4499 if (!pTargetData->b_nameValid) {
4500 AIS_Target_Name_Hash::iterator it = AISTargetNamesC->find(mmsi);
4501 if (it != AISTargetNamesC->end()) {
4502 ship_name = (*AISTargetNamesC)[mmsi].Left(20);
4503 strncpy(pTargetData->ShipName, ship_name.mb_str(),
4504 ship_name.length() + 1);
4505 pTargetData->b_nameValid = true;
4506 pTargetData->b_nameFromCache = true;
4507 } else if (!g_bUseOnlyConfirmedAISName) {
4508 it = AISTargetNamesNC->find(mmsi);
4509 if (it != AISTargetNamesNC->end()) {
4510 ship_name = (*AISTargetNamesNC)[mmsi].Left(20);
4511 strncpy(pTargetData->ShipName, ship_name.mb_str(),
4512 ship_name.length() + 1);
4513 pTargetData->b_nameValid = true;
4514 pTargetData->b_nameFromCache = true;
4515 }
4516 }
4517 }
4518 // else there IS a valid name, lets check if it is in one of the hash lists.
4519 else if ((pTargetData->MID == 5) || (pTargetData->MID == 24) ||
4520 (pTargetData->MID == 19) ||
4521 (pTargetData->MID == 123) || // 123: Has got a name from SignalK
4522 (pTargetData->MID == 124)) { // 124: Has got a name from n2k
4523 // This message contains ship static data, so has a name field
4524 pTargetData->b_nameFromCache = false;
4525 ship_name = trimAISField(pTargetData->ShipName);
4526 AIS_Target_Name_Hash::iterator itC = AISTargetNamesC->find(mmsi);
4527 AIS_Target_Name_Hash::iterator itNC = AISTargetNamesNC->find(mmsi);
4528 if (itC !=
4529 AISTargetNamesC->end()) { // There is a confirmed entry for this mmsi
4530 if ((*AISTargetNamesC)[mmsi] ==
4531 ship_name) { // Received name is same as confirmed name
4532 if (itNC != AISTargetNamesNC->end()) { // there is also an entry in
4533 // the NC list, delete it
4534 AISTargetNamesNC->erase(itNC);
4535 }
4536 } else { // There is a confirmed name but that one is different
4537 if (itNC != AISTargetNamesNC->end()) { // there is an entry in the NC
4538 // list check if name is same
4539 if ((*AISTargetNamesNC)[mmsi] ==
4540 ship_name) { // Same name is already in NC list so promote till
4541 // confirmed list
4542 (*AISTargetNamesC)[mmsi] = ship_name;
4543 // And delete from NC list
4544 AISTargetNamesNC->erase(itNC);
4545 } else { // A different name is in the NC list, update with
4546 // received one
4547 (*AISTargetNamesNC)[mmsi] = ship_name;
4548 }
4549 if (g_bUseOnlyConfirmedAISName)
4550 strncpy(pTargetData->ShipName, (*AISTargetNamesC)[mmsi].mb_str(),
4551 (*AISTargetNamesC)[mmsi].Left(20).Length() + 1);
4552 } else { // The C list name is different but no NC entry. Add it.
4553 (*AISTargetNamesNC)[mmsi] = ship_name;
4554 }
4555 }
4556 } else { // No confirmed entry available
4557 if (itNC !=
4558 AISTargetNamesNC->end()) { // there is an entry in the NC list,
4559 if ((*AISTargetNamesNC)[mmsi] ==
4560 ship_name) { // Received name same as already in NC list, promote
4561 // to confirmen
4562 (*AISTargetNamesC)[mmsi] = ship_name;
4563 // And delete from NC list
4564 AISTargetNamesNC->erase(itNC);
4565 } else { // entry in NC list is not same as received one
4566 (*AISTargetNamesNC)[mmsi] = ship_name;
4567 }
4568 } else { // No entry in NC list so add it
4569 (*AISTargetNamesNC)[mmsi] = ship_name;
4570 }
4571 if (g_bUseOnlyConfirmedAISName) { // copy back previous name
4572 pTargetData->ShipName[SHIP_NAME_LEN - 1] = '\0';
4573 strncpy(pTargetData->ShipName, "Unknown ",
4574 SHIP_NAME_LEN - 1);
4575 }
4576 }
4577 }
4578 }
4579}
4580
4581wxString GetShipNameFromFile(int nmmsi) {
4582 wxString name = wxEmptyString;
4583 if (g_benableAISNameCache) {
4584 std::ifstream infile(AISTargetNameFileName.mb_str());
4585 if (infile) {
4586 std::string line;
4587 while (getline(infile, line)) {
4588 wxStringTokenizer tokenizer(wxString::FromUTF8(line.c_str()), _T(","));
4589 if (nmmsi == wxAtoi(tokenizer.GetNextToken())) {
4590 name = tokenizer.GetNextToken().Trim();
4591 break;
4592 } else
4593 tokenizer.GetNextToken();
4594 }
4595 }
4596 infile.close();
4597 }
4598 return name;
4599}
4600
4601void AisDecoder::UpdateMMSItoNameFile(const wxString &mmsi,
4602 const wxString &name) {
4603 // Path to the mmsitoname.csv file is already in AISTargetNameFileName
4604
4605 // Create a map to hold the current contents of the file
4606 std::map<wxString, wxString> mmsi_name_map;
4607
4608 // Read the existing file
4609 std::ifstream infile(AISTargetNameFileName.mb_str());
4610 if (infile) {
4611 std::string line;
4612 while (getline(infile, line)) {
4613 wxStringTokenizer tokenizer(wxString::FromUTF8(line.c_str()), _T(","));
4614 wxString file_mmsi = tokenizer.GetNextToken();
4615 wxString file_name = tokenizer.GetNextToken().Trim();
4616 mmsi_name_map[file_mmsi] = file_name;
4617 }
4618 infile.close();
4619 }
4620
4621 // Update or add the new entry.
4622 mmsi_name_map[mmsi] = name.Upper();
4623
4624 // Write the updated map back to the file
4625 std::ofstream outfile(AISTargetNameFileName.mb_str());
4626 if (outfile) {
4627 for (const auto &pair : mmsi_name_map) {
4628 std::string line = std::string(pair.first.mb_str()) + "," +
4629 std::string(pair.second.mb_str()) + "\n";
4630 outfile << line;
4631 }
4632 outfile.close();
4633 }
4634}
4635
4636wxString AisDecoder::GetMMSItoNameEntry(const wxString &mmsi) {
4637 return GetShipNameFromFile(wxAtoi(mmsi));
4638}
4639
4640// Assign a unique meteo mmsi related to position
4641int AisMeteoNewMmsi(int orig_mmsi, int m_lat, int m_lon, int lon_bits = 0,
4642 int siteID = 0) {
4643 bool found = false;
4644 int new_mmsi = 0;
4645 if ((!lon_bits || lon_bits == 999) && siteID) {
4646 // Check if a ais8_367_33 data report belongs to a present site
4647 // Or SignalK data (lon_bits == 999)
4648 auto &points = AisMeteoPoints::GetInstance().GetPoints();
4649 if (points.size()) {
4650 for (const auto &point : points) {
4651 // Does this station ID exist
4652 if (siteID == point.siteID && orig_mmsi == point.orig_mmsi) {
4653 // Created before. Continue
4654 new_mmsi = point.mmsi;
4655 found = true;
4656 break;
4657 }
4658 }
4659 }
4660 if (!found && !lon_bits) {
4661 // ais8_367_33
4662 return 0;
4663 }
4664 }
4665 double lon_tentative = 181.;
4666 double lat_tentative = 91.;
4667
4668 if (lon_bits == 25) {
4669 if (m_lon & 0x01000000) // negative?
4670 m_lon |= 0xFE000000;
4671 lon_tentative = m_lon / 60000.;
4672
4673 if (m_lat & 0x00800000) // negative?
4674 m_lat |= 0xFF000000;
4675 lat_tentative = m_lat / 60000.;
4676
4677 } else if (lon_bits == 28) {
4678 if (m_lon & 0x08000000) // negative?
4679 m_lon |= 0xf0000000;
4680 lon_tentative = m_lon / 600000.;
4681
4682 if (m_lat & 0x04000000) // negative?
4683 m_lat |= 0xf8000000;
4684 lat_tentative = m_lat / 600000.;
4685 }
4686
4687 // Since buoys can move we use position precision not better
4688 // than 50 m to be able to compare previous messages
4689 wxString slon = wxString::Format("%0.3f", lon_tentative);
4690 wxString slat = wxString::Format("%0.3f", lat_tentative);
4691
4692 // Change mmsi_ID number
4693 // Some countries use one equal mmsi for all meteo stations.
4694 // Others use the same mmsi for a meteo station and a nearby AtoN
4695 // So we create our own fake mmsi to separate them.
4696 // 199 is INMARSAT-A MID, should not occur ever in AIS stream.
4697 // 1992 to 1993 are already used so here we use 1994+
4698 static int nextMeteommsi = 199400000;
4699 auto &points = AisMeteoPoints::GetInstance().GetPoints();
4700
4701 if (lon_bits != 999 && !points.empty()) { // 999 comes from SignalK
4702 wxString t_lat, t_lon;
4703 for (const auto &point : points) {
4704 // Does this station position exist
4705 if (slat.IsSameAs(point.lat) && slon.IsSameAs(point.lon)) {
4706 // Created before. Continue
4707 new_mmsi = point.mmsi;
4708 found = true;
4709 break;
4710 }
4711 }
4712 }
4713 if (!found) {
4714 // Create a new post
4715 nextMeteommsi++;
4716 points.emplace_back(
4717 AisMeteoPoint(nextMeteommsi, slat, slon, siteID, orig_mmsi));
4718 new_mmsi = nextMeteommsi;
4719 }
4720 return new_mmsi;
4721}
Class AisDecoder and helpers.
Global state for AIS decoder.
int GetInt(int sp, int len, bool signed_flag=false)
sp is starting bit, 1-based
EventVar plugin_msg
A JSON message should be sent.
EventVar new_track
Notified on new track creation.
EventVar info_update
Notified when AIS user dialogs should update.
EventVar touch_state
Notified when gFrame->TouchAISActive() should be invoked.
Add a new point to the list of Meteo stations.
void Notify() override
Notify all listeners, no data supplied.
Process incoming AIS messages.
Definition ais_decoder.h:73
void LogInputMessage(const std::shared_ptr< const NavMsg > &msg, bool is_filtered, bool is_error, const wxString &error_msg="") const
Logs an input message with context information.
void Notify(std::shared_ptr< const NavMsg > message)
Accept message received by driver, make it available for upper layers.
A regular Nmea0183 message.
See: https://github.com/OpenCPN/OpenCPN/issues/2729#issuecomment-1179506343.
void Listen(const std::string &key, wxEvtHandler *listener, wxEventType evt)
Set object to send wxEventType ev to listener on changes in key.
Custom event class for OpenCPN's notification system.
std::shared_ptr< const void > GetSharedPtr() const
Gets the event's payload data.
Represents a waypoint or mark within the navigation system.
Definition route_point.h:70
A parsed SignalK message over ipv4.
Represents a single point in a track.
Definition track.h:53
Represents a track, which is a series of connected track points.
Definition track.h:111
The JSON document writer.
Definition jsonwriter.h:50
void Write(const wxJSONValue &value, wxString &str)
Write the JSONvalue object to a JSON text.
Raw messages layer, supports sending and recieving navmsg messages.
Multiplexer class and helpers.
A generic position and navigation data structure.
Definition ocpn_types.h:74
double kCog
Course over ground in degrees.
Definition ocpn_types.h:92
double kHdt
True heading in degrees.
Definition ocpn_types.h:117
double kHdm
Magnetic heading in degrees.
Definition ocpn_types.h:110
double kLat
Latitude in decimal degrees.
Definition ocpn_types.h:81
double kSog
Speed over ground in knots.
Definition ocpn_types.h:98
double kVar
Magnetic variation in degrees.
Definition ocpn_types.h:104
double kLon
Longitude in decimal degrees.
Definition ocpn_types.h:89