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