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