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