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