You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

igc.cc 28KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956
  1. /*
  2. * FAI/IGC data format translation.
  3. *
  4. * Refer to Appendix 1 of
  5. * http://www.fai.org:81/gliding/gnss/tech_spec_gnss.asp for the
  6. * specification of the IGC data format. This translation code was
  7. * written when the latest ammendment list for the specification was AL6.
  8. *
  9. * Copyright (C) 2004 Chris Jones
  10. *
  11. * This program is free software; you can redistribute it and/or modify it
  12. * under the terms of the GNU General Public License as published by the
  13. * Free Software Foundation; either version 2 of the License, or (at your
  14. * option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful, but
  17. * WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  19. * General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU General Public License along
  22. * with this program; if not, write to the Free Software Foundation, Inc.,
  23. * 59 Temple Place - Suite 330, Boston, MA 02111 USA
  24. */
  25. #include "defs.h"
  26. #include "cet_util.h"
  27. #include <errno.h>
  28. #include <math.h>
  29. #include <stdio.h>
  30. #include <stdlib.h>
  31. static gbfile* file_in, *file_out;
  32. static char manufacturer[4];
  33. static const route_head* head;
  34. static char* timeadj = NULL;
  35. static int lineno;
  36. #define MYNAME "IGC"
  37. #define MAXRECLEN 79 // Includes null terminator and CR/LF
  38. #define MAXDESCLEN 1024
  39. #define PRESTRKNAME "PRESALTTRK"
  40. #define GNSSTRKNAME "GNSSALTTRK"
  41. #define HDRMAGIC "IGCHDRS"
  42. #define HDRDELIM "~"
  43. #define DATEMAGIC "IGCDATE"
  44. /*
  45. * IGC record types.
  46. * These appear as the first char in each record.
  47. */
  48. typedef enum {
  49. rec_manuf_id = 'A', // FR manufacturer and identification
  50. rec_fix = 'B', // Fix
  51. rec_task = 'C', // Task/declaration
  52. rec_diff_gps = 'D', // Differential GPS
  53. rec_event = 'E', // Event
  54. rec_constel = 'F', // Constellation
  55. rec_security = 'G', // Security
  56. rec_header = 'H', // File header
  57. rec_fix_defn = 'I', // List of extension data included at end of each fix (B) record
  58. rec_extn_defn = 'J', // List of data included in each extension (K) record
  59. rec_extn_data = 'K', // Extension data
  60. rec_log_book = 'L', // Logbook/comments
  61. // M..Z are spare
  62. rec_none = 0, // No record
  63. rec_bad = 1, // Bad record
  64. } igc_rec_type_t;
  65. /*
  66. * See if two lat/lon pairs are approximately equal.
  67. * @param lat1 The latitude of coordinate pair 1
  68. * @param lon1 The longitude of coordinate pair 1
  69. * @param lat2 The latitude of coordinate pair 2
  70. * @param lon2 The longitude of coordinate pair 2
  71. * @retval 1 The coordinates are approximately equal
  72. * @retval 0 The coordinates are significantly different
  73. */
  74. static unsigned char coords_match(double lat1, double lon1, double lat2, double lon2)
  75. {
  76. return (fabs(lat1 - lat2) < 0.0001 && fabs(lon1 - lon2) < 0.0001) ? 1 : 0;
  77. }
  78. /*************************************************************************************************
  79. * Input file processing
  80. */
  81. /*
  82. * Get an IGC record from the input file
  83. * @param rec Caller allocated storage for the record. At least MAXRECLEN chars must be allocated.
  84. * @return the record type. rec_none on EOF, rec_bad on fgets() or parse error.
  85. */
  86. static igc_rec_type_t get_record(char** rec)
  87. {
  88. size_t len;
  89. char* c;
  90. retry:
  91. *rec = c = gbfgetstr(file_in);
  92. if ((lineno++ == 0) && file_in->unicode) {
  93. cet_convert_init(CET_CHARSET_UTF8, 1);
  94. }
  95. if (c == NULL) {
  96. return rec_none;
  97. }
  98. len = strlen(c);
  99. /* Trackwiev writes (bogus) blank links between each record */
  100. if (len == 0) {
  101. goto retry;
  102. }
  103. if (len < 3 || c[0] < 'A' || c[0] > 'Z') {
  104. warning(MYNAME " bad input record: '%s'\n", c);
  105. return rec_bad;
  106. }
  107. return (igc_rec_type_t) c[0];
  108. }
  109. static void rd_init(const QString& fname)
  110. {
  111. char* ibuf;
  112. file_in = gbfopen(fname, "r", MYNAME);
  113. lineno = 0;
  114. // File must begin with a manufacturer/ID record
  115. if (get_record(&ibuf) != rec_manuf_id || sscanf(ibuf, "A%3[A-Z]", manufacturer) != 1) {
  116. fatal(MYNAME ": %s is not an IGC file\n", qPrintable(fname));
  117. }
  118. }
  119. static void rd_deinit(void)
  120. {
  121. gbfclose(file_in);
  122. }
  123. typedef enum { id, takeoff, start, turnpoint, finish, landing } state_t;
  124. #if __cplusplus
  125. inline state_t operator++(state_t& rs, int)
  126. {
  127. return rs = (state_t)((int)rs + 1);
  128. }
  129. #endif
  130. /**
  131. * Handle pre- or post-flight task declarations.
  132. * A route is created for each set of waypoints in a task declaration.
  133. * @param rec A single task record
  134. */
  135. static void igc_task_rec(const char* rec)
  136. {
  137. static char flight_date[7];
  138. static unsigned int num_tp, tp_ct;
  139. static route_head* rte_head;
  140. static time_t creation;
  141. char task_num[5];
  142. char task_desc[MAXRECLEN];
  143. Waypoint* wpt;
  144. unsigned int lat_deg, lat_min, lat_frac;
  145. unsigned int lon_deg, lon_min, lon_frac;
  146. char lat_hemi[2], lon_hemi[2];
  147. char short_name[8];
  148. char tmp_str[MAXRECLEN];
  149. struct tm tm;
  150. static state_t state = id;
  151. // First task record identifies the task to follow
  152. if (id == state) {
  153. task_desc[0] = '\0';
  154. if (sscanf(rec, "C%2u%2u%2u%2u%2u%2u%6[0-9]%4c%2u%[^\r]\r\n",
  155. &tm.tm_mday, &tm.tm_mon, &tm.tm_year,
  156. &tm.tm_hour, &tm.tm_min, &tm.tm_sec,
  157. flight_date, task_num, &num_tp, task_desc) < 9) {
  158. fatal(MYNAME ": task id (C) record parse error\n'%s'", rec);
  159. }
  160. task_num[4] = '\0';
  161. tm.tm_mon -= 1;
  162. if (tm.tm_year < 70) {
  163. tm.tm_year += 100;
  164. }
  165. tm.tm_isdst = 0;
  166. creation = mkgmtime(&tm);
  167. // Create a route to store the task data in.
  168. rte_head = route_head_alloc();
  169. rte_head->rte_name = task_num;
  170. sprintf(tmp_str, DATEMAGIC "%s: %s", flight_date, task_desc);
  171. rte_head->rte_desc = tmp_str;
  172. route_add_head(rte_head);
  173. state++;
  174. return;
  175. }
  176. // Get the waypoint
  177. tmp_str[0] = '\0';
  178. if (sscanf(rec, "C%2u%2u%3u%1[NS]%3u%2u%3u%1[WE]%[^\r]\r\n",
  179. &lat_deg, &lat_min, &lat_frac, lat_hemi,
  180. &lon_deg, &lon_min, &lon_frac, lon_hemi, tmp_str) < 8) {
  181. fatal(MYNAME ": task waypoint (C) record parse error\n%s", rec);
  182. }
  183. wpt = new Waypoint;
  184. wpt->latitude = ('N' == lat_hemi[0] ? 1 : -1) *
  185. (lat_deg + (lat_min * 1000 + lat_frac) / 1000.0 / 60);
  186. wpt->longitude = ('E' == lon_hemi[0] ? 1 : -1) *
  187. (lon_deg + (lon_min * 1000 + lon_frac) / 1000.0 / 60);
  188. wpt->SetCreationTime(creation);
  189. wpt->description = tmp_str;
  190. // Name the waypoint according to the order of the task record
  191. switch (state) {
  192. case takeoff:
  193. snprintf(short_name, 8, "TAKEOFF");
  194. state++;
  195. break;
  196. case start:
  197. snprintf(short_name, 8, "START");
  198. tp_ct = 0;
  199. state++;
  200. break;
  201. case turnpoint:
  202. if (++tp_ct == num_tp) {
  203. state++;
  204. }
  205. snprintf(short_name, 8, "TURN%02u", tp_ct);
  206. break;
  207. case finish:
  208. snprintf(short_name, 8, "FINISH");
  209. state++;
  210. break;
  211. case landing:
  212. snprintf(short_name, 8, "LANDING");
  213. state = id;
  214. break;
  215. default:
  216. fatal(MYNAME ": task id (C) record internal error\n%s", rec);
  217. break;
  218. }
  219. // Zero lat and lon indicates an unknown waypoint
  220. if (coords_match(wpt->latitude, wpt->longitude, 0.0, 0.0)) {
  221. delete wpt;
  222. return;
  223. }
  224. wpt->shortname = short_name;
  225. route_add_wpt(rte_head, wpt);
  226. }
  227. static void data_read(void)
  228. {
  229. char* ibuf;
  230. igc_rec_type_t rec_type;
  231. unsigned int hours, mins, secs;
  232. unsigned int lat_deg, lat_min, lat_frac;
  233. unsigned int lon_deg, lon_min, lon_frac;
  234. char lat_hemi[2], lon_hemi[2];
  235. char validity;
  236. route_head* pres_head = NULL;
  237. route_head* gnss_head = NULL;
  238. int pres_alt, gnss_alt;
  239. char pres_valid = 0;
  240. char gnss_valid = 0;
  241. Waypoint* pres_wpt = NULL;
  242. Waypoint* gnss_wpt = NULL;
  243. time_t date = 0;
  244. time_t prev_tod = 0;
  245. time_t tod;
  246. struct tm tm;
  247. char tmp_str[20];
  248. char* hdr_data;
  249. size_t remain;
  250. char trk_desc[MAXDESCLEN + 1];
  251. strcpy(trk_desc, HDRMAGIC HDRDELIM);
  252. while (1) {
  253. rec_type = get_record(&ibuf);
  254. switch (rec_type) {
  255. case rec_manuf_id:
  256. // Manufacturer/ID record already found in rd_init().
  257. warning(MYNAME ": duplicate manufacturer/ID record\n");
  258. break;
  259. case rec_header:
  260. // Get the header sub type
  261. if (sscanf(ibuf, "H%*1[FOP]%3s", tmp_str) != 1) {
  262. fatal(MYNAME ": header (H) record parse error\n%s\n%s\n", ibuf, tmp_str);
  263. }
  264. // Optional long name of record sub type is followed by a
  265. // colon. Actual header data follows that.
  266. if (NULL == (hdr_data = strchr(ibuf, ':'))) {
  267. hdr_data = ibuf + 5;
  268. } else {
  269. hdr_data++;
  270. }
  271. // Date sub type
  272. if (strcmp(tmp_str, "DTE") == 0) {
  273. if (sscanf(hdr_data, "%2u%2u%2u", &tm.tm_mday, &tm.tm_mon, &tm.tm_year) != 3) {
  274. fatal(MYNAME ": date (H) record parse error\n'%s'\n", ibuf);
  275. }
  276. tm.tm_sec = tm.tm_min = tm.tm_hour = 0;
  277. tm.tm_mon -= 1;
  278. if (tm.tm_year < 70) {
  279. tm.tm_year += 100;
  280. }
  281. tm.tm_isdst = 0;
  282. date = mkgmtime(&tm);
  283. } else {
  284. // Store other header data in the track descriptions
  285. if (strlen(trk_desc) < MAXDESCLEN) {
  286. strcat(ibuf, HDRDELIM);
  287. remain = MAXDESCLEN - strlen(trk_desc);
  288. strncat(trk_desc, ibuf, remain);
  289. }
  290. }
  291. break;
  292. case rec_fix:
  293. // Date must appear in file before the first fix record
  294. if (date < 1000000L) {
  295. fatal(MYNAME ": bad date %d\n", (int)date);
  296. }
  297. // Create a track for pressure altitude waypoints
  298. if (!pres_head) {
  299. pres_head = route_head_alloc();
  300. pres_head->rte_name = PRESTRKNAME;
  301. pres_head->rte_desc = trk_desc;
  302. track_add_head(pres_head);
  303. }
  304. // Create a second track for GNSS altitude waypoints
  305. if (!gnss_head) {
  306. gnss_head = route_head_alloc();
  307. gnss_head->rte_name = GNSSTRKNAME;
  308. gnss_head->rte_desc = trk_desc;
  309. track_add_head(gnss_head);
  310. }
  311. // Create a waypoint from the fix record data
  312. if (sscanf(ibuf,
  313. "B%2u%2u%2u%2u%2u%3u%1[NS]%3u%2u%3u%1[WE]%c%5d%5d",
  314. &hours, &mins, &secs, &lat_deg, &lat_min, &lat_frac,
  315. lat_hemi, &lon_deg, &lon_min, &lon_frac, lon_hemi,
  316. &validity, &pres_alt, &gnss_alt) != 14) {
  317. fatal(MYNAME ": fix (B) record parse error\n%s\n", ibuf);
  318. }
  319. pres_wpt = new Waypoint;
  320. pres_wpt->latitude = ('N' == lat_hemi[0] ? 1 : -1) *
  321. (lat_deg + (lat_min * 1000 + lat_frac) / 1000.0 / 60);
  322. pres_wpt->longitude = ('E' == lon_hemi[0] ? 1 : -1) *
  323. (lon_deg + (lon_min * 1000 + lon_frac) / 1000.0 / 60);
  324. // Increment date if we pass midnight UTC
  325. tod = (hours * 60 + mins) * 60 + secs;
  326. if (tod < prev_tod) {
  327. date += 24 * 60 * 60;
  328. }
  329. prev_tod = tod;
  330. pres_wpt->SetCreationTime(date + tod);
  331. // Add the waypoint to the pressure altitude track
  332. if (pres_alt) {
  333. pres_valid = 1;
  334. pres_wpt->altitude = pres_alt;
  335. } else {
  336. pres_wpt->altitude = unknown_alt;
  337. }
  338. track_add_wpt(pres_head, pres_wpt);
  339. // Add the same waypoint with GNSS altitude to the second
  340. // track
  341. gnss_wpt = new Waypoint(*pres_wpt);
  342. if (gnss_alt) {
  343. gnss_valid = 1;
  344. gnss_wpt->altitude = gnss_alt;
  345. } else {
  346. gnss_wpt->altitude = unknown_alt;
  347. }
  348. track_add_wpt(gnss_head, gnss_wpt);
  349. break;
  350. case rec_task:
  351. // Create a route for each pre-flight declaration
  352. igc_task_rec(ibuf);
  353. break;
  354. case rec_log_book:
  355. // Get the log book sub type
  356. if (sscanf(ibuf, "L%3s", tmp_str) != 1) {
  357. fatal(MYNAME ": log book (L) record parse error\n'%s'\n", ibuf);
  358. }
  359. if (strcmp(tmp_str, "PFC") == 0) {
  360. // Create a route for each post-flight declaration
  361. igc_task_rec(ibuf + 4);
  362. break;
  363. } else if (global_opts.debug_level) {
  364. if (strcmp(tmp_str, "OOI") == 0) {
  365. fputs(MYNAME ": Observer Input> ", stdout);
  366. } else if (strcmp(tmp_str, "PLT") == 0) {
  367. fputs(MYNAME ": Pilot Input> ", stdout);
  368. } else if (strcmp(tmp_str, manufacturer) == 0) {
  369. fputs(MYNAME ": Manufacturer Input> ", stdout);
  370. } else {
  371. fputs(MYNAME ": Anonymous Input> ", stdout);
  372. fputs(ibuf + 1, stdout);
  373. break;
  374. }
  375. fputs(ibuf + 4, stdout);
  376. putchar('\n');
  377. }
  378. break;
  379. // These record types are discarded
  380. case rec_diff_gps:
  381. case rec_event:
  382. case rec_constel:
  383. case rec_security:
  384. case rec_fix_defn:
  385. case rec_extn_defn:
  386. case rec_extn_data:
  387. break;
  388. // No more records
  389. case rec_none:
  390. // Include pressure altitude track only if it has useful
  391. // altitude data or if it is the only track available.
  392. if (pres_head && !pres_valid && gnss_head) {
  393. track_del_head(pres_head);
  394. pres_head = NULL;
  395. }
  396. // Include GNSS altitude track only if it has useful altitude
  397. // data or if it is the only track available.
  398. if (gnss_head && !gnss_valid && pres_head) {
  399. track_del_head(gnss_head);
  400. }
  401. return; // All done so bail
  402. default:
  403. case rec_bad:
  404. fatal(MYNAME ": failure reading file\n");
  405. break;
  406. }
  407. }
  408. }
  409. /*************************************************************************************************
  410. * Output file processing
  411. */
  412. /*************************************************
  413. * Callbacks used to scan for specific track types
  414. */
  415. static void detect_pres_track(const route_head* rh)
  416. {
  417. if (rh->rte_name.startsWith(PRESTRKNAME)) {
  418. head = rh;
  419. }
  420. }
  421. static void detect_gnss_track(const route_head* rh)
  422. {
  423. if (rh->rte_name.startsWith(GNSSTRKNAME)) {
  424. head = rh;
  425. }
  426. }
  427. static void detect_other_track(const route_head* rh)
  428. {
  429. static int max_waypt_ct;
  430. if (!head) {
  431. max_waypt_ct = 0;
  432. }
  433. // Find other track with the most waypoints
  434. if (rh->rte_waypt_ct > max_waypt_ct &&
  435. (rh->rte_name.isEmpty() ||
  436. (!rh->rte_name.startsWith(PRESTRKNAME) &&
  437. !rh->rte_name.startsWith(GNSSTRKNAME)))) {
  438. head = rh;
  439. max_waypt_ct = rh->rte_waypt_ct;
  440. }
  441. }
  442. /*
  443. * Identify the pressure altitude and GNSS altitude tracks.
  444. * @param pres_track Set by the function to the pressure altitude track
  445. * head. NULL if not found.
  446. * @param gnss_track Set by the function to the GNSS altitude track
  447. * head. NULL if not found.
  448. */
  449. static void get_tracks(const route_head** pres_track, const route_head** gnss_track)
  450. {
  451. head = NULL;
  452. track_disp_all(detect_pres_track, NULL, NULL);
  453. *pres_track = head;
  454. head = NULL;
  455. track_disp_all(detect_gnss_track, NULL, NULL);
  456. *gnss_track = head;
  457. head = NULL;
  458. track_disp_all(detect_other_track, NULL, NULL);
  459. if (!*pres_track && *gnss_track && head) {
  460. *pres_track = head;
  461. }
  462. if (!*gnss_track && head) {
  463. *gnss_track = head;
  464. }
  465. }
  466. /*************************************************
  467. * IGC string formatting functions
  468. */
  469. static char* latlon2str(const Waypoint* wpt)
  470. {
  471. static char str[18] = "";
  472. char lat_hemi = wpt->latitude < 0 ? 'S' : 'N';
  473. char lon_hemi = wpt->longitude < 0 ? 'W' : 'E';
  474. unsigned char lat_deg = fabs(wpt->latitude);
  475. unsigned char lon_deg = fabs(wpt->longitude);
  476. unsigned int lat_min = (fabs(wpt->latitude) - lat_deg) * 60000 + 0.500000000001;
  477. unsigned int lon_min = (fabs(wpt->longitude) - lon_deg) * 60000 + 0.500000000001;
  478. if (snprintf(str, 18, "%02u%05u%c%03u%05u%c",
  479. lat_deg, lat_min, lat_hemi, lon_deg, lon_min, lon_hemi) != 17) {
  480. fatal(MYNAME ": Bad waypoint format '%s'\n", str);
  481. }
  482. return str;
  483. }
  484. static char* date2str(struct tm* dt)
  485. {
  486. static char str[7] = "";
  487. if (snprintf(str, 7, "%02u%02u%02u", dt->tm_mday, dt->tm_mon + 1, dt->tm_year % 100) != 6) {
  488. fatal(MYNAME ": Bad date format '%s'\n", str);
  489. }
  490. return str;
  491. }
  492. static char* tod2str(struct tm* tod)
  493. {
  494. static char str[7] = "";
  495. if (snprintf(str, 7, "%02u%02u%02u", tod->tm_hour, tod->tm_min, tod->tm_sec) != 6) {
  496. fatal(MYNAME ": Bad time of day format '%s'\n", str);
  497. }
  498. return str;
  499. }
  500. /*
  501. * Write header records
  502. */
  503. static void wr_header(void)
  504. {
  505. const route_head* pres_track;
  506. const route_head* track;
  507. struct tm* tm;
  508. time_t date;
  509. static const char dflt_str[] = "Unknown";
  510. const char* str = NULL;
  511. Waypoint* wpt;
  512. get_tracks(&pres_track, &track);
  513. if (!track && pres_track) {
  514. track = pres_track;
  515. }
  516. // Date in header record is that of the first fix record
  517. date = !track ? current_time().toTime_t() :
  518. ((Waypoint*) QUEUE_FIRST(&track->waypoint_list))->GetCreationTime().toTime_t();
  519. if (NULL == (tm = gmtime(&date))) {
  520. fatal(MYNAME ": Bad track timestamp\n");
  521. }
  522. gbfprintf(file_out, "HFDTE%s\r\n", date2str(tm));
  523. // Other header data may have been stored in track description
  524. #if NEW_STRINGS
  525. if (track && track->rte_desc.startsWith(HDRMAGIC)) {
  526. char *rd = xstrdup(track->rte_desc);
  527. for (str = strtok(rd + strlen(HDRMAGIC) + strlen(HDRDELIM), HDRDELIM);
  528. str; str = strtok(NULL, HDRDELIM)) {
  529. gbfprintf(file_out, "%s\r\n", str);
  530. }
  531. xfree(rd);
  532. rd = NULL;
  533. #else
  534. if (track && track->rte_desc && strncmp(track->rte_desc, HDRMAGIC, strlen(HDRMAGIC)) == 0) {
  535. for (str = strtok(CSTRc(track->rte_desc) + strlen(HDRMAGIC) + strlen(HDRDELIM), HDRDELIM);
  536. str; str = strtok(NULL, HDRDELIM)) {
  537. gbfprintf(file_out, "%s\r\n", str);
  538. }
  539. #endif
  540. } else {
  541. #if NEW_STRINGS
  542. // FIXME: This almost certainly introduces a memory leak because str
  543. // is a c string that's used for totally too many things. Just let it
  544. // leak for now. 2013-12-31 robertl
  545. if (NULL != (wpt = find_waypt_by_name("PILOT")) && !wpt->description.isEmpty()) {
  546. xfree(str);
  547. str = xstrdup(CSTRc(wpt->description));
  548. #else
  549. if (NULL != (wpt = find_waypt_by_name("PILOT")) && wpt->description) {
  550. str = CSTRc(wpt->description);
  551. #endif
  552. } else {
  553. // IGC header info not found so synthesise it.
  554. // If a waypoint is supplied with a short name of "PILOT", use
  555. // its description as the pilot's name in the header.
  556. str = xstrdup(dflt_str);
  557. }
  558. gbfprintf(file_out, "HFPLTPILOT:%s\r\n", str);
  559. xfree(str);
  560. }
  561. }
  562. /*************************************************
  563. * Generation of IGC task declaration records
  564. */
  565. static void wr_task_wpt_name(const Waypoint* wpt, const char* alt_name)
  566. {
  567. gbfprintf(file_out, "C%s%s\r\n", latlon2str(wpt),
  568. !wpt->description.isEmpty() ? CSTR(wpt->description) : !wpt->shortname.isEmpty() ? CSTR(wpt->shortname) : alt_name);
  569. }
  570. static void wr_task_hdr(const route_head* rte)
  571. {
  572. unsigned char have_takeoff = 0;
  573. const Waypoint* wpt;
  574. char flight_date[7] = "000000";
  575. char task_desc[MAXRECLEN] = "";
  576. int num_tps = rte->rte_waypt_ct - 2;
  577. struct tm* tm;
  578. time_t rte_time;
  579. static unsigned int task_num = 1;
  580. if (num_tps < 0) {
  581. fatal(MYNAME ": Empty task route\n");
  582. }
  583. // See if the takeoff and landing waypoints are there or if we need to
  584. // generate them.
  585. wpt = (Waypoint*) QUEUE_LAST(&rte->waypoint_list);
  586. if (wpt->shortname.startsWith("LANDING")) {
  587. num_tps--;
  588. }
  589. wpt = (Waypoint*) QUEUE_FIRST(&rte->waypoint_list);
  590. if (wpt->shortname.startsWith("TAKEOFF")) {
  591. have_takeoff = 1;
  592. num_tps--;
  593. }
  594. if (num_tps < 0) {
  595. fatal(MYNAME ": Too few waypoints in task route\n");
  596. } else if (num_tps > 99) {
  597. fatal(MYNAME ": Too much waypoints (more than 99) in task route.\n");
  598. }
  599. // Gather data to write to the task identification (first) record
  600. rte_time = wpt->GetCreationTime().isValid() ? wpt->GetCreationTime().toTime_t() : current_time().toTime_t();
  601. if (NULL == (tm = gmtime(&rte_time))) {
  602. fatal(MYNAME ": Bad task route timestamp\n");
  603. }
  604. if (!rte->rte_desc.isEmpty()) {
  605. // desc will be something like "IGCDATE160701: 500KTri"
  606. sscanf(CSTR(rte->rte_desc), DATEMAGIC "%6[0-9]: %s", flight_date, task_desc);
  607. }
  608. gbfprintf(file_out, "C%s%s%s%04u%02u%s\r\n", date2str(tm),
  609. tod2str(tm), flight_date, task_num++, num_tps, task_desc);
  610. if (!have_takeoff) {
  611. // Generate the takeoff waypoint
  612. wr_task_wpt_name(wpt, "TAKEOFF");
  613. }
  614. }
  615. static void wr_task_wpt(const Waypoint* wpt)
  616. {
  617. wr_task_wpt_name(wpt, "");
  618. }
  619. static void wr_task_tlr(const route_head* rte)
  620. {
  621. // If the landing waypoint is not supplied we need to generate it.
  622. const Waypoint* wpt = (Waypoint*) QUEUE_LAST(&rte->waypoint_list);
  623. QString sn = wpt->shortname;
  624. // if (!wpt->shortname || strncmp(wpt->shortname, "LANDIN", 6) != 0) {
  625. if (sn.isEmpty() || !sn.startsWith("LANDIN")) {
  626. wr_task_wpt_name(wpt, "LANDING");
  627. }
  628. }
  629. static void wr_tasks(void)
  630. {
  631. route_disp_all(wr_task_hdr, wr_task_tlr, wr_task_wpt);
  632. }
  633. /*
  634. * Write a single fix record
  635. */
  636. static void wr_fix_record(const Waypoint* wpt, int pres_alt, int gnss_alt)
  637. {
  638. struct tm* tm;
  639. const time_t tt = wpt->GetCreationTime().toTime_t();
  640. tm = gmtime(&tt);
  641. if (NULL == tm) {
  642. fatal(MYNAME ": bad track timestamp\n");
  643. }
  644. if (unknown_alt == pres_alt) {
  645. pres_alt = 0;
  646. }
  647. if (unknown_alt == gnss_alt) {
  648. gnss_alt = 0;
  649. }
  650. gbfprintf(file_out, "B%02u%02u%02u%sA%05d%05d\r\n", tm->tm_hour,
  651. tm->tm_min, tm->tm_sec, latlon2str(wpt), pres_alt, gnss_alt);
  652. }
  653. /**
  654. * Attempt to align the pressure and GNSS tracks in time.
  655. * This is useful when trying to merge a track (lat/lon/time) recorded by a
  656. * GPS with a barograph (alt/time) recorded by a seperate instrument with
  657. * independent clocks which are not closely synchronised.
  658. * @return The number of seconds to add to the GNSS track in order to align
  659. * it with the pressure track.
  660. */
  661. static int correlate_tracks(const route_head* pres_track, const route_head* gnss_track)
  662. {
  663. const queue* elem;
  664. double last_alt, alt_diff;
  665. double speed;
  666. time_t pres_time, gnss_time;
  667. int time_diff;
  668. const Waypoint* wpt;
  669. // Deduce the landing time from the pressure altitude track based on
  670. // when we last descended to within 10m of the final track altitude.
  671. elem = QUEUE_LAST(&pres_track->waypoint_list);
  672. last_alt = ((Waypoint*) elem)->altitude;
  673. do {
  674. elem = elem->prev;
  675. if (&pres_track->waypoint_list == elem) {
  676. // No track left
  677. return 0;
  678. }
  679. alt_diff = last_alt - ((Waypoint*) elem)->altitude;
  680. if (alt_diff > 10.0) {
  681. // Last part of track was ascending
  682. return 0;
  683. }
  684. } while (alt_diff > -10.0);
  685. pres_time = ((Waypoint*) elem->next)->GetCreationTime().toTime_t();
  686. if (global_opts.debug_level >= 1) {
  687. printf(MYNAME ": pressure landing time %s", ctime(&pres_time));
  688. }
  689. // Deduce the landing time from the GNSS altitude track based on
  690. // when the groundspeed last dropped below a certain level.
  691. elem = QUEUE_LAST(&gnss_track->waypoint_list);
  692. last_alt = ((Waypoint*) elem)->altitude;
  693. do {
  694. wpt = (Waypoint*) elem;
  695. elem = elem->prev;
  696. if (&gnss_track->waypoint_list == elem) {
  697. // No track left
  698. return 0;
  699. }
  700. // Get a crude indication of groundspeed from the change in lat/lon
  701. time_diff = wpt->GetCreationTime().toTime_t() - ((Waypoint*) elem)->GetCreationTime().toTime_t();
  702. speed = !time_diff ? 0 :
  703. (fabs(wpt->latitude - ((Waypoint*) elem)->latitude) +
  704. fabs(wpt->longitude - ((Waypoint*) elem)->longitude)) / time_diff;
  705. if (global_opts.debug_level >= 2) {
  706. printf(MYNAME ": speed=%f\n", speed);
  707. }
  708. } while (speed < 0.00003);
  709. gnss_time = ((Waypoint*) elem->next)->GetCreationTime().toTime_t();
  710. if (global_opts.debug_level >= 1) {
  711. printf(MYNAME ": gnss landing time %s", ctime(&gnss_time));
  712. }
  713. // Time adjustment is difference between the two estimated landing times
  714. if (15 * 60 < abs(time_diff = pres_time - gnss_time)) {
  715. warning(MYNAME ": excessive time adjustment %ds\n", time_diff);
  716. }
  717. return time_diff;
  718. }
  719. /**
  720. * Interpolate altitude from a track at a given time.
  721. * @param track The track containing altitude data.
  722. * @param time The time that we are interested in.
  723. * @return The altitude interpolated from the track.
  724. */
  725. static double interpolate_alt(const route_head* track, time_t time)
  726. {
  727. static const queue* prev_elem = NULL;
  728. static const queue* curr_elem = NULL;
  729. const Waypoint* prev_wpt;
  730. const Waypoint* curr_wpt;
  731. int time_diff;
  732. double alt_diff;
  733. // Start search at the beginning of the track
  734. if (!prev_elem) {
  735. curr_elem = prev_elem = QUEUE_FIRST(&track->waypoint_list);
  736. }
  737. // Find the track points either side of the requested time
  738. while (((Waypoint*) curr_elem)->GetCreationTime().toTime_t() < time) {
  739. if (QUEUE_LAST(&track->waypoint_list) == curr_elem) {
  740. // Requested time later than all track points, we can't interpolate
  741. return unknown_alt;
  742. }
  743. prev_elem = curr_elem;
  744. curr_elem = QUEUE_NEXT(prev_elem);
  745. }
  746. prev_wpt = (Waypoint*) prev_elem;
  747. curr_wpt = (Waypoint*) curr_elem;
  748. if (QUEUE_FIRST(&track->waypoint_list) == curr_elem) {
  749. if (curr_wpt->GetCreationTime().toTime_t() == time) {
  750. // First point's creation time is an exact match so use it's altitude
  751. return curr_wpt->altitude;
  752. } else {
  753. // Requested time is prior to any track points, we can't interpolate
  754. return unknown_alt;
  755. }
  756. }
  757. // Interpolate
  758. if (0 == (time_diff = curr_wpt->GetCreationTime().toTime_t() - prev_wpt->GetCreationTime().toTime_t())) {
  759. // Avoid divide by zero
  760. return curr_wpt->altitude;
  761. }
  762. alt_diff = curr_wpt->altitude - prev_wpt->altitude;
  763. return prev_wpt->altitude + (alt_diff / time_diff) * (time - prev_wpt->GetCreationTime().toTime_t());
  764. }
  765. /*
  766. * Pressure altitude and GNSS altitude may be provided in two seperate
  767. * tracks. This function attempts to merge them into one.
  768. */
  769. static void wr_track(void)
  770. {
  771. const route_head* pres_track;
  772. const route_head* gnss_track;
  773. const Waypoint* wpt;
  774. const queue* elem;
  775. const queue* tmp;
  776. int time_adj;
  777. double pres_alt;
  778. // Find pressure altitude and GNSS altitude tracks
  779. get_tracks(&pres_track, &gnss_track);
  780. // If both found, attempt to merge them
  781. if (pres_track && gnss_track) {
  782. if (timeadj) {
  783. if (strcmp(timeadj, "auto") == 0) {
  784. time_adj = correlate_tracks(pres_track, gnss_track);
  785. } else if (sscanf(timeadj, "%d", &time_adj) != 1) {
  786. fatal(MYNAME ": bad timeadj argument '%s'\n", timeadj);
  787. }
  788. } else {
  789. time_adj = 0;
  790. }
  791. if (global_opts.debug_level >= 1) {
  792. printf(MYNAME ": adjusting time by %ds\n", time_adj);
  793. }
  794. // Iterate through waypoints in both tracks simultaneously
  795. QUEUE_FOR_EACH(&gnss_track->waypoint_list, elem, tmp) {
  796. wpt = (Waypoint*) elem;
  797. pres_alt = interpolate_alt(pres_track, wpt->GetCreationTime().toTime_t() + time_adj);
  798. wr_fix_record(wpt, (int) pres_alt, (int) wpt->altitude);
  799. }
  800. } else {
  801. if (pres_track) {
  802. // Only the pressure altitude track was found so generate fix
  803. // records from it alone.
  804. QUEUE_FOR_EACH(&pres_track->waypoint_list, elem, tmp) {
  805. wr_fix_record((Waypoint*) elem, (int)((Waypoint*) elem)->altitude, (int) unknown_alt);
  806. }
  807. } else if (gnss_track) {
  808. // Only the GNSS altitude track was found so generate fix
  809. // records from it alone.
  810. QUEUE_FOR_EACH(&gnss_track->waypoint_list, elem, tmp) {
  811. wr_fix_record((Waypoint*) elem, (int) unknown_alt, (int)((Waypoint*) elem)->altitude);
  812. }
  813. } else {
  814. // No tracks found so nothing to do
  815. return;
  816. }
  817. }
  818. }
  819. static void wr_init(const QString& fname)
  820. {
  821. file_out = gbfopen(fname, "wb", MYNAME);
  822. }
  823. static void wr_deinit(void)
  824. {
  825. gbfclose(file_out);
  826. }
  827. static void data_write(void)
  828. {
  829. gbfputs("AXXXZZZGPSBabel\r\n", file_out);
  830. wr_header();
  831. wr_tasks();
  832. wr_track();
  833. gbfprintf(file_out, "LXXXGenerated by GPSBabel Version %s\r\n", gpsbabel_version);
  834. gbfputs("GGPSBabelSecurityRecordGuaranteedToFailVALIChecks\r\n", file_out);
  835. }
  836. static arglist_t igc_args[] = {
  837. {
  838. "timeadj", &timeadj,
  839. "(integer sec or 'auto') Barograph to GPS time diff",
  840. NULL, ARGTYPE_STRING, ARG_NOMINMAX
  841. },
  842. ARG_TERMINATOR
  843. };
  844. ff_vecs_t igc_vecs = {
  845. ff_type_file,
  846. { ff_cap_none , (ff_cap)(ff_cap_read | ff_cap_write), (ff_cap)(ff_cap_read | ff_cap_write) },
  847. rd_init,
  848. wr_init,
  849. rd_deinit,
  850. wr_deinit,
  851. data_read,
  852. data_write,
  853. NULL,
  854. igc_args,
  855. CET_CHARSET_ASCII, 0 /* CET-REVIEW */
  856. };