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.specialManeuver") {
2353 if (strcmp("not available", item["value"].GetString()) != 0 &&
2354 pTargetData->IMO < 1) {
2355 const wxString &bluesign = item["value"].GetString();
2356 if ("not engaged" == bluesign) {
2357 pTargetData->blue_paddle = 1;
2358 }
2359 if ("engaged" == bluesign) {
2360 pTargetData->blue_paddle = 2;
2361 }
2362 pTargetData->b_blue_paddle =
2363 pTargetData->blue_paddle == 2 ? true : false;
2364 }
2365 } else if (update_path == "sensors.ais.designatedAreaCode") {
2366 if (item["value"].GetInt() == 200) { // European inland
2367 pTargetData->b_hasInlandDac = true;
2368 }
2369 } else if (update_path == "sensors.ais.functionalId") {
2370 if (item["value"].GetInt() == 10 && pTargetData->b_hasInlandDac) {
2371 // "Inland ship static and voyage related data"
2372 pTargetData->b_isEuroInland = true;
2373 }
2374
2375 // METEO Data
2376 } else if (update_path == "environment.date") {
2377 wxString issued = item["value"].GetString();
2378 if (issued.Len()) {
2379 // Parse ISO 8601 date/time
2380 wxDateTime tz;
2381 ParseGPXDateTime(tz, issued);
2382 pTargetData->met_data.day = tz.GetDay();
2383 pTargetData->met_data.hour = tz.GetHour();
2384 pTargetData->met_data.minute = tz.GetMinute();
2385 }
2386 } else if (update_path == "environment.wind.averageSpeed" &&
2387 item["value"].IsNumber()) {
2388 pTargetData->met_data.wind_kn = MS2KNOTS(item["value"].GetDouble());
2389 } else if (update_path == "environment.wind.gust" &&
2390 item["value"].IsNumber()) {
2391 pTargetData->met_data.wind_gust_kn = MS2KNOTS(item["value"].GetDouble());
2392 } else if (update_path == "environment.wind.directionTrue" &&
2393 item["value"].IsNumber()) {
2394 pTargetData->met_data.wind_dir =
2395 GEODESIC_RAD2DEG(item["value"].GetDouble());
2396 } else if (update_path == "environment.wind.gustDirectionTrue" &&
2397 item["value"].IsNumber()) {
2398 pTargetData->met_data.wind_gust_dir =
2399 GEODESIC_RAD2DEG(item["value"].GetDouble());
2400 } else if (update_path == "environment.outside.temperature" &&
2401 item["value"].IsNumber()) {
2402 pTargetData->met_data.air_temp = KelvinToC(item["value"].GetDouble());
2403 } else if (update_path == "environment.outside.relativeHumidity" &&
2404 item["value"].IsNumber()) {
2405 pTargetData->met_data.rel_humid = item["value"].GetDouble();
2406 } else if (update_path == "environment.outside.dewPointTemperature" &&
2407 item["value"].IsNumber()) {
2408 pTargetData->met_data.dew_point = KelvinToC(item["value"].GetDouble());
2409 } else if (update_path == "environment.outside.pressure" &&
2410 item["value"].IsNumber()) {
2411 pTargetData->met_data.airpress =
2412 static_cast<int>(item["value"].GetDouble() / 100);
2413 } else if (update_path == "environment.water.level" &&
2414 item["value"].IsNumber()) {
2415 pTargetData->met_data.water_lev_dev = item["value"].GetDouble();
2416 } else if (update_path == "environment.water.current.drift" &&
2417 item["value"].IsNumber()) { // surfcurrspd
2418 pTargetData->met_data.current = MS2KNOTS(item["value"].GetDouble());
2419 } else if (update_path == "environment.water.current.set" &&
2420 item["value"].IsNumber()) { // surfcurrdir
2421 pTargetData->met_data.curr_dir =
2422 GEODESIC_RAD2DEG(item["value"].GetDouble());
2423 } else if (update_path == "environment.water.levelTendencyValue" &&
2424 item["value"].IsNumber()) {
2425 pTargetData->met_data.water_lev_trend =
2426 static_cast<int>(item["value"].GetDouble());
2427 } else if (update_path == "environment.water.levelTendency") {
2428 // Don't use this text we parse it ourself.
2429 } else if (update_path == "environment.water.waves.significantHeight" &&
2430 item["value"].IsNumber()) {
2431 pTargetData->met_data.wave_height = item["value"].GetDouble();
2432 } else if (update_path == "environment.water.waves.period" &&
2433 item["value"].IsNumber()) {
2434 pTargetData->met_data.wave_period =
2435 static_cast<int>(item["value"].GetDouble());
2436 } else if (update_path == "environment.water.waves.directionTrue" &&
2437 item["value"].IsNumber()) {
2438 pTargetData->met_data.wave_dir =
2439 GEODESIC_RAD2DEG(item["value"].GetDouble());
2440 } else if (update_path == "environment.water.swell.height" &&
2441 item["value"].IsNumber()) {
2442 pTargetData->met_data.swell_height = item["value"].GetDouble();
2443 } else if (update_path == "environment.water.swell.period" &&
2444 item["value"].IsNumber()) {
2445 pTargetData->met_data.swell_per =
2446 static_cast<int>(item["value"].GetDouble());
2447 } else if (update_path == "environment.water.swell.directionTrue" &&
2448 item["value"].IsNumber()) {
2449 pTargetData->met_data.swell_dir =
2450 GEODESIC_RAD2DEG(item["value"].GetDouble());
2451 } else if (update_path == "environment.water.temperature" &&
2452 item["value"].IsNumber()) {
2453 pTargetData->met_data.water_temp = KelvinToC(item["value"].GetDouble());
2454 } else if (update_path == "environment.water.salinity" &&
2455 item["value"].IsNumber()) {
2456 pTargetData->met_data.salinity = item["value"].GetDouble();
2457 } else if (update_path == "environment.water.ice") {
2458 // Don't use. We parse it ourself
2459 } else if (update_path == "environment.water.iceValue" &&
2460 item["value"].IsNumber()) {
2461 pTargetData->met_data.ice = static_cast<int>(item["value"].GetDouble());
2462 } else if (update_path == "environment.water.seaStateValue" &&
2463 item["value"].IsNumber()) {
2464 pTargetData->met_data.seastate =
2465 static_cast<int>(item["value"].GetDouble());
2466 } else if (update_path == "environment.water.seaState") {
2467 // This is the parsed (air!) Bf-scale. Don't use
2468 } else if (update_path == "environment.outside.precipitation") {
2469 // Don't use. We parse it ourself
2470 } else if (update_path == "environment.outside.precipitationValue" &&
2471 item["value"].IsNumber()) {
2472 pTargetData->met_data.precipitation =
2473 static_cast<int>(item["value"].GetDouble());
2474 } else if (update_path == "environment.outside.pressureTendencyValue" &&
2475 item["value"].IsNumber()) {
2476 pTargetData->met_data.airpress_tend =
2477 static_cast<int>(item["value"].GetDouble());
2478 } else if (update_path == "environment.outside.pressureTendency") {
2479 // Parsed value, don't use, we do it ourself
2480 } else if (update_path == "environment.outside.horizontalVisibility" &&
2481 item["value"].IsNumber()) {
2482 pTargetData->met_data.hor_vis =
2483 GEODESIC_METERS2NM(item["value"].GetDouble());
2484 } else if (update_path ==
2485 "environment.outside.horizontalVisibility.overRange") {
2486 pTargetData->met_data.hor_vis_GT = item["value"].GetBool();
2487 } else if (update_path.empty()) {
2488 if (item["value"].HasMember("name")) {
2489 const wxString &name = item["value"]["name"].GetString();
2490 strncpy(pTargetData->ShipName, name.c_str(), SHIP_NAME_LEN - 1);
2491 pTargetData->b_nameValid = true;
2492 pTargetData->MID = 123; // Indicates a name from SignalK
2493 } else if (item["value"].HasMember("registrations")) {
2494 const wxString &imo = item["value"]["registrations"]["imo"].GetString();
2495 pTargetData->IMO = wxAtoi(imo.Right(7));
2496 } else if (item["value"].HasMember("communication")) {
2497 const wxString &callsign =
2498 item["value"]["communication"]["callsignVhf"].GetString();
2499 strncpy(pTargetData->CallSign, callsign.c_str(), 7);
2500 }
2501 if (item["value"].HasMember("mmsi") &&
2502 1994 != (pTargetData->MMSI) / 100000) { // Meteo
2503 long mmsi;
2504 wxString tmp = item["value"]["mmsi"].GetString();
2505 if (tmp.ToLong(&mmsi)) {
2506 pTargetData->MMSI = mmsi;
2507
2508 if (97 == mmsi / 10000000) {
2509 pTargetData->Class = AIS_SART;
2510 }
2511 if (111 == mmsi / 1000000) {
2512 pTargetData->b_SarAircraftPosnReport = true;
2513 }
2514
2515 AISshipNameCache(pTargetData.get(), AISTargetNamesC, AISTargetNamesNC,
2516 mmsi);
2517 }
2518 }
2519 } else {
2520 wxLogMessage(wxString::Format(
2521 "** AisDecoder::updateItem: unhandled path %s", update_path));
2522#if 1
2523 rapidjson::StringBuffer buffer;
2524 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
2525 item.Accept(writer);
2526 wxString msg("update: ");
2527 msg.append(buffer.GetString());
2528 wxLogMessage(msg);
2529#endif
2530 }
2531 }
2532}
2533
2534//----------------------------------------------------------------------------------
2535// Decode a single AIVDO sentence to a Generic Position Report
2536//----------------------------------------------------------------------------------
2537AisError AisDecoder::DecodeSingleVDO(const wxString &str, GenericPosDatEx *pos,
2538 wxString *accumulator) {
2539 // Make some simple tests for validity
2540 if (str.Len() > 128) return AIS_NMEAVDX_TOO_LONG;
2541
2542 if (!NMEACheckSumOK(str)) return AIS_NMEAVDX_CHECKSUM_BAD;
2543
2544 if (!pos) return AIS_GENERIC_ERROR;
2545
2546 if (!accumulator) return AIS_GENERIC_ERROR;
2547
2548 // We only process AIVDO messages
2549 if (!str.Mid(1, 5).IsSameAs("AIVDO")) return AIS_GENERIC_ERROR;
2550
2551 // Use a tokenizer to pull out the first 4 fields
2552 wxStringTokenizer tkz(str, ",");
2553
2554 wxString token;
2555 token = tkz.GetNextToken(); // !xxVDx
2556
2557 token = tkz.GetNextToken();
2558 int nsentences = atoi(token.mb_str());
2559
2560 token = tkz.GetNextToken();
2561 int isentence = atoi(token.mb_str());
2562
2563 token = tkz.GetNextToken(); // skip 2 fields
2564 token = tkz.GetNextToken();
2565
2566 wxString string_to_parse;
2567 string_to_parse.Clear();
2568
2569 // Fill the output structure with all NANs
2570 pos->kLat = NAN;
2571 pos->kLon = NAN;
2572 pos->kCog = NAN;
2573 pos->kSog = NAN;
2574 pos->kHdt = NAN;
2575 pos->kVar = NAN;
2576 pos->kHdm = NAN;
2577
2578 // Simple case first
2579 // First and only part of a one-part sentence
2580 if ((1 == nsentences) && (1 == isentence)) {
2581 string_to_parse = tkz.GetNextToken(); // the encapsulated data
2582 }
2583
2584 else if (nsentences > 1) {
2585 if (1 == isentence) {
2586 *accumulator = tkz.GetNextToken(); // the encapsulated data
2587 }
2588
2589 else {
2590 accumulator->Append(tkz.GetNextToken());
2591 }
2592
2593 if (isentence == nsentences) { // ready to parse
2594 string_to_parse = *accumulator;
2595 }
2596 }
2597
2598 if (string_to_parse.IsEmpty() &&
2599 (nsentences > 1)) { // not ready, so return with NAN
2600 return AIS_INCOMPLETE_MULTIPART; // and non-zero return
2601 }
2602
2603 // Create the bit accessible string
2604 AisBitstring strbit(string_to_parse.mb_str());
2605
2606 // auto TargetData = std::make_unique<AisTargetData>(
2607 // *AisTargetDataMaker::GetInstance().GetTargetData());
2608
2609 auto TargetData = AisTargetDataMaker::GetInstance().GetTargetData();
2610
2611 bool bdecode_result = Parse_VDXBitstring(&strbit, TargetData);
2612
2613 if (bdecode_result) {
2614 switch (TargetData->MID) {
2615 case 1:
2616 case 2:
2617 case 3:
2618 case 18: {
2619 if (!TargetData->b_positionDoubtful) {
2620 pos->kLat = TargetData->Lat;
2621 pos->kLon = TargetData->Lon;
2622 } else {
2623 pos->kLat = NAN;
2624 pos->kLon = NAN;
2625 }
2626
2627 if (TargetData->COG == 360.0)
2628 pos->kCog = NAN;
2629 else
2630 pos->kCog = TargetData->COG;
2631
2632 if (TargetData->SOG > 102.2)
2633 pos->kSog = NAN;
2634 else
2635 pos->kSog = TargetData->SOG;
2636
2637 if ((int)TargetData->HDG == 511)
2638 pos->kHdt = NAN;
2639 else
2640 pos->kHdt = TargetData->HDG;
2641
2642 // VDO messages do not contain variation or magnetic heading
2643 pos->kVar = NAN;
2644 pos->kHdm = NAN;
2645 break;
2646 }
2647 default:
2648 return AIS_GENERIC_ERROR; // unrecognised sentence
2649 }
2650
2651 return AIS_NoError;
2652 } else
2653 return AIS_GENERIC_ERROR;
2654}
2655
2656//----------------------------------------------------------------------------------------
2657// Decode NMEA VDM/VDO/FRPOS/DSCDSE/TTM/TLL/OSD/RSD/TLB/WPL sentence to AIS
2658// Target(s)
2659//----------------------------------------------------------------------------------------
2660
2661AisError AisDecoder::DecodeN0183(const wxString &str) {
2662 AisError ret = AIS_GENERIC_ERROR;
2663 wxString string_to_parse;
2664
2665 double gpsg_lat, gpsg_lon, gpsg_mins, gpsg_degs;
2666 double gpsg_cog, gpsg_sog, gpsg_utc_time;
2667 int gpsg_utc_hour = 0;
2668 int gpsg_utc_min = 0;
2669 int gpsg_utc_sec = 0;
2670 char gpsg_name_str[21];
2671 wxString gpsg_date;
2672
2673 bool bdecode_result = false;
2674
2675 int gpsg_mmsi = 0;
2676 int arpa_mmsi = 0;
2677 int aprs_mmsi = 0;
2678 unsigned mmsi = 0;
2679
2680 long arpa_tgt_num = 0;
2681 double arpa_sog = 0.;
2682 double arpa_cog = 0.;
2683 double arpa_lat = 0.;
2684 double arpa_lon = 0.;
2685 double arpa_dist = 0.;
2686 double arpa_brg = 0.;
2687 wxString arpa_brgunit;
2688 wxString arpa_status;
2689 wxString arpa_distunit;
2690 wxString arpa_cogunit;
2691 double arpa_mins, arpa_degs;
2692 double arpa_utc_time;
2693 int arpa_utc_hour = 0;
2694 int arpa_utc_min = 0;
2695 int arpa_utc_sec = 0;
2696 char arpa_name_str[21];
2697 bool arpa_lost = true;
2698 bool arpa_nottracked = false;
2699
2700 double aprs_lat = 0.;
2701 double aprs_lon = 0.;
2702 char aprs_name_str[21];
2703 double aprs_mins, aprs_degs;
2704
2705 std::shared_ptr<AisTargetData> pTargetData = nullptr;
2706 std::shared_ptr<AisTargetData> pStaleTarget = nullptr;
2707 bool bnewtarget = false;
2708 int last_report_ticks;
2709
2710 // Make some simple tests for validity
2711
2712 if (str.Len() > 128) return AIS_NMEAVDX_TOO_LONG;
2713
2714 if (!NMEACheckSumOK(str)) {
2715 return AIS_NMEAVDX_CHECKSUM_BAD;
2716 }
2717 if (str.Mid(1, 2).IsSameAs("CD")) {
2718 ProcessDSx(str);
2719 return AIS_NoError;
2720 } else if (str.Mid(3, 3).IsSameAs("TTM")) {
2721 //$--TTM,xx,x.x,x.x,a,x.x,x.x,a,x.x,x.x,a,c--c,a,a*hh <CR><LF>
2722 // or
2723 //$--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>
2724 wxStringTokenizer tkz(str, ",*");
2725
2726 wxString token;
2727 token = tkz.GetNextToken(); // Sentence (xxTTM)
2728 token = tkz.GetNextToken(); // 1) Target Number
2729 token.ToLong(&arpa_tgt_num);
2730 token = tkz.GetNextToken(); // 2)Target Distance
2731 token.ToDouble(&arpa_dist);
2732 token = tkz.GetNextToken(); // 3) Bearing from own ship
2733 token.ToDouble(&arpa_brg);
2734 arpa_brgunit = tkz.GetNextToken(); // 4) Bearing Units
2735 if (arpa_brgunit == "R") {
2736 if (std::isnan(arpa_ref_hdg)) {
2737 if (!std::isnan(gHdt))
2738 arpa_brg += gHdt;
2739 else
2740 arpa_brg += gCog;
2741 } else
2742 arpa_brg += arpa_ref_hdg;
2743 if (arpa_brg >= 360.) arpa_brg -= 360.;
2744 }
2745 token = tkz.GetNextToken(); // 5) Target speed
2746 token.ToDouble(&arpa_sog);
2747 token = tkz.GetNextToken(); // 6) Target Course
2748 token.ToDouble(&arpa_cog);
2749 arpa_cogunit = tkz.GetNextToken(); // 7) Course Units
2750 if (arpa_cogunit == "R") {
2751 if (std::isnan(arpa_ref_hdg)) {
2752 if (!std::isnan(gHdt))
2753 arpa_cog += gHdt;
2754 else
2755 arpa_cog += gCog;
2756 } else
2757 arpa_cog += arpa_ref_hdg;
2758 if (arpa_cog >= 360.) arpa_cog -= 360.;
2759 }
2760 token = tkz.GetNextToken(); // 8) Distance of closest-point-of-approach
2761 token = tkz.GetNextToken(); // 9) Time until closest-point-of-approach "-"
2762 // means increasing
2763 arpa_distunit = tkz.GetNextToken(); // 10)Speed/ dist unit
2764 token = tkz.GetNextToken(); // 11) Target name
2765 if (token == "") token = wxString::Format("ARPA %ld", arpa_tgt_num);
2766 int len = wxMin(token.Length(), 20);
2767 strncpy(arpa_name_str, token.mb_str(), len);
2768 arpa_name_str[len] = 0;
2769 arpa_status = tkz.GetNextToken(); // 12) Target Status
2770 if (arpa_status != "L") {
2771 arpa_lost = false;
2772 } else if (arpa_status != "")
2773 arpa_nottracked = true;
2774 wxString arpa_reftarget = tkz.GetNextToken(); // 13) Reference Target
2775 if (tkz.HasMoreTokens()) {
2776 token = tkz.GetNextToken();
2777 token.ToDouble(&arpa_utc_time);
2778 arpa_utc_hour = (int)(arpa_utc_time / 10000.0);
2779 arpa_utc_min = (int)(arpa_utc_time / 100.0) - arpa_utc_hour * 100;
2780 arpa_utc_sec =
2781 (int)arpa_utc_time - arpa_utc_hour * 10000 - arpa_utc_min * 100;
2782 } else {
2783 arpa_utc_hour = wxDateTime::Now().ToUTC().GetHour();
2784 arpa_utc_min = wxDateTime::Now().ToUTC().GetMinute();
2785 arpa_utc_sec = wxDateTime::Now().ToUTC().GetSecond();
2786 }
2787
2788 if (arpa_distunit == "K") {
2789 arpa_dist = fromUsrDistance(arpa_dist, DISTANCE_KM, g_iDistanceFormat);
2790 arpa_sog = fromUsrSpeed(arpa_sog, SPEED_KMH, g_iSpeedFormat);
2791 } else if (arpa_distunit == "S") {
2792 arpa_dist = fromUsrDistance(arpa_dist, DISTANCE_MI, g_iDistanceFormat);
2793 arpa_sog = fromUsrSpeed(arpa_sog, SPEED_MPH, g_iSpeedFormat);
2794 }
2795
2796 mmsi = arpa_mmsi = 199200000 + arpa_tgt_num;
2797 // 199 is INMARSAT-A MID, should not occur ever in AIS
2798 // stream + we make sure we are out of the hashes for
2799 // GPSGate buddies by being above 1992*
2800 } else if (str.Mid(3, 3).IsSameAs("TLL")) {
2801 //$--TLL,xx,llll.lll,a,yyyyy.yyy,a,c--c,hhmmss.ss,a,a*hh<CR><LF>
2802 //"$RATLL,01,5603.370,N,01859.976,E,ALPHA,015200.36,T,*75\r\n"
2803 wxStringTokenizer tkz(str, ",*");
2804
2805 wxString token;
2806 wxString aprs_tll_str = tkz.GetNextToken(); // Sentence (xxTLL)
2807 token = tkz.GetNextToken(); // 1) Target number 00 - 99
2808 token.ToLong(&arpa_tgt_num);
2809 token = tkz.GetNextToken(); // 2) Latitude, N/S
2810 token.ToDouble(&arpa_lat);
2811 arpa_degs = (int)(arpa_lat / 100.0);
2812 arpa_mins = arpa_lat - arpa_degs * 100.0;
2813 arpa_lat = arpa_degs + arpa_mins / 60.0;
2814 token = tkz.GetNextToken(); // hemisphere N or S
2815 if (token.Mid(0, 1).Contains("S") == true ||
2816 token.Mid(0, 1).Contains("s") == true)
2817 arpa_lat = 0. - arpa_lat;
2818 token = tkz.GetNextToken(); // 3) Longitude, E/W
2819 token.ToDouble(&arpa_lon);
2820 arpa_degs = (int)(arpa_lon / 100.0);
2821 arpa_mins = arpa_lon - arpa_degs * 100.0;
2822 arpa_lon = arpa_degs + arpa_mins / 60.0;
2823 token = tkz.GetNextToken(); // hemisphere E or W
2824 if (token.Mid(0, 1).Contains("W") == true ||
2825 token.Mid(0, 1).Contains("w") == true)
2826 arpa_lon = 0. - arpa_lon;
2827 token = tkz.GetNextToken(); // 4) Target name
2828 if (token == "") token = wxString::Format("ARPA %d", arpa_tgt_num);
2829 int len = wxMin(token.Length(), 20);
2830 strncpy(arpa_name_str, token.mb_str(), len);
2831 arpa_name_str[len] = 0;
2832 token = tkz.GetNextToken(); // 5) UTC of data
2833 token.ToDouble(&arpa_utc_time);
2834 arpa_utc_hour = (int)(arpa_utc_time / 10000.0);
2835 arpa_utc_min = (int)(arpa_utc_time / 100.0) - arpa_utc_hour * 100;
2836 arpa_utc_sec =
2837 (int)arpa_utc_time - arpa_utc_hour * 10000 - arpa_utc_min * 100;
2838 arpa_status =
2839 tkz.GetNextToken(); // 6) Target status: L = lost,tracked
2840 // target has beenlost Q = query,target in
2841 // the process of acquisition T = tracking
2842 if (arpa_status != "L")
2843 arpa_lost = false;
2844 else if (arpa_status != "")
2845 arpa_nottracked = true;
2846 wxString arpa_reftarget = tkz.GetNextToken(); // 7) Reference target=R,null
2847 mmsi = arpa_mmsi = 199200000 + arpa_tgt_num;
2848 // 199 is INMARSAT-A MID, should not occur ever in AIS
2849 // stream + we make sure we are out of the hashes for
2850 // GPSGate buddies by being above 1992*
2851 } else if (str.Mid(3, 3).IsSameAs("OSD")) {
2852 //$--OSD,x.x,A,x.x,a,x.x,a,x.x,x.x,a*hh <CR><LF>
2853 wxStringTokenizer tkz(str, ",*");
2854
2855 wxString token;
2856 token = tkz.GetNextToken(); // Sentence (xxOSD)
2857 token = tkz.GetNextToken(); // 1) Heading (true)
2858 token.ToDouble(&arpa_ref_hdg);
2859 // 2) speed
2860 // 3) Vessel Course, degrees True
2861 // 4) Course Reference, B/M/W/R/P (see note)
2862 // 5) Vessel Speed
2863 // 6) Speed Reference, B/M/W/R/P (see note)
2864 // 7) Vessel Set, degrees True - Manually entered
2865 // 8) Vessel drift (speed) - Manually entered
2866 // 9) Speed Units K = km/h; N = Knots; S = statute miles/h
2867
2868 } else if (g_bWplUsePosition && str.Mid(3, 3).IsSameAs("WPL")) {
2869 //** $--WPL,llll.ll,a,yyyyy.yy,a,c--c*hh<CR><LF>
2870 wxStringTokenizer tkz(str, ",*");
2871
2872 wxString token;
2873 token = tkz.GetNextToken(); // Sentence (xxWPL)
2874 token = tkz.GetNextToken(); // 1) Latitude, N/S
2875 token.ToDouble(&aprs_lat);
2876 aprs_degs = (int)(aprs_lat / 100.0);
2877 aprs_mins = aprs_lat - aprs_degs * 100.0;
2878 aprs_lat = aprs_degs + aprs_mins / 60.0;
2879 token = tkz.GetNextToken(); // 2) hemisphere N or S
2880 if (token.Mid(0, 1).Contains("S") == true ||
2881 token.Mid(0, 1).Contains("s") == true)
2882 aprs_lat = 0. - aprs_lat;
2883 token = tkz.GetNextToken(); // 3) Longitude, E/W
2884 token.ToDouble(&aprs_lon);
2885 aprs_degs = (int)(aprs_lon / 100.0);
2886 aprs_mins = aprs_lon - aprs_degs * 100.0;
2887 aprs_lon = aprs_degs + aprs_mins / 60.0;
2888 token = tkz.GetNextToken(); // 4) hemisphere E or W
2889 if (token.Mid(0, 1).Contains("W") == true ||
2890 token.Mid(0, 1).Contains("w") == true)
2891 aprs_lon = 0. - aprs_lon;
2892 token = tkz.GetNextToken(); // 5) Target name
2893 int len = wxMin(token.Length(), 20);
2894 strncpy(aprs_name_str, token.mb_str(), len + 1);
2895 if (0 == g_WplAction) { // APRS position reports
2896 int i, hash = 0;
2897 aprs_name_str[len] = 0;
2898 for (i = 0; i < len; i++) {
2899 hash = hash * 10;
2900 hash += (int)(aprs_name_str[i]);
2901 while (hash >= 100000) hash = hash / 100000;
2902 }
2903 mmsi = aprs_mmsi = 199300000 + hash;
2904 // 199 is INMARSAT-A MID, should not occur ever in AIS stream +
2905 // we make sure we are out of the hashes for GPSGate buddies
2906 // and ARPA by being above 1993*
2907 } else if (1 == g_WplAction) { // Create mark
2908 auto *pWP =
2909 new RoutePoint(aprs_lat, aprs_lon, "", aprs_name_str, "", false);
2910 pWP->m_bIsolatedMark = true;
2911 InsertWpt(pWP, true);
2912 }
2913 } else if (str.Mid(1, 5).IsSameAs("FRPOS")) {
2914 // parse a GpsGate Position message $FRPOS,.....
2915
2916 // Use a tokenizer to pull out the first 9 fields
2917 wxStringTokenizer tkz(str, ",*");
2918
2919 wxString token;
2920 token = tkz.GetNextToken(); // !$FRPOS
2921
2922 token = tkz.GetNextToken(); // latitude DDMM.MMMM
2923 token.ToDouble(&gpsg_lat);
2924 gpsg_degs = (int)(gpsg_lat / 100.0);
2925 gpsg_mins = gpsg_lat - gpsg_degs * 100.0;
2926 gpsg_lat = gpsg_degs + gpsg_mins / 60.0;
2927
2928 token = tkz.GetNextToken(); // hemisphere N or S
2929 if (token.Mid(0, 1).Contains("S") == true ||
2930 token.Mid(0, 1).Contains("s") == true)
2931 gpsg_lat = 0. - gpsg_lat;
2932
2933 token = tkz.GetNextToken(); // longitude DDDMM.MMMM
2934 token.ToDouble(&gpsg_lon);
2935 gpsg_degs = (int)(gpsg_lon / 100.0);
2936 gpsg_mins = gpsg_lon - gpsg_degs * 100.0;
2937 gpsg_lon = gpsg_degs + gpsg_mins / 60.0;
2938
2939 token = tkz.GetNextToken(); // hemisphere E or W
2940 if (token.Mid(0, 1).Contains("W") == true ||
2941 token.Mid(0, 1).Contains("w") == true)
2942 gpsg_lon = 0. - gpsg_lon;
2943
2944 token = tkz.GetNextToken(); // altitude AA.a
2945 // token.toDouble(&gpsg_alt);
2946
2947 token = tkz.GetNextToken(); // speed over ground SSS.SS knots
2948 token.ToDouble(&gpsg_sog);
2949
2950 token = tkz.GetNextToken(); // heading over ground HHH.hh degrees
2951 token.ToDouble(&gpsg_cog);
2952
2953 token = tkz.GetNextToken(); // date DDMMYY
2954 gpsg_date = token;
2955
2956 token = tkz.GetNextToken(); // time UTC hhmmss.dd
2957 token.ToDouble(&gpsg_utc_time);
2958 gpsg_utc_hour = (int)(gpsg_utc_time / 10000.0);
2959 gpsg_utc_min = (int)(gpsg_utc_time / 100.0) - gpsg_utc_hour * 100;
2960 gpsg_utc_sec =
2961 (int)gpsg_utc_time - gpsg_utc_hour * 10000 - gpsg_utc_min * 100;
2962
2963 // now comes the name, followed by in * and NMEA checksum
2964
2965 token = tkz.GetNextToken();
2966 int i, len, hash = 0;
2967 len = wxMin(wxStrlen(token), 20);
2968 strncpy(gpsg_name_str, token.mb_str(), len);
2969 gpsg_name_str[len] = 0;
2970 for (i = 0; i < len; i++) {
2971 hash = hash * 10;
2972 hash += (int)(token[i]);
2973 while (hash >= 100000) hash = hash / 100000;
2974 }
2975 // 199 is INMARSAT-A MID, should not occur ever in AIS stream
2976 gpsg_mmsi = 199000000 + hash;
2977 mmsi = gpsg_mmsi;
2978 } else if (!str.Mid(3, 2).IsSameAs("VD")) {
2979 return AIS_NMEAVDX_BAD;
2980 }
2981
2982 // OK, looks like the sentence is OK
2983
2984 // Use a tokenizer to pull out the first 4 fields
2985 string_to_parse.Clear();
2986
2987 if (str.Mid(3, 2).IsSameAs("VD")) {
2988 wxStringTokenizer tkz(str, ",");
2989
2990 wxString token;
2991 token = tkz.GetNextToken(); // !xxVDx
2992
2993 token = tkz.GetNextToken();
2994 nsentences = atoi(token.mb_str());
2995
2996 token = tkz.GetNextToken();
2997 isentence = atoi(token.mb_str());
2998
2999 token = tkz.GetNextToken();
3000 long lsequence_id = 0;
3001 token.ToLong(&lsequence_id);
3002
3003 token = tkz.GetNextToken();
3004 long lchannel;
3005 token.ToLong(&lchannel);
3006 // Now, some decisions
3007
3008 // Simple case first
3009 // First and only part of a one-part sentence
3010 if ((1 == nsentences) && (1 == isentence)) {
3011 string_to_parse = tkz.GetNextToken(); // the encapsulated data
3012 }
3013
3014 else if (nsentences > 1) {
3015 if (1 == isentence) {
3016 sentence_accumulator = tkz.GetNextToken(); // the encapsulated data
3017 }
3018
3019 else {
3020 sentence_accumulator += tkz.GetNextToken();
3021 }
3022
3023 if (isentence == nsentences) {
3024 string_to_parse = sentence_accumulator;
3025 }
3026 }
3027 }
3028
3029 if (mmsi || (!string_to_parse.IsEmpty() &&
3030 (string_to_parse.Len() < AIS_MAX_MESSAGE_LEN))) {
3031 // Create the bit accessible string
3032 wxCharBuffer abuf = string_to_parse.ToUTF8();
3033 if (!abuf.data()) // badly formed sentence?
3034 return AIS_GENERIC_ERROR;
3035
3036 AisBitstring strbit(abuf.data());
3037
3038 // Extract the MMSI
3039 if (!mmsi) mmsi = strbit.GetInt(9, 30);
3040 long mmsi_long = mmsi;
3041
3042 // Ais8_001_31 || ais8_367_33 (class AIS_METEO) test for a new mmsi ID
3043 int origin_mmsi = 0;
3044 int messID = strbit.GetInt(1, 6);
3045 int dac = strbit.GetInt(41, 10);
3046 int fi = strbit.GetInt(51, 6);
3047 if (messID == 8) {
3048 int met_lon, met_lat;
3049 if (dac == 001 && fi == 31) {
3050 origin_mmsi = mmsi;
3051 met_lon = strbit.GetInt(57, 25);
3052 met_lat = strbit.GetInt(82, 24);
3053 mmsi = AisMeteoNewMmsi(mmsi, met_lat, met_lon, 25, 0);
3054 mmsi_long = mmsi;
3055
3056 } else if (dac == 367 && fi == 33) { // ais8_367_33
3057 // Check for a valid message size before further handling
3058 const int size = strbit.GetBitCount();
3059 if (size < 168) return AIS_GENERIC_ERROR;
3060 const int startb = 56;
3061 const int slot_size = 112;
3062 const int extra_bits = (size - startb) % slot_size;
3063 if (extra_bits > 0) return AIS_GENERIC_ERROR;
3064
3065 int mes_type = strbit.GetInt(57, 4);
3066 int site_ID = strbit.GetInt(77, 7);
3067 if (mes_type == 0) { // Location
3068 origin_mmsi = mmsi;
3069 met_lon = strbit.GetInt(90, 28);
3070 met_lat = strbit.GetInt(118, 27);
3071 mmsi = AisMeteoNewMmsi(mmsi, met_lat, met_lon, 28, site_ID);
3072 mmsi_long = mmsi;
3073 } else { // Other messsage types without position.
3074 // We need a previously received type 0, position message
3075 // to get use of any sensor report.
3076 int x_mmsi = AisMeteoNewMmsi(mmsi, 91, 181, 0, site_ID);
3077 if (x_mmsi) {
3078 origin_mmsi = mmsi;
3079 mmsi = x_mmsi;
3080 mmsi_long = mmsi;
3081 } else // So far no use for this report.
3082 return AIS_GENERIC_ERROR;
3083 }
3084 }
3085 }
3086
3087 // Check for own ship mmsi. It's not a valid AIS target.
3088 if (mmsi == g_OwnShipmmsi) return AIS_GENERIC_ERROR;
3089
3090 // Search the current AISTargetList for an MMSI match
3091 auto it = AISTargetList.find(mmsi);
3092 if (it == AISTargetList.end()) // not found
3093 {
3094 pTargetData = AisTargetDataMaker::GetInstance().GetTargetData();
3095 bnewtarget = true;
3096 m_n_targets++;
3097
3098 if (origin_mmsi) { // New mmsi allocated for a Meteo station
3099 pTargetData->MMSI = mmsi;
3100 pTargetData->met_data.original_mmsi = origin_mmsi;
3101 }
3102 } else {
3103 pTargetData = it->second; // find current entry
3104
3105 if (!bnewtarget)
3106 pStaleTarget = pTargetData; // save a pointer to stale data
3107 if (origin_mmsi) { // Meteo point
3108 pTargetData->MMSI = mmsi;
3109 pTargetData->met_data.original_mmsi = origin_mmsi;
3110 }
3111 }
3112 for (unsigned int i = 0; i < g_MMSI_Props_Array.GetCount(); i++) {
3114 if (mmsi == static_cast<unsigned>(props->MMSI)) {
3115 // Check if this target has a dedicated tracktype
3116 if (TRACKTYPE_NEVER == props->TrackType) {
3117 pTargetData->b_show_track = false;
3118 } else if (TRACKTYPE_ALWAYS == props->TrackType) {
3119 pTargetData->b_show_track = true;
3120 }
3121
3122 // Check to see if this MMSI has been configured to be ignored
3123 // completely...
3124 if (props->m_bignore) return AIS_NoError;
3125 // Check to see if this MMSI wants VDM translated to VDO or whether we
3126 // want to persist it's track...
3127 else if (props->m_bVDM) {
3128 // Only single line VDM messages to be translated
3129 if (str.Mid(3, 9).IsSameAs("VDM,1,1,,")) {
3130 int message_ID = strbit.GetInt(1, 6); // Parse on message ID
3131 // Only translate the dynamic positionreport messages (1, 2, 3 or
3132 // 18)
3133 if ((message_ID <= 3) || (message_ID == 18)) {
3134 // set OwnShip to prevent target from being drawn
3135 pTargetData->b_OwnShip = true;
3136 // Rename nmea sentence to AIVDO and calc a new checksum
3137 wxString aivdostr = str;
3138 aivdostr.replace(1, 5, "AIVDO");
3139 unsigned char calculated_checksum = 0;
3140 wxString::iterator j;
3141 for (j = aivdostr.begin() + 1; j != aivdostr.end() && *j != '*';
3142 ++j)
3143 calculated_checksum ^= static_cast<unsigned char>(*j);
3144 // if i is not at least 3 positons befoere end, there is no
3145 // checksum added so also no need to add one now.
3146 if (j <= aivdostr.end() - 3)
3147 aivdostr.replace(
3148 j + 1, j + 3,
3149 wxString::Format(_("%02X"), calculated_checksum));
3150
3151 gps_watchdog_timeout_ticks =
3152 60; // increase watchdog time up to 1 minute
3153 // add the changed sentence into nmea message system
3154 std::string full_sentence = aivdostr.ToStdString();
3155 auto address = std::make_shared<NavAddr0183>("virtual");
3156 // We notify based on full message, including the Talker ID
3157 // Notify message listener
3158 auto msg = std::make_shared<const Nmea0183Msg>(
3159 "AIVDO", full_sentence, address);
3160 NavMsgBus::GetInstance().Notify(std::move(msg));
3161 }
3162 }
3163 return AIS_NoError;
3164 } else
3165 break;
3166 }
3167 }
3168
3169 // Grab the stale targets's last report time
3170 wxDateTime now = wxDateTime::Now();
3171 now.MakeGMT();
3172
3173 if (pStaleTarget)
3174 last_report_ticks = pStaleTarget->PositionReportTicks;
3175 else
3176 last_report_ticks = now.GetTicks();
3177
3178 // Delete the stale AIS Target selectable point
3179 if (pStaleTarget)
3180 pSelectAIS->DeleteSelectablePoint((void *)mmsi_long, SELTYPE_AISTARGET);
3181
3182 if (pTargetData) {
3183 if (gpsg_mmsi) {
3184 pTargetData->LastPositionReportTicks = pTargetData->PositionReportTicks;
3185 pTargetData->PositionReportTicks = now.GetTicks();
3186 pTargetData->StaticReportTicks = now.GetTicks();
3187 pTargetData->m_utc_hour = gpsg_utc_hour;
3188 pTargetData->m_utc_min = gpsg_utc_min;
3189 pTargetData->m_utc_sec = gpsg_utc_sec;
3190 pTargetData->m_date_string = gpsg_date;
3191 pTargetData->MMSI = gpsg_mmsi;
3192 pTargetData->NavStatus = 0; // underway
3193 pTargetData->Lat = gpsg_lat;
3194 pTargetData->Lon = gpsg_lon;
3195 pTargetData->b_positionOnceValid = true;
3196 pTargetData->COG = gpsg_cog;
3197 pTargetData->SOG = gpsg_sog;
3198 pTargetData->ShipType = 52; // buddy
3199 pTargetData->Class = AIS_GPSG_BUDDY;
3200 memcpy(pTargetData->ShipName, gpsg_name_str, sizeof(gpsg_name_str));
3201 pTargetData->b_nameValid = true;
3202 pTargetData->b_active = true;
3203 pTargetData->b_lost = false;
3204
3205 bdecode_result = true;
3206 } else if (arpa_mmsi) {
3207 pTargetData->m_utc_hour = arpa_utc_hour;
3208 pTargetData->m_utc_min = arpa_utc_min;
3209 pTargetData->m_utc_sec = arpa_utc_sec;
3210 pTargetData->MMSI = arpa_mmsi;
3211 pTargetData->NavStatus = 15; // undefined
3212 if (str.Mid(3, 3).IsSameAs("TLL")) {
3213 if (!bnewtarget) {
3214 int age_of_last =
3215 (now.GetTicks() - pTargetData->PositionReportTicks);
3216 if (age_of_last > 0) {
3217 ll_gc_ll_reverse(pTargetData->Lat, pTargetData->Lon, arpa_lat,
3218 arpa_lon, &pTargetData->COG, &pTargetData->SOG);
3219 pTargetData->SOG = pTargetData->SOG * 3600 / age_of_last;
3220 }
3221 }
3222 pTargetData->Lat = arpa_lat;
3223 pTargetData->Lon = arpa_lon;
3224 } else if (str.Mid(3, 3).IsSameAs("TTM")) {
3225 if (arpa_dist != 0.) // Not a new or turned off target
3226 ll_gc_ll(gLat, gLon, arpa_brg, arpa_dist, &pTargetData->Lat,
3227 &pTargetData->Lon);
3228 else
3229 arpa_lost = true;
3230 pTargetData->COG = arpa_cog;
3231 pTargetData->SOG = arpa_sog;
3232 }
3233 pTargetData->LastPositionReportTicks = pTargetData->PositionReportTicks;
3234 pTargetData->PositionReportTicks = now.GetTicks();
3235 pTargetData->StaticReportTicks = now.GetTicks();
3236 pTargetData->b_positionOnceValid = true;
3237 pTargetData->ShipType = 55; // arpa
3238 pTargetData->Class = AIS_ARPA;
3239
3240 memcpy(pTargetData->ShipName, arpa_name_str, sizeof(arpa_name_str));
3241 if (arpa_status != "Q")
3242 pTargetData->b_nameValid = true;
3243 else
3244 pTargetData->b_nameValid = false;
3245 pTargetData->b_active = !arpa_lost;
3246 pTargetData->b_lost = arpa_nottracked;
3247
3248 bdecode_result = true;
3249 } else if (aprs_mmsi) {
3250 pTargetData->m_utc_hour = now.GetHour();
3251 pTargetData->m_utc_min = now.GetMinute();
3252 pTargetData->m_utc_sec = now.GetSecond();
3253 pTargetData->MMSI = aprs_mmsi;
3254 pTargetData->NavStatus = 15; // undefined
3255 if (!bnewtarget) {
3256 int age_of_last = (now.GetTicks() - pTargetData->PositionReportTicks);
3257 if (age_of_last > 0) {
3258 ll_gc_ll_reverse(pTargetData->Lat, pTargetData->Lon, aprs_lat,
3259 aprs_lon, &pTargetData->COG, &pTargetData->SOG);
3260 pTargetData->SOG = pTargetData->SOG * 3600 / age_of_last;
3261 }
3262 }
3263 pTargetData->LastPositionReportTicks = pTargetData->PositionReportTicks;
3264 pTargetData->PositionReportTicks = now.GetTicks();
3265 pTargetData->StaticReportTicks = now.GetTicks();
3266 pTargetData->Lat = aprs_lat;
3267 pTargetData->Lon = aprs_lon;
3268 pTargetData->b_positionOnceValid = true;
3269 pTargetData->ShipType = 56; // aprs
3270 pTargetData->Class = AIS_APRS;
3271 memcpy(pTargetData->ShipName, aprs_name_str, sizeof(aprs_name_str));
3272 pTargetData->b_nameValid = true;
3273 pTargetData->b_active = true;
3274 pTargetData->b_lost = false;
3275
3276 bdecode_result = true;
3277 } else {
3278 // The normal Plain-Old AIS target code path....
3279 bdecode_result =
3280 Parse_VDXBitstring(&strbit, pTargetData); // Parse the new data
3281 }
3282
3283 // Catch mmsi properties like track, persistent track, follower.
3284 getMmsiProperties(pTargetData);
3285
3286 // Update the most recent report period
3287 pTargetData->RecentPeriod =
3288 pTargetData->PositionReportTicks - last_report_ticks;
3289 }
3290 ret = AIS_NoError;
3291 } else {
3292 ret = AIS_Partial; // accumulating parts of a multi-sentence message
3293 pTargetData = nullptr;
3294 }
3295
3296 if (pTargetData) {
3297 // pTargetData is valid, either new or existing. Commit to GUI
3298 CommitAISTarget(pTargetData, str, bdecode_result, bnewtarget);
3299 }
3300
3301 n_msgs++;
3302#ifdef AIS_DEBUG
3303 if ((n_msgs % 10000) == 0)
3304 printf("n_msgs %10d m_n_targets: %6d n_msg1: %10d n_msg5+24: %10d \n",
3305 n_msgs, m_n_targets, n_msg1, n_msg5 + n_msg24);
3306#endif
3307
3308 return ret;
3309}
3310
3311void AisDecoder::CommitAISTarget(
3312 const std::shared_ptr<AisTargetData> &pTargetData, const wxString &str,
3313 bool message_valid, bool new_target) {
3314 m_pLatestTargetData = pTargetData;
3315
3316 if (!str.IsEmpty()) { // NMEA0183 message
3317 if (str.Mid(3, 3).IsSameAs("VDO"))
3318 pTargetData->b_OwnShip = true;
3319 else
3320 pTargetData->b_OwnShip = false;
3321 }
3322
3323 if (!pTargetData->b_OwnShip) {
3324 // set mmsi-props to default values
3325 if (0 == m_persistent_tracks.count(pTargetData->MMSI)) {
3326 // Normal target
3327 pTargetData->b_PersistTrack = false;
3328 // Or first decode for this target
3329 for (unsigned int i = 0; i < g_MMSI_Props_Array.GetCount(); i++) {
3330 if (pTargetData->MMSI == g_MMSI_Props_Array[i]->MMSI) {
3332 pTargetData->b_mPropPersistTrack = props->m_bPersistentTrack;
3333 break;
3334 }
3335 }
3336 } else {
3337 // The track persistency enabled in the query window or mmsi-props
3338 }
3339 pTargetData->b_NoTrack = false;
3340 }
3341
3342 // If the message was decoded correctly
3343 // Update the AIS Target information
3344 if (message_valid) {
3345 // Print to name cache only if not mmsi = 0
3346 if (pTargetData->MMSI) {
3347 AISshipNameCache(pTargetData.get(), AISTargetNamesC, AISTargetNamesNC,
3348 pTargetData->MMSI);
3349 }
3350 AISTargetList[pTargetData->MMSI] =
3351 pTargetData; // update the hash table entry
3352
3353 if (!pTargetData->area_notices.empty()) {
3354 auto it = AIS_AreaNotice_Sources.find(pTargetData->MMSI);
3355 if (it == AIS_AreaNotice_Sources.end())
3356 AIS_AreaNotice_Sources[pTargetData->MMSI] = pTargetData;
3357 }
3358
3359 // If this is not an ownship message, update the AIS Target in the
3360 // Selectable list, and update the CPA info
3361 if (!pTargetData->b_OwnShip) {
3362 if (pTargetData->b_positionOnceValid) {
3363 long mmsi_long = pTargetData->MMSI;
3364 SelectItem *pSel = pSelectAIS->AddSelectablePoint(
3365 pTargetData->Lat, pTargetData->Lon, (void *)mmsi_long,
3366 SELTYPE_AISTARGET);
3367 pSel->SetUserData(pTargetData->MMSI);
3368 }
3369
3370 // Calculate CPA info for this target immediately
3371 UpdateOneCPA(pTargetData.get());
3372
3373 // Update this target's track
3374 if (pTargetData->b_show_track) UpdateOneTrack(pTargetData.get());
3375 }
3376 // TODO add ais message call
3377 plugin_msg.Notify(std::make_shared<AisTargetData>(*pTargetData), "");
3378 } else {
3379 // printf("Unrecognised AIS message ID: %d\n",
3380 // pTargetData->MID);
3381 if (new_target) {
3382 // delete pTargetData; // this target is not going to be used
3383 m_n_targets--;
3384 } else {
3385 // If this is not an ownship message, update the AIS Target in the
3386 // Selectable list even if the message type was not recognized
3387 if (!pTargetData->b_OwnShip) {
3388 if (pTargetData->b_positionOnceValid) {
3389 long mmsi_long = pTargetData->MMSI;
3390 SelectItem *pSel = pSelectAIS->AddSelectablePoint(
3391 pTargetData->Lat, pTargetData->Lon, (void *)mmsi_long,
3392 SELTYPE_AISTARGET);
3393 pSel->SetUserData(pTargetData->MMSI);
3394 }
3395 }
3396 }
3397 }
3398}
3399
3400void AisDecoder::getAISTarget(long mmsi,
3401 std::shared_ptr<AisTargetData> &pTargetData,
3402 std::shared_ptr<AisTargetData> &pStaleTarget,
3403 bool &bnewtarget, int &last_report_ticks,
3404 wxDateTime &now) {
3405 now = wxDateTime::Now();
3406 auto it = AISTargetList.find(mmsi);
3407 if (it == AISTargetList.end()) // not found
3408 {
3409 pTargetData = AisTargetDataMaker::GetInstance().GetTargetData();
3410 bnewtarget = true;
3411 m_n_targets++;
3412 } else {
3413 pTargetData = it->second; // find current entry
3414 pStaleTarget = pTargetData; // save a pointer to stale data
3415 }
3416
3417 // Grab the stale targets's last report time
3418 now.MakeGMT();
3419
3420 if (pStaleTarget)
3421 last_report_ticks = pStaleTarget->PositionReportTicks;
3422 else
3423 last_report_ticks = now.GetTicks();
3424
3425 // Delete the stale AIS Target selectable point
3426 if (pStaleTarget)
3427 pSelectAIS->DeleteSelectablePoint((void *)mmsi, SELTYPE_AISTARGET);
3428}
3429
3430std::shared_ptr<AisTargetData> AisDecoder::ProcessDSx(const wxString &str,
3431 bool b_take_dsc) {
3432 double dsc_lat = 0.;
3433 double dsc_lon = 0.;
3434 double dsc_mins, dsc_degs, dsc_tmp, dsc_addr;
3435 double dse_tmp;
3436 double dse_lat = 0.;
3437 double dse_lon = 0.;
3438 long dsc_fmt, dsc_quadrant, dsc_cat, dsc_nature;
3439
3440 int dsc_mmsi = 0;
3441 int dsc_tx_mmsi = 0;
3442 int dse_mmsi = 0;
3443 double dse_cog = 0.;
3444 double dse_sog = 0.;
3445 wxString dse_shipName = "";
3446 wxString dseSymbol;
3447
3448 unsigned mmsi = 0;
3449
3450 std::shared_ptr<AisTargetData> pTargetData = nullptr;
3451
3452 // parse a DSC Position message $CDDSx,.....
3453 // Use a tokenizer to pull out the first 9 fields
3454 wxStringTokenizer tkz(str, ",*");
3455
3456 wxString token;
3457 token = tkz.GetNextToken(); // !$CDDS
3458
3459 if (str.Mid(3, 3).IsSameAs("DSC")) {
3460 m_dsc_last_string = str;
3461
3462 token = tkz.GetNextToken(); // format specifier
3463 token.ToLong(
3464 &dsc_fmt); // (02-area,12-distress,16-allships,20-individual,...)
3465
3466 token = tkz.GetNextToken(); // address i.e. mmsi*10 for received msg, or
3467 // area spec or sender mmsi for (12) and (16)
3468 if (dsc_fmt == 12 || dsc_fmt == 16) {
3469 dsc_mmsi = wxAtoi(token.Mid(0, 9));
3470 } else {
3471 token.ToDouble(&dsc_addr);
3472 dsc_mmsi = 0 - (int)(dsc_addr / 10); // as per NMEA 0183 3.01
3473 }
3474
3475 token = tkz.GetNextToken(); // category
3476 token.ToLong(&dsc_cat); // 12 - Distress (relayed)
3477
3478 token = tkz.GetNextToken(); // nature of distress or telecommand1
3479 if (!token.IsSameAs("")) { // 00-12 = nature of distress
3480 token.ToLong(&dsc_nature);
3481 } else
3482 dsc_nature = 99;
3483
3484 token = tkz.GetNextToken(); // comm type or telecommand2
3485
3486 token = tkz.GetNextToken(); // position or channel/freq
3487 token.ToDouble(&dsc_tmp);
3488
3489 token = tkz.GetNextToken(); // time or tel. no.
3490 token = tkz.GetNextToken(); // mmsi of ship in distress, relay
3491 if (dsc_fmt == 16 && dsc_cat == 12 && !token.IsSameAs("")) {
3492 // wxString dmmsi = token.Mid(0,9);
3493 dsc_tx_mmsi = dsc_mmsi; // mmsi of relay issuer
3494 dsc_mmsi = wxAtoi(token.Mid(0, 9));
3495 }
3496 token = tkz.GetNextToken(); // nature of distress, relay
3497 if (dsc_fmt == 16 && dsc_cat == 12) {
3498 if (!token.IsSameAs("")) { // 00-12 = nature of distress
3499 token.ToLong(&dsc_nature);
3500 } else
3501 dsc_nature = 99;
3502 }
3503 token = tkz.GetNextToken(); // acknowledgement
3504 token = tkz.GetNextToken(); // expansion indicator
3505
3506 dsc_quadrant = (int)(dsc_tmp / 1000000000.0);
3507
3508 if (dsc_quadrant > 3) // Position is "Unspecified", or 9999999999
3509 return nullptr;
3510
3511 dsc_lat = (int)(dsc_tmp / 100000.0);
3512 dsc_lon = dsc_tmp - dsc_lat * 100000.0;
3513 dsc_lat = dsc_lat - dsc_quadrant * 10000;
3514 dsc_degs = (int)(dsc_lat / 100.0);
3515 dsc_mins = dsc_lat - dsc_degs * 100.0;
3516 dsc_lat = dsc_degs + dsc_mins / 60.0;
3517
3518 dsc_degs = (int)(dsc_lon / 100.0);
3519 dsc_mins = dsc_lon - dsc_degs * 100.0;
3520 dsc_lon = dsc_degs + dsc_mins / 60.0;
3521 switch (dsc_quadrant) {
3522 case 0:
3523 break; // NE
3524 case 1:
3525 dsc_lon = -dsc_lon;
3526 break; // NW
3527 case 2:
3528 dsc_lat = -dsc_lat;
3529 break; // SE
3530 case 3:
3531 dsc_lon = -dsc_lon;
3532 dsc_lat = -dsc_lat;
3533 break; // SW
3534 default:
3535 break;
3536 }
3537 if (dsc_fmt != 02) mmsi = (int)dsc_mmsi;
3538
3539 } else if (str.Mid(3, 3).IsSameAs("DSE")) {
3540 token = tkz.GetNextToken(); // total number of sentences
3541 token = tkz.GetNextToken(); // sentence number
3542 token = tkz.GetNextToken(); // query/rely flag
3543 token = tkz.GetNextToken(); // vessel MMSI
3544 dse_mmsi = wxAtoi(token.Mid(
3545 0, 9)); // ITU-R M.493-10 �5.2
3546 // token.ToDouble(&dse_addr);
3547 // 0 - (int)(dse_addr / 10); // as per NMEA 0183 3.01
3548
3549#if 0
3550 token = tkz.GetNextToken(); // code field
3551 token =
3552 tkz.GetNextToken(); // data field - position - 2*4 digits latlon .mins
3553 token.ToDouble(&dse_tmp);
3554 dse_lat = (int)(dse_tmp / 10000.0);
3555 dse_lon = (int)(dse_tmp - dse_lat * 10000.0);
3556 dse_lat = dse_lat / 600000.0;
3557 dse_lon = dse_lon / 600000.0;
3558#endif
3559 // DSE Sentence may contain multiple dse expansion data items
3560 while (tkz.HasMoreTokens()) {
3561 dseSymbol = tkz.GetNextToken(); // dse expansion data symbol
3562 token = tkz.GetNextToken(); // dse expansion data
3563 if (dseSymbol.IsSameAs("00")) { // Position
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 } else if (dseSymbol.IsSameAs("01")) { // Source & Datum
3570 } else if (dseSymbol.IsSameAs("02")) { // SOG
3571 token.ToDouble(&dse_tmp);
3572 dse_sog = dse_tmp / 10.0;
3573 } else if (dseSymbol.IsSameAs("03")) { // COG
3574 token.ToDouble(&dse_tmp);
3575 dse_cog = dse_tmp / 10.0;
3576 } else if (dseSymbol.IsSameAs("04")) { // Station Information
3577 dse_shipName = DecodeDSEExpansionCharacters(token);
3578 } else if (dseSymbol.IsSameAs("05")) { // Geographic Information
3579 } else if (dseSymbol.IsSameAs("06")) { // Persons On Board
3580 }
3581 }
3582 mmsi = abs((int)dse_mmsi);
3583 }
3584
3585 // Get the last report time for this target, if it exists
3586 wxDateTime now = wxDateTime::Now();
3587 now.MakeGMT();
3588 int last_report_ticks = now.GetTicks();
3589
3590 // Search the current AISTargetList for an MMSI match
3591 auto it = AISTargetList.find(mmsi);
3592 std::shared_ptr<AisTargetData> pStaleTarget = nullptr;
3593 if (it == AISTargetList.end()) { // not found
3594 } else {
3595 pStaleTarget = it->second; // find current entry
3596 last_report_ticks = pStaleTarget->PositionReportTicks;
3597 }
3598
3599 if (dsc_mmsi) {
3600 // Create a tentative target, but do not post it pending receipt of
3601 // extended data
3602 m_ptentative_dsctarget = AisTargetDataMaker::GetInstance().GetTargetData();
3603
3604 m_ptentative_dsctarget->PositionReportTicks = now.GetTicks();
3605 m_ptentative_dsctarget->StaticReportTicks = now.GetTicks();
3606
3607 m_ptentative_dsctarget->MMSI = mmsi;
3608 m_ptentative_dsctarget->NavStatus =
3609 99; // Undefind. "-" in the AIS target list
3610 m_ptentative_dsctarget->Lat = dsc_lat;
3611 m_ptentative_dsctarget->Lon = dsc_lon;
3612 m_ptentative_dsctarget->b_positionOnceValid = true;
3613 m_ptentative_dsctarget->COG = 0;
3614 m_ptentative_dsctarget->SOG = 0;
3615 m_ptentative_dsctarget->ShipType = dsc_fmt;
3616 m_ptentative_dsctarget->Class = AIS_DSC;
3617 m_ptentative_dsctarget->b_isDSCtarget = true;
3618 m_ptentative_dsctarget->b_nameValid = true;
3619 if (dsc_fmt == 12 || (dsc_fmt == 16 && dsc_cat == 12)) {
3620 snprintf(m_ptentative_dsctarget->ShipName, SHIP_NAME_LEN, "DISTRESS %u",
3621 mmsi);
3622 m_ptentative_dsctarget->m_dscNature = int(dsc_nature);
3623 m_ptentative_dsctarget->m_dscTXmmsi = dsc_tx_mmsi;
3624 } else {
3625 snprintf(m_ptentative_dsctarget->ShipName, SHIP_NAME_LEN, "POSITION %u",
3626 mmsi);
3627 }
3628
3629 m_ptentative_dsctarget->b_active = true;
3630 m_ptentative_dsctarget->b_lost = false;
3631 m_ptentative_dsctarget->RecentPeriod =
3632 m_ptentative_dsctarget->PositionReportTicks - last_report_ticks;
3633
3634 // Start a timer, looking for an expected DSE extension message
3635 if (!b_take_dsc) m_dsc_timer.Start(1000, wxTIMER_ONE_SHOT);
3636 }
3637
3638 // Got an extension message, or the timer expired and no extension is
3639 // expected
3640 if (dse_mmsi || b_take_dsc) {
3641 if (m_ptentative_dsctarget) {
3642 // stop the timer for sure
3643 m_dsc_timer.Stop();
3644
3645 // Update the extended information
3646 if (dse_mmsi) {
3647 m_ptentative_dsctarget->Lat =
3648 m_ptentative_dsctarget->Lat +
3649 ((m_ptentative_dsctarget->Lat) >= 0 ? dse_lat : -dse_lat);
3650 m_ptentative_dsctarget->Lon =
3651 m_ptentative_dsctarget->Lon +
3652 ((m_ptentative_dsctarget->Lon) >= 0 ? dse_lon : -dse_lon);
3653 if (!dse_shipName.empty()) {
3654 memset(m_ptentative_dsctarget->ShipName, '\0', SHIP_NAME_LEN);
3655 snprintf(m_ptentative_dsctarget->ShipName, dse_shipName.length(),
3656 "%s", dse_shipName.ToAscii().data());
3657 }
3658 m_ptentative_dsctarget->COG = dse_cog;
3659 m_ptentative_dsctarget->SOG = dse_sog;
3660 }
3661
3662 // Update the most recent report period
3663 m_ptentative_dsctarget->RecentPeriod =
3664 m_ptentative_dsctarget->PositionReportTicks - last_report_ticks;
3665
3666 // And post the target
3667
3668 // Search the current AISTargetList for an MMSI match
3669 auto found = AISTargetList.find(mmsi);
3670 if (found == AISTargetList.end()) { // not found
3671 pTargetData = m_ptentative_dsctarget;
3672 } else {
3673 pTargetData = found->second; // find current entry
3674 std::vector<AISTargetTrackPoint> ptrack =
3675 std::move(pTargetData->m_ptrack);
3676 pTargetData->CloneFrom(
3677 m_ptentative_dsctarget
3678 .get()); // this will make an empty track list
3679
3680 pTargetData->m_ptrack =
3681 std::move(ptrack); // and substitute the old track list
3682
3683 // delete m_ptentative_dsctarget;
3684 }
3685
3686 // Reset for next time
3687 m_ptentative_dsctarget = nullptr;
3688
3689 m_pLatestTargetData = pTargetData;
3690
3691 AISTargetList[pTargetData->MMSI] =
3692 pTargetData; // update the hash table entry
3693
3694 long mmsi_long = pTargetData->MMSI;
3695
3696 // Delete any stale Target selectable point
3697 if (pStaleTarget)
3698 pSelectAIS->DeleteSelectablePoint((void *)mmsi_long, SELTYPE_AISTARGET);
3699 // And add the updated target
3700 SelectItem *pSel =
3701 pSelectAIS->AddSelectablePoint(pTargetData->Lat, pTargetData->Lon,
3702 (void *)mmsi_long, SELTYPE_AISTARGET);
3703 pSel->SetUserData(pTargetData->MMSI);
3704
3705 // Calculate CPA info for this target immediately
3706 UpdateOneCPA(pTargetData.get());
3707
3708 // Update this target's track
3709 if (pTargetData->b_show_track) UpdateOneTrack(pTargetData.get());
3710 }
3711 }
3712
3713 return pTargetData;
3714}
3715
3716bool AisDecoder::NMEACheckSumOK(const wxString &str_in) {
3717 unsigned char checksum_value = 0;
3718 int sentence_hex_sum;
3719
3720 wxCharBuffer buf = str_in.ToUTF8();
3721 if (!buf.data()) return false; // cannot decode string
3722
3723 char str_ascii[AIS_MAX_MESSAGE_LEN + 1];
3724 strncpy(str_ascii, buf.data(), AIS_MAX_MESSAGE_LEN);
3725 str_ascii[AIS_MAX_MESSAGE_LEN] = '\0';
3726
3727 int string_length = strlen(str_ascii);
3728
3729 int payload_length = 0;
3730 while ((payload_length < string_length) &&
3731 (str_ascii[payload_length] != '*')) // look for '*'
3732 payload_length++;
3733
3734 if (payload_length == string_length)
3735 return false; // '*' not found at all, no checksum
3736
3737 int index = 1; // Skip over the $ at the begining of the sentence
3738
3739 while (index < payload_length) {
3740 checksum_value ^= str_ascii[index];
3741 index++;
3742 }
3743
3744 if (string_length > 4) {
3745 char scanstr[3];
3746 scanstr[0] = str_ascii[payload_length + 1];
3747 scanstr[1] = str_ascii[payload_length + 2];
3748 scanstr[2] = 0;
3749 sscanf(scanstr, "%2x", &sentence_hex_sum);
3750
3751 if (sentence_hex_sum == checksum_value) return true;
3752 }
3753
3754 return false;
3755}
3756
3757void AisDecoder::UpdateAllCPA() {
3758 // Iterate thru all the targets
3759 for (const auto &it : GetTargetList()) {
3760 std::shared_ptr<AisTargetData> td = it.second;
3761
3762 if (nullptr != td) UpdateOneCPA(td.get());
3763 }
3764}
3765
3766void AisDecoder::UpdateAllTracks() {
3767 for (const auto &it : GetTargetList()) {
3768 std::shared_ptr<AisTargetData> td = it.second;
3769 if (td) UpdateOneTrack(td.get());
3770 }
3771}
3772
3773int gdup;
3774void AisDecoder::UpdateOneTrack(AisTargetData *ptarget) {
3775 if (!ptarget->b_positionOnceValid) return;
3776 // Reject for unbelievable jumps (corrupted/bad data)
3777 if (!ptarget->m_ptrack.empty()) {
3778 const AISTargetTrackPoint &LastTrackpoint = ptarget->m_ptrack.back();
3779 if (fabs(LastTrackpoint.m_lat - ptarget->Lat) > .1 ||
3780 fabs(LastTrackpoint.m_lon - ptarget->Lon) > .1) {
3781 // after an unlikely jump in pos, the last trackpoint might also be wrong
3782 // just to be sure we do delete this one as well.
3783 ptarget->m_ptrack.pop_back();
3784 ptarget->b_positionDoubtful = true;
3785 return;
3786 }
3787 }
3788
3789 // Avoid duplicate track points
3790 // Do not add track point if time since last point is < 2 seconds.
3791 if ((ptarget->PositionReportTicks - ptarget->LastPositionReportTicks) > 2) {
3792 // Create the newest point
3793 AISTargetTrackPoint ptrackpoint;
3794 ptrackpoint.m_lat = ptarget->Lat;
3795 ptrackpoint.m_lon = ptarget->Lon;
3796 ptrackpoint.m_time = wxDateTime::Now().GetTicks();
3797
3798 ptarget->m_ptrack.push_back(ptrackpoint);
3799
3800 if (ptarget->b_PersistTrack || ptarget->b_mPropPersistTrack) {
3801 Track *t;
3802 if (0 == m_persistent_tracks.count(ptarget->MMSI)) {
3803 t = new Track();
3804 t->SetName(wxString::Format(
3805 "AIS %s (%u) %s %s", ptarget->GetFullName().c_str(), ptarget->MMSI,
3806 wxDateTime::Now().FormatISODate().c_str(),
3807 wxDateTime::Now().FormatISOTime().c_str()));
3808 g_TrackList.push_back(t);
3809 new_track.Notify(t);
3810 m_persistent_tracks[ptarget->MMSI] = t;
3811 } else {
3812 t = m_persistent_tracks[ptarget->MMSI];
3813 }
3814 TrackPoint *tp = t->GetLastPoint();
3815 vector2D point(ptrackpoint.m_lon, ptrackpoint.m_lat);
3816 TrackPoint *tp1 =
3817 t->AddNewPoint(point, wxDateTime(ptrackpoint.m_time).ToUTC());
3818
3819 if (tp)
3820 pSelect->AddSelectableTrackSegment(tp->m_lat, tp->m_lon, tp1->m_lat,
3821 tp1->m_lon, tp, tp1, t);
3822
3823 // We do not want dependency on the GUI here, do we?
3824 // if( pRouteManagerDialog && pRouteManagerDialog->IsShown() )
3825 // pRouteManagerDialog->UpdateTrkListCtrl();
3826
3827 } else {
3828 // Walk the list, removing any track points that are older than the
3829 // stipulated time
3830 time_t test_time =
3831 wxDateTime::Now().GetTicks() - (time_t)(g_AISShowTracks_Mins * 60);
3832
3833 ptarget->m_ptrack.erase(
3834 std::remove_if(ptarget->m_ptrack.begin(), ptarget->m_ptrack.end(),
3835 [=](const AISTargetTrackPoint &track) {
3836 return track.m_time < test_time;
3837 }),
3838 ptarget->m_ptrack.end());
3839 }
3840 }
3841}
3842
3843void AisDecoder::DeletePersistentTrack(const Track *track) {
3844 for (auto it = m_persistent_tracks.begin(); it != m_persistent_tracks.end();
3845 it++) {
3846 if (it->second == track) {
3847 unsigned mmsi = it->first;
3848 m_persistent_tracks.erase(it);
3849 // Last tracks for this target?
3850 if (0 == m_persistent_tracks.count(mmsi)) {
3851 for (unsigned int i = 0; i < g_MMSI_Props_Array.GetCount(); i++) {
3852 if (mmsi == static_cast<unsigned>(g_MMSI_Props_Array[i]->MMSI)) {
3854 if (props->m_bPersistentTrack) {
3855 // Ask if mmsi props should be changed.
3856 // Avoid creation of a new track while messaging
3857
3858 std::shared_ptr<AisTargetData> td =
3859 Get_Target_Data_From_MMSI(mmsi);
3860 if (td) {
3861 props->m_bPersistentTrack = false;
3862 td->b_mPropPersistTrack = false;
3863 }
3864 if (!m_callbacks.confirm_stop_track()) {
3865 props->m_bPersistentTrack = true;
3866 }
3867 }
3868 break;
3869 }
3870 }
3871 }
3872 break;
3873 }
3874 }
3875}
3876
3877void AisDecoder::UpdateAllAlarms() {
3878 m_bGeneralAlert = false; // no alerts yet
3879
3880 // Iterate thru all the targets
3881 for (const auto &it : GetTargetList()) {
3882 std::shared_ptr<AisTargetData> td = it.second;
3883
3884 if (td) {
3885 // Maintain General Alert
3886 if (!m_bGeneralAlert) {
3887 // Quick check on basic condition
3888 if ((td->CPA < g_CPAWarn_NM) && (td->TCPA > 0) &&
3889 (td->Class != AIS_ATON) && (td->Class != AIS_BASE))
3890 m_bGeneralAlert = true;
3891
3892 // Some options can suppress general alerts
3893 if (g_bAIS_CPA_Alert_Suppress_Moored && (td->SOG <= g_ShowMoored_Kts))
3894 m_bGeneralAlert = false;
3895
3896 // Skip distant targets if requested
3897 if ((g_bCPAMax) && (td->Range_NM > g_CPAMax_NM))
3898 m_bGeneralAlert = false;
3899
3900 // Skip if TCPA is too long
3901 if ((g_bTCPA_Max) && (td->TCPA > g_TCPA_Max)) m_bGeneralAlert = false;
3902
3903 // SART targets always alert if "Active"
3904 if (td->Class == AIS_SART && td->NavStatus == 14)
3905 m_bGeneralAlert = true;
3906
3907 // DSC Distress targets always alert
3908 if ((td->Class == AIS_DSC) &&
3909 ((td->ShipType == 12) || (td->ShipType == 16)))
3910 m_bGeneralAlert = true;
3911 }
3912
3913 ais_alert_type this_alarm = AIS_NO_ALERT;
3914
3915 // SART targets always alert if "Active"
3916 if (td->Class == AIS_SART && td->NavStatus == 14)
3917 this_alarm = AIS_ALERT_SET;
3918
3919 // DSC Distress targets always alert
3920 if ((td->Class == AIS_DSC) &&
3921 ((td->ShipType == 12) || (td->ShipType == 16)))
3922 this_alarm = AIS_ALERT_SET;
3923
3924 if (g_bCPAWarn && td->b_active && td->b_positionOnceValid &&
3925 (td->Class != AIS_SART) && (td->Class != AIS_DSC)) {
3926 // Skip anchored/moored(interpreted as low speed) targets if
3927 // requested
3928 if ((g_bHideMoored) && (td->SOG <= g_ShowMoored_Kts)) { // dsr
3929 td->n_alert_state = AIS_NO_ALERT;
3930 continue;
3931 }
3932
3933 // No Alert on moored(interpreted as low speed) targets if so
3934 // requested
3935 if (g_bAIS_CPA_Alert_Suppress_Moored &&
3936 (td->SOG <= g_ShowMoored_Kts)) { // dsr
3937 td->n_alert_state = AIS_NO_ALERT;
3938 continue;
3939 }
3940
3941 // Skip distant targets if requested
3942 if (g_bCPAMax) {
3943 if (td->Range_NM > g_CPAMax_NM) {
3944 td->n_alert_state = AIS_NO_ALERT;
3945 continue;
3946 }
3947 }
3948
3949 if ((td->CPA < g_CPAWarn_NM) && (td->TCPA > 0) &&
3950 (td->Class != AIS_ATON) && (td->Class != AIS_BASE) &&
3951 (td->Class != AIS_METEO)) {
3952 if (g_bTCPA_Max) {
3953 if (td->TCPA < g_TCPA_Max) {
3954 if (td->b_isFollower)
3955 this_alarm = AIS_ALERT_NO_DIALOG_SET;
3956 else
3957 this_alarm = AIS_ALERT_SET;
3958 }
3959 } else {
3960 if (td->b_isFollower)
3961 this_alarm = AIS_ALERT_NO_DIALOG_SET;
3962 else
3963 this_alarm = AIS_ALERT_SET;
3964 }
3965 }
3966 }
3967
3968 // Maintain the timer for in_ack flag
3969 // SART and DSC targets always maintain ack timeout
3970
3971 if (g_bAIS_ACK_Timeout || (td->Class == AIS_SART) ||
3972 ((td->Class == AIS_DSC) &&
3973 ((td->ShipType == 12) || (td->ShipType == 16)))) {
3974 if (td->b_in_ack_timeout) {
3975 wxTimeSpan delta = wxDateTime::Now() - td->m_ack_time;
3976 if (delta.GetMinutes() > g_AckTimeout_Mins)
3977 td->b_in_ack_timeout = false;
3978 }
3979 } else {
3980 // Not using ack timeouts.
3981 // If a target has been acknowledged, leave it ack'ed until it goes out
3982 // of AIS_ALARM_SET state
3983 if (td->b_in_ack_timeout) {
3984 if (this_alarm == AIS_NO_ALERT) td->b_in_ack_timeout = false;
3985 }
3986 }
3987
3988 td->n_alert_state = this_alarm;
3989 }
3990 }
3991}
3992
3993void AisDecoder::UpdateOneCPA(AisTargetData *ptarget) {
3994 ptarget->Range_NM = -1.; // Defaults
3995 ptarget->Brg = -1.;
3996
3997 // Compute the current Range/Brg to the target
3998 // This should always be possible even if GPS data is not valid
3999 // because O must always have a position for own-ship. Plugins need
4000 // AIS target range and bearing from own-ship position even if GPS is not
4001 // valid.
4002 double brg, dist;
4003 DistanceBearingMercator(ptarget->Lat, ptarget->Lon, gLat, gLon, &brg, &dist);
4004 ptarget->Range_NM = dist;
4005 ptarget->Brg = brg;
4006
4007 if (dist <= 1e-5) ptarget->Brg = -1.0; // Brg is undefined if Range == 0.
4008
4009 if (!ptarget->b_positionOnceValid || !bGPSValid) {
4010 ptarget->bCPA_Valid = false;
4011 return;
4012 }
4013 // Ais Meteo is not a hard target in danger for collision
4014 if (ptarget->Class == AIS_METEO) {
4015 ptarget->bCPA_Valid = false;
4016 return;
4017 }
4018
4019 // There can be no collision between ownship and itself....
4020 // This can happen if AIVDO messages are received, and there is another
4021 // source of ownship position, like NMEA GLL The two positions are always
4022 // temporally out of sync, and one will always be exactly in front of the
4023 // other one.
4024 if (ptarget->b_OwnShip) {
4025 ptarget->CPA = 100;
4026 ptarget->TCPA = -100;
4027 ptarget->bCPA_Valid = false;
4028 return;
4029 }
4030
4031 double cpa_calc_ownship_cog = gCog;
4032 double cpa_calc_target_cog = ptarget->COG;
4033
4034 // Ownship is not reporting valid SOG, so no way to calculate CPA
4035 if (std::isnan(gSog) || (gSog > 102.2)) {
4036 ptarget->bCPA_Valid = false;
4037 return;
4038 }
4039
4040 // Ownship is maybe anchored and not reporting COG
4041 if (std::isnan(gCog) || gCog == 360.0) {
4042 if (gSog < .01)
4043 cpa_calc_ownship_cog =
4044 0.; // substitute value
4045 // for the case where SOG ~= 0, and COG is unknown.
4046 else {
4047 ptarget->bCPA_Valid = false;
4048 return;
4049 }
4050 }
4051
4052 // Target is maybe anchored and not reporting COG
4053 if (ptarget->COG == 360.0) {
4054 if (ptarget->SOG > 102.2) {
4055 ptarget->bCPA_Valid = false;
4056 return;
4057 } else if (ptarget->SOG < .01) {
4058 cpa_calc_target_cog = 0.; // substitute value
4059 // for the case where SOG ~= 0, and COG is unknown.
4060 } else {
4061 ptarget->bCPA_Valid = false;
4062 return;
4063 }
4064 }
4065
4066 // Express the SOGs as meters per hour
4067 double v0 = gSog * 1852.;
4068 double v1 = ptarget->SOG * 1852.;
4069
4070 if ((v0 < 1e-6) && (v1 < 1e-6)) {
4071 ptarget->TCPA = 0.;
4072 ptarget->CPA = 0.;
4073
4074 ptarget->bCPA_Valid = false;
4075 } else {
4076 // Calculate the TCPA first
4077
4078 // Working on a Reduced Lat/Lon orthogonal plotting sheet....
4079 // Get easting/northing to target, in meters
4080
4081 double east1 = (ptarget->Lon - gLon) * 60 * 1852;
4082 double north1 = (ptarget->Lat - gLat) * 60 * 1852;
4083
4084 double east = east1 * (cos(gLat * PI / 180.));
4085
4086 double north = north1;
4087
4088 // Convert COGs trigonometry to standard unit circle
4089 double cosa = cos((90. - cpa_calc_ownship_cog) * PI / 180.);
4090 double sina = sin((90. - cpa_calc_ownship_cog) * PI / 180.);
4091 double cosb = cos((90. - cpa_calc_target_cog) * PI / 180.);
4092 double sinb = sin((90. - cpa_calc_target_cog) * PI / 180.);
4093
4094 // These will be useful
4095 double fc = (v0 * cosa) - (v1 * cosb);
4096 double fs = (v0 * sina) - (v1 * sinb);
4097
4098 double d = (fc * fc) + (fs * fs);
4099 double tcpa;
4100
4101 // the tracks are almost parallel
4102 if (fabs(d) < 1e-6)
4103 tcpa = 0.;
4104 else
4105 // Here is the equation for t, which will be in hours
4106 tcpa = ((fc * east) + (fs * north)) / d;
4107
4108 // Convert to minutes
4109 ptarget->TCPA = tcpa * 60.;
4110
4111 // Calculate CPA
4112 // Using TCPA, predict ownship and target positions
4113
4114 double OwnshipLatCPA, OwnshipLonCPA, TargetLatCPA, TargetLonCPA;
4115
4116 ll_gc_ll(gLat, gLon, cpa_calc_ownship_cog, gSog * tcpa, &OwnshipLatCPA,
4117 &OwnshipLonCPA);
4118 ll_gc_ll(ptarget->Lat, ptarget->Lon, cpa_calc_target_cog,
4119 ptarget->SOG * tcpa, &TargetLatCPA, &TargetLonCPA);
4120
4121 // And compute the distance
4122 ptarget->CPA = DistGreatCircle(OwnshipLatCPA, OwnshipLonCPA, TargetLatCPA,
4123 TargetLonCPA);
4124
4125 ptarget->bCPA_Valid = true;
4126
4127 if (ptarget->TCPA < 0) ptarget->bCPA_Valid = false;
4128 }
4129}
4130
4131void AisDecoder::OnTimerDSC(wxTimerEvent &event) {
4132 // Timer expired, no CDDSE message was received, so accept the latest CDDSC
4133 // message
4134 if (m_ptentative_dsctarget) {
4135 ProcessDSx(m_dsc_last_string, true);
4136 }
4137}
4138
4139void AisDecoder::OnTimerAIS(wxTimerEvent &event) {
4140 TimerAIS.Stop();
4141 // Scrub the target hash list
4142 // removing any targets older than stipulated age
4143
4144 wxDateTime now = wxDateTime::Now();
4145 now.MakeGMT();
4146
4147 std::unordered_map<int, std::shared_ptr<AisTargetData>> &current_targets =
4148 GetTargetList();
4149
4150 auto it = current_targets.begin();
4151 std::vector<int> remove_array; // collector for MMSI of targets to be removed
4152
4153 while (it != current_targets.end()) {
4154 if (it->second ==
4155 nullptr) // This should never happen, but I saw it once....
4156 {
4157 current_targets.erase(it);
4158 break; // leave the loop
4159 }
4160 // std::shared_ptr<AisTargetData>
4161 // xtd(std::make_shared<AisTargetData>(*it->second));
4162 std::shared_ptr<AisTargetData> xtd = it->second;
4163
4164 int target_posn_age = now.GetTicks() - xtd->PositionReportTicks;
4165 int target_static_age = now.GetTicks() - xtd->StaticReportTicks;
4166
4167 // Global variables controlling lost target handling
4168 // g_bMarkLost
4169 // g_MarkLost_Mins // Minutes until black "cross out
4170 // g_bRemoveLost
4171 // g_RemoveLost_Mins); // minutes until target is removed from screen and
4172 // internal lists
4173
4174 // g_bInlandEcdis
4175
4176 // Mark lost targets if specified
4177 double removelost_Mins = fmax(g_RemoveLost_Mins, g_MarkLost_Mins);
4178
4179 if (g_bInlandEcdis && (xtd->Class != AIS_ARPA)) {
4180 double iECD_LostTimeOut = 0.0;
4181 // special rules apply for europe inland ecdis timeout settings. overrule
4182 // option settings Won't apply for ARPA targets where the radar has all
4183 // control
4184 if (xtd->Class == AIS_CLASS_B) {
4185 if ((xtd->NavStatus == MOORED) || (xtd->NavStatus == AT_ANCHOR))
4186 iECD_LostTimeOut = 18 * 60;
4187 else
4188 iECD_LostTimeOut = 180;
4189 }
4190 if (xtd->Class == AIS_CLASS_A) {
4191 if ((xtd->NavStatus == MOORED) || (xtd->NavStatus == AT_ANCHOR)) {
4192 if (xtd->SOG < 3.)
4193 iECD_LostTimeOut = 18 * 60;
4194 else
4195 iECD_LostTimeOut = 60;
4196 } else
4197 iECD_LostTimeOut = 60;
4198 }
4199
4200 if ((target_posn_age > iECD_LostTimeOut) &&
4201 (xtd->Class != AIS_GPSG_BUDDY))
4202 xtd->b_active = false;
4203
4204 removelost_Mins = (2 * iECD_LostTimeOut) / 60.;
4205 } else if (g_bMarkLost) {
4206 if ((target_posn_age > g_MarkLost_Mins * 60) &&
4207 (xtd->Class != AIS_GPSG_BUDDY))
4208 xtd->b_active = false;
4209 }
4210
4211 if (xtd->Class == AIS_SART || xtd->Class == AIS_METEO)
4212 removelost_Mins = 18.0;
4213
4214 // Remove lost targets if specified
4215
4216 if (g_bRemoveLost || g_bInlandEcdis) {
4217 bool b_arpalost =
4218 (xtd->Class == AIS_ARPA &&
4219 xtd->b_lost); // A lost ARPA target would be deleted at once
4220 if (((target_posn_age > removelost_Mins * 60) &&
4221 (xtd->Class != AIS_GPSG_BUDDY)) ||
4222 b_arpalost) {
4223 // So mark the target as lost, with unknown position, and make it
4224 // not selectable
4225 xtd->b_lost = true;
4226 xtd->b_positionOnceValid = false;
4227 xtd->COG = 360.0;
4228 xtd->SOG = 103.0;
4229 xtd->HDG = 511.0;
4230 xtd->ROTAIS = -128;
4231
4232 plugin_msg.Notify(xtd, "");
4233
4234 long mmsi_long = xtd->MMSI;
4235 pSelectAIS->DeleteSelectablePoint((void *)mmsi_long, SELTYPE_AISTARGET);
4236
4237 // If we have not seen a static report in 3 times the removal spec,
4238 // then remove the target from all lists
4239 // or a lost ARPA target.
4240 if (target_static_age > removelost_Mins * 60 * 3 || b_arpalost) {
4241 xtd->b_removed = true;
4242 plugin_msg.Notify(xtd, "");
4243 remove_array.push_back(xtd->MMSI); // Add this target to removal list
4244 }
4245 }
4246 }
4247
4248 // Remove any targets specified as to be "ignored", so that they won't
4249 // trigger phantom alerts (e.g. SARTs)
4250 for (unsigned int i = 0; i < g_MMSI_Props_Array.GetCount(); i++) {
4252 if (xtd->MMSI == props->MMSI) {
4253 if (props->m_bignore) {
4254 remove_array.push_back(xtd->MMSI); // Add this target to removal list
4255 xtd->b_removed = true;
4256 plugin_msg.Notify(xtd, "");
4257 }
4258 break;
4259 }
4260 }
4261
4262 // Check if the target has recently been set as own MMSI
4263 if (static_cast<unsigned>(xtd->MMSI) == g_OwnShipmmsi) {
4264 remove_array.push_back(xtd->MMSI); // Add this target to removal list
4265 xtd->b_removed = true;
4266 plugin_msg.Notify(xtd, "");
4267 }
4268
4269 ++it;
4270 }
4271
4272 // Remove all the targets collected in remove_array in one pass
4273 for (unsigned int i = 0; i < remove_array.size(); i++) {
4274 auto itd = current_targets.find(remove_array[i]);
4275 if (itd != current_targets.end()) {
4276 std::shared_ptr<AisTargetData> td = itd->second;
4277 current_targets.erase(itd);
4278 // delete td;
4279 }
4280 }
4281
4282 UpdateAllCPA();
4283 UpdateAllAlarms();
4284
4285 // Update the general suppression flag
4286 m_bSuppressed = false;
4287 if (g_bAIS_CPA_Alert_Suppress_Moored || g_bHideMoored ||
4288 (g_bShowScaled && g_bAllowShowScaled))
4289 m_bSuppressed = true;
4290
4291 m_bAIS_Audio_Alert_On = false; // default, may be set on
4292
4293 // Process any Alarms
4294
4295 // If the AIS Alert Dialog is not currently shown....
4296
4297 // Scan all targets, looking for SART, DSC Distress, and CPA incursions
4298 // In the case of multiple targets of the same type, select the shortest
4299 // range or shortest TCPA
4300 std::shared_ptr<AisTargetData> palert_target = nullptr;
4301
4302 if (!g_pais_alert_dialog_active) {
4303 pAISMOBRoute = nullptr; // Reset the AISMOB auto route.
4304 double tcpa_min = 1e6; // really long
4305 double sart_range = 1e6;
4306 std::shared_ptr<AisTargetData> palert_target_cpa = nullptr;
4307 std::shared_ptr<AisTargetData> palert_target_sart = nullptr;
4308 std::shared_ptr<AisTargetData> palert_target_dsc = nullptr;
4309
4310 for (it = current_targets.begin(); it != current_targets.end(); ++it) {
4311 std::shared_ptr<AisTargetData> td = it->second;
4312 if (td) {
4313 if ((td->Class != AIS_SART) && (td->Class != AIS_DSC)) {
4314 if (g_bAIS_CPA_Alert && td->b_active) {
4315 if ((AIS_ALERT_SET == td->n_alert_state) && !td->b_in_ack_timeout) {
4316 if (td->TCPA < tcpa_min) {
4317 tcpa_min = td->TCPA;
4318 palert_target_cpa = td;
4319 }
4320 }
4321 }
4322 } else if ((td->Class == AIS_DSC) &&
4323 ((td->ShipType == 12) || (td->ShipType == 16))) {
4324 if (td->b_active) {
4325 if ((AIS_ALERT_SET == td->n_alert_state) && !td->b_in_ack_timeout) {
4326 palert_target_dsc = td;
4327 } else { // Reset DCS flag to open for a real AIS for the same
4328 // target
4329 td->b_isDSCtarget = false;
4330 }
4331 }
4332 }
4333
4334 else if (td->Class == AIS_SART) {
4335 if (td->b_active) {
4336 if ((AIS_ALERT_SET == td->n_alert_state) && !td->b_in_ack_timeout) {
4337 if (td->Range_NM < sart_range) {
4338 tcpa_min = sart_range;
4339 palert_target_sart = td;
4340 }
4341 }
4342 }
4343 }
4344 }
4345 }
4346
4347 // Which of multiple targets?
4348 // Give priority to SART targets, then DSC Distress, then CPA incursion
4349 palert_target = palert_target_cpa;
4350
4351 if (palert_target_sart) {
4352 palert_target = palert_target_sart;
4353 }
4354
4355 if (palert_target_dsc) {
4356 palert_target = palert_target_dsc;
4357 }
4358 } else {
4359 // Alert is currently shown, get target from from knowable GUI
4360 palert_target = Get_Target_Data_From_MMSI(m_callbacks.get_target_mmsi());
4361 }
4362 // Show or update the alert
4363 if (palert_target) info_update.Notify(palert_target, "");
4364
4365 TimerAIS.Start(TIMER_AIS_MSEC, wxTIMER_CONTINUOUS);
4366}
4367
4368std::shared_ptr<AisTargetData> AisDecoder::Get_Target_Data_From_MMSI(
4369 unsigned mmsi) {
4370 if (AISTargetList.find(mmsi) == AISTargetList.end()) return nullptr;
4371 return AISTargetList[mmsi];
4372}
4373
4374ArrayOfMmsiProperties g_MMSI_Props_Array;
4375
4376// MmsiProperties Implementation
4377
4378MmsiProperties::MmsiProperties(wxString &spec) {
4379 Init();
4380 wxStringTokenizer tkz(spec, ";");
4381 wxString s;
4382
4383 s = tkz.GetNextToken();
4384 long mmsil;
4385 s.ToLong(&mmsil);
4386 MMSI = (int)mmsil;
4387
4388 s = tkz.GetNextToken();
4389 if (s.Len()) {
4390 if (s.Upper() == "ALWAYS")
4391 TrackType = TRACKTYPE_ALWAYS;
4392 else if (s.Upper() == "NEVER")
4393 TrackType = TRACKTYPE_NEVER;
4394 }
4395
4396 s = tkz.GetNextToken();
4397 if (s.Len()) {
4398 if (s.Upper() == "IGNORE") m_bignore = true;
4399 }
4400
4401 s = tkz.GetNextToken();
4402 if (s.Len()) {
4403 if (s.Upper() == "MOB") m_bMOB = true;
4404 }
4405
4406 s = tkz.GetNextToken();
4407 if (s.Len()) {
4408 if (s.Upper() == "VDM") m_bVDM = true;
4409 }
4410
4411 s = tkz.GetNextToken();
4412 if (s.Len()) {
4413 if (s.Upper() == "FOLLOWER") m_bFollower = true;
4414 }
4415
4416 s = tkz.GetNextToken();
4417 if (s.Len()) {
4418 if (s.Upper() == "PERSIST") m_bPersistentTrack = true;
4419 }
4420
4421 s = tkz.GetNextToken();
4422 if (s.Len()) {
4423 m_ShipName = s.Upper();
4424 }
4425}
4426
4427MmsiProperties::~MmsiProperties() = default;
4428
4429void MmsiProperties::Init() {
4430 MMSI = -1;
4431 TrackType = TRACKTYPE_DEFAULT;
4432 m_bignore = false;
4433 m_bMOB = false;
4434 m_bVDM = false;
4435 m_bFollower = false;
4436 m_bPersistentTrack = false;
4437 m_ShipName = "";
4438}
4439
4440wxString MmsiProperties::Serialize() {
4441 wxString sMMSI;
4442 wxString s;
4443
4444 sMMSI.Printf("%d", MMSI);
4445 sMMSI << ";";
4446
4447 if (TrackType) {
4448 if (TRACKTYPE_ALWAYS == TrackType)
4449 sMMSI << "always";
4450 else if (TRACKTYPE_NEVER == TrackType)
4451 sMMSI << "never";
4452 }
4453 sMMSI << ";";
4454
4455 if (m_bignore) {
4456 sMMSI << "ignore";
4457 }
4458 sMMSI << ";";
4459
4460 if (m_bMOB) {
4461 sMMSI << "MOB";
4462 }
4463 sMMSI << ";";
4464
4465 if (m_bVDM) {
4466 sMMSI << "VDM";
4467 }
4468 sMMSI << ";";
4469
4470 if (m_bFollower) {
4471 sMMSI << "Follower";
4472 }
4473 sMMSI << ";";
4474
4475 if (m_bPersistentTrack) {
4476 sMMSI << "PERSIST";
4477 }
4478 sMMSI << ";";
4479
4480 if (m_ShipName == "") {
4481 m_ShipName = GetShipNameFromFile(MMSI);
4482 }
4483 sMMSI << m_ShipName;
4484 return sMMSI;
4485}
4486
4487void AISshipNameCache(AisTargetData *pTargetData,
4488 AIS_Target_Name_Hash *AISTargetNamesC,
4489 AIS_Target_Name_Hash *AISTargetNamesNC, long mmsi) {
4490 if (g_benableAISNameCache) {
4491 wxString ship_name = "";
4492
4493 // Check for valid name data
4494 if (!pTargetData->b_nameValid) {
4495 AIS_Target_Name_Hash::iterator it = AISTargetNamesC->find(mmsi);
4496 if (it != AISTargetNamesC->end()) {
4497 ship_name = (*AISTargetNamesC)[mmsi].Left(20);
4498 strncpy(pTargetData->ShipName, ship_name.mb_str(),
4499 ship_name.length() + 1);
4500 pTargetData->b_nameValid = true;
4501 pTargetData->b_nameFromCache = true;
4502 } else if (!g_bUseOnlyConfirmedAISName) {
4503 it = AISTargetNamesNC->find(mmsi);
4504 if (it != AISTargetNamesNC->end()) {
4505 ship_name = (*AISTargetNamesNC)[mmsi].Left(20);
4506 strncpy(pTargetData->ShipName, ship_name.mb_str(),
4507 ship_name.length() + 1);
4508 pTargetData->b_nameValid = true;
4509 pTargetData->b_nameFromCache = true;
4510 }
4511 }
4512 }
4513 // else there IS a valid name, lets check if it is in one of the hash lists.
4514 else if ((pTargetData->MID == 5) || (pTargetData->MID == 24) ||
4515 (pTargetData->MID == 19) ||
4516 (pTargetData->MID == 123) || // 123: Has got a name from SignalK
4517 (pTargetData->MID == 124)) { // 124: Has got a name from n2k
4518 // This message contains ship static data, so has a name field
4519 pTargetData->b_nameFromCache = false;
4520 ship_name = trimAISField(pTargetData->ShipName);
4521 AIS_Target_Name_Hash::iterator itC = AISTargetNamesC->find(mmsi);
4522 AIS_Target_Name_Hash::iterator itNC = AISTargetNamesNC->find(mmsi);
4523 if (itC !=
4524 AISTargetNamesC->end()) { // There is a confirmed entry for this mmsi
4525 if ((*AISTargetNamesC)[mmsi] ==
4526 ship_name) { // Received name is same as confirmed name
4527 if (itNC != AISTargetNamesNC->end()) { // there is also an entry in
4528 // the NC list, delete it
4529 AISTargetNamesNC->erase(itNC);
4530 }
4531 } else { // There is a confirmed name but that one is different
4532 if (itNC != AISTargetNamesNC->end()) { // there is an entry in the NC
4533 // list check if name is same
4534 if ((*AISTargetNamesNC)[mmsi] ==
4535 ship_name) { // Same name is already in NC list so promote till
4536 // confirmed list
4537 (*AISTargetNamesC)[mmsi] = ship_name;
4538 // And delete from NC list
4539 AISTargetNamesNC->erase(itNC);
4540 } else { // A different name is in the NC list, update with
4541 // received one
4542 (*AISTargetNamesNC)[mmsi] = ship_name;
4543 }
4544 if (g_bUseOnlyConfirmedAISName)
4545 strncpy(pTargetData->ShipName, (*AISTargetNamesC)[mmsi].mb_str(),
4546 (*AISTargetNamesC)[mmsi].Left(20).Length() + 1);
4547 } else { // The C list name is different but no NC entry. Add it.
4548 (*AISTargetNamesNC)[mmsi] = ship_name;
4549 }
4550 }
4551 } else { // No confirmed entry available
4552 if (itNC !=
4553 AISTargetNamesNC->end()) { // there is an entry in the NC list,
4554 if ((*AISTargetNamesNC)[mmsi] ==
4555 ship_name) { // Received name same as already in NC list, promote
4556 // to confirmen
4557 (*AISTargetNamesC)[mmsi] = ship_name;
4558 // And delete from NC list
4559 AISTargetNamesNC->erase(itNC);
4560 } else { // entry in NC list is not same as received one
4561 (*AISTargetNamesNC)[mmsi] = ship_name;
4562 }
4563 } else { // No entry in NC list so add it
4564 (*AISTargetNamesNC)[mmsi] = ship_name;
4565 }
4566 if (g_bUseOnlyConfirmedAISName) { // copy back previous name
4567 pTargetData->ShipName[SHIP_NAME_LEN - 1] = '\0';
4568 strncpy(pTargetData->ShipName, "Unknown ",
4569 SHIP_NAME_LEN - 1);
4570 }
4571 }
4572 }
4573 }
4574}
4575
4576wxString GetShipNameFromFile(int nmmsi) {
4577 wxString name = "";
4578 if (g_benableAISNameCache) {
4579 std::ifstream infile(AISTargetNameFileName.mb_str());
4580 if (infile) {
4581 std::string line;
4582 while (getline(infile, line)) {
4583 wxStringTokenizer tokenizer(wxString::FromUTF8(line.c_str()), ",");
4584 if (nmmsi == wxAtoi(tokenizer.GetNextToken())) {
4585 name = tokenizer.GetNextToken().Trim();
4586 break;
4587 } else
4588 tokenizer.GetNextToken();
4589 }
4590 }
4591 infile.close();
4592 }
4593 return name;
4594}
4595
4596void AisDecoder::UpdateMMSItoNameFile(const wxString &mmsi,
4597 const wxString &name) {
4598 // Path to the mmsitoname.csv file is already in AISTargetNameFileName
4599
4600 // Create a map to hold the current contents of the file
4601 std::map<wxString, wxString> mmsi_name_map;
4602
4603 // Read the existing file
4604 std::ifstream infile(AISTargetNameFileName.mb_str());
4605 if (infile) {
4606 std::string line;
4607 while (getline(infile, line)) {
4608 wxStringTokenizer tokenizer(wxString::FromUTF8(line.c_str()), ",");
4609 wxString file_mmsi = tokenizer.GetNextToken();
4610 wxString file_name = tokenizer.GetNextToken().Trim();
4611 mmsi_name_map[file_mmsi] = file_name;
4612 }
4613 infile.close();
4614 }
4615
4616 // Update or add the new entry.
4617 mmsi_name_map[mmsi] = name.Upper();
4618
4619 // Write the updated map back to the file
4620 std::ofstream outfile(AISTargetNameFileName.mb_str());
4621 if (outfile) {
4622 for (const auto &pair : mmsi_name_map) {
4623 std::string line = std::string(pair.first.mb_str()) + "," +
4624 std::string(pair.second.mb_str()) + "\n";
4625 outfile << line;
4626 }
4627 outfile.close();
4628 }
4629}
4630
4631wxString AisDecoder::GetMMSItoNameEntry(const wxString &mmsi) {
4632 return GetShipNameFromFile(wxAtoi(mmsi));
4633}
4634
4635// Assign a unique meteo mmsi related to position
4636int AisMeteoNewMmsi(int orig_mmsi, int m_lat, int m_lon, int lon_bits = 0,
4637 int siteID = 0) {
4638 bool found = false;
4639 int new_mmsi = 0;
4640 if ((!lon_bits || lon_bits == 999) && siteID) {
4641 // Check if a ais8_367_33 data report belongs to a present site
4642 // Or SignalK data (lon_bits == 999)
4643 auto &points = AisMeteoPoints::GetInstance().GetPoints();
4644 if (points.size()) {
4645 for (const auto &point : points) {
4646 // Does this station ID exist
4647 if (siteID == point.siteID && orig_mmsi == point.orig_mmsi) {
4648 // Created before. Continue
4649 new_mmsi = point.mmsi;
4650 found = true;
4651 break;
4652 }
4653 }
4654 }
4655 if (!found && !lon_bits) {
4656 // ais8_367_33
4657 return 0;
4658 }
4659 }
4660 double lon_tentative = 181.;
4661 double lat_tentative = 91.;
4662
4663 if (lon_bits == 25) {
4664 if (m_lon & 0x01000000) // negative?
4665 m_lon |= 0xFE000000;
4666 lon_tentative = m_lon / 60000.;
4667
4668 if (m_lat & 0x00800000) // negative?
4669 m_lat |= 0xFF000000;
4670 lat_tentative = m_lat / 60000.;
4671
4672 } else if (lon_bits == 28) {
4673 if (m_lon & 0x08000000) // negative?
4674 m_lon |= 0xf0000000;
4675 lon_tentative = m_lon / 600000.;
4676
4677 if (m_lat & 0x04000000) // negative?
4678 m_lat |= 0xf8000000;
4679 lat_tentative = m_lat / 600000.;
4680 }
4681
4682 // Since buoys can move we use position precision not better
4683 // than 50 m to be able to compare previous messages
4684 wxString slon = wxString::Format("%0.3f", lon_tentative);
4685 wxString slat = wxString::Format("%0.3f", lat_tentative);
4686
4687 // Change mmsi_ID number
4688 // Some countries use one equal mmsi for all meteo stations.
4689 // Others use the same mmsi for a meteo station and a nearby AtoN
4690 // So we create our own fake mmsi to separate them.
4691 // 199 is INMARSAT-A MID, should not occur ever in AIS stream.
4692 // 1992 to 1993 are already used so here we use 1994+
4693 static int nextMeteommsi = 199400000;
4694 auto &points = AisMeteoPoints::GetInstance().GetPoints();
4695
4696 if (lon_bits != 999 && !points.empty()) { // 999 comes from SignalK
4697 wxString t_lat, t_lon;
4698 for (const auto &point : points) {
4699 // Does this station position exist
4700 if (slat.IsSameAs(point.lat) && slon.IsSameAs(point.lon)) {
4701 // Created before. Continue
4702 new_mmsi = point.mmsi;
4703 found = true;
4704 break;
4705 }
4706 }
4707 }
4708 if (!found) {
4709 // Create a new post
4710 nextMeteommsi++;
4711 points.emplace_back(
4712 AisMeteoPoint(nextMeteommsi, slat, slon, siteID, orig_mmsi));
4713 new_mmsi = nextMeteommsi;
4714 }
4715 return new_mmsi;
4716}
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.