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