1 /*
2
3 Support for MapSource Text Export (Tab delimited) files.
4
5 Copyright (C) 2006 Olaf Klein, o.b.klein@gpsbabel.org
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or
10 (at your option) any later version.
11
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
21 */
22
23 #include "defs.h"
24
25 #if CSVFMTS_ENABLED
26 #include <cctype> // for toupper
27 #include <cmath> // for fabs, floor
28 #include <cstdio> // for NULL, snprintf, sscanf
29 #include <cstdint>
30 #include <cstdlib> // for atoi, abs, qsort
31 #include <cstring> // for memset, strstr, strcat, strchr, strlen, strcmp, strcpy, strncpy
32 #include <ctime> // for gmtime, localtime, strftime
33
34 #include <QtCore/QByteArray> // for QByteArray
35 #include <QtCore/QChar> // for QChar, QChar::Other_Control
36 #include <QtCore/QIODevice> // for QIODevice, QIODevice::ReadOnly, QIODevice::WriteOnly
37 #include <QtCore/QString> // for QString, operator!=
38 #include <QtCore/QTextStream> // for QTextStream
39 #include <QtCore/QVector> // for QVector
40 #include <QtCore/Qt> // for CaseInsensitive
41 #include <QtCore/QtGlobal> // for qPrintable
42
43 #include "csv_util.h" // for csv_lineparse
44 #include "garmin_fs.h" // for garmin_fs_t, garmin_fs_alloc, garmin_fs_convert_category, GMSD_SECTION_CATEGORIES
45 #include "garmin_tables.h" // for gt_display_modes_e, gt_find_desc_from_icon_number, gt_find_icon_number_from_desc, gt_get_mps_grid_longname, gt_lookup_datum_index, gt_lookup_grid_type, GDB, gt_get_icao_cc, gt_get_icao_country, gt_get_mps_datum_name, gt_waypt_class_names, GT_DISPLAY_MODE...
46 #include "inifile.h" // for inifile_readstr
47 #include "jeeps/gpsmath.h" // for GPS_Math_Known_Datum_To_UTM_EN, GPS_Math_WGS84_To_Known_Datum_M, GPS_Math_WGS84_To_Swiss_EN, GPS_Math_WGS84_To_UKOSMap_M
48 #include "src/core/datetime.h" // for DateTime
49 #include "src/core/textstream.h" // for TextStream
50 #include "strptime.h" // for strptime
51
52
53 #define MYNAME "garmin_txt"
54
55 struct gtxt_flags_t {
56 unsigned int metric:1;
57 unsigned int celsius:1;
58 unsigned int utc:1;
59 unsigned int enum_waypoints:1;
60 unsigned int route_header_written:1;
61 unsigned int track_header_written:1;
62 };
63
64 static gpsbabel::TextStream* fin = nullptr;
65 static gpsbabel::TextStream* fout = nullptr;
66 static route_head* current_trk, *current_rte;
67 static int waypoints;
68 static int routepoints;
69 static const Waypoint** wpt_a;
70 static int wpt_a_ct;
71 static grid_type grid_index;
72 static int datum_index;
73 static const char* datum_str;
74 static int current_line;
75 static char* date_time_format = nullptr;
76 static int precision = 3;
77 static time_t utc_offs = 0;
78
79 static gtxt_flags_t gtxt_flags;
80
81 enum header_type {
82 waypt_header = 0,
83 rtept_header,
84 trkpt_header,
85 route_header,
86 track_header,
87 unknown_header
88 };
89
operator ++(header_type & s)90 inline header_type& operator++(header_type& s) // prefix
91 {
92 return s = static_cast<header_type>(s + 1);
93 }
operator ++(header_type & s,int)94 inline header_type operator++(header_type& s, int) // postfix
95 {
96 header_type ret(s);
97 s = ++s;
98 return ret;
99 }
100
operator ++(gt_display_modes_e & s)101 inline gt_display_modes_e& operator++(gt_display_modes_e& s) // prefix
102 {
103 return s = static_cast<gt_display_modes_e>(s + 1);
104 }
operator ++(gt_display_modes_e & s,int)105 inline gt_display_modes_e operator++(gt_display_modes_e& s, int) // postfix
106 {
107 gt_display_modes_e ret(s);
108 s = ++s;
109 return ret;
110 }
111
112 #define MAX_HEADER_FIELDS 36
113
114 static char* header_lines[unknown_header + 1][MAX_HEADER_FIELDS];
115 static int header_fields[unknown_header + 1][MAX_HEADER_FIELDS];
116 static int header_ct[unknown_header + 1];
117
118 #define GARMIN_UNKNOWN_ALT 1.0e25f
119 #define DEFAULT_DISPLAY garmin_display_symbol_and_name
120 #define DEFAULT_DATE_FORMAT "dd/mm/yyyy"
121 #define DEFAULT_TIME_FORMAT "HH:mm:ss"
122
123 /* macros */
124
125 #define IS_VALID_ALT(a) (((a) != unknown_alt) && ((a) < GARMIN_UNKNOWN_ALT))
126 #define DUPSTR(a) (((a) != NULL) && ((a)[0] != 0)) ? ((a)) : NULL
127
128 static char* opt_datum = nullptr;
129 static char* opt_dist = nullptr;
130 static char* opt_temp = nullptr;
131 static char* opt_date_format = nullptr;
132 static char* opt_time_format = nullptr;
133 static char* opt_precision = nullptr;
134 static char* opt_utc = nullptr;
135 static char* opt_grid = nullptr;
136
137 static
138 QVector<arglist_t> garmin_txt_args = {
139 {"date", &opt_date_format, "Read/Write date format (i.e. yyyy/mm/dd)", nullptr, ARGTYPE_STRING, ARG_NOMINMAX, nullptr},
140 {"datum", &opt_datum, "GPS datum (def. WGS 84)", "WGS 84", ARGTYPE_STRING, ARG_NOMINMAX, nullptr},
141 {"dist", &opt_dist, "Distance unit [m=metric, s=statute]", "m", ARGTYPE_STRING, ARG_NOMINMAX, nullptr},
142 {"grid", &opt_grid, "Write position using this grid.", nullptr, ARGTYPE_STRING, ARG_NOMINMAX, nullptr},
143 {"prec", &opt_precision, "Precision of coordinates", "3", ARGTYPE_INT, ARG_NOMINMAX, nullptr},
144 {"temp", &opt_temp, "Temperature unit [c=Celsius, f=Fahrenheit]", "c", ARGTYPE_STRING, ARG_NOMINMAX, nullptr},
145 {"time", &opt_time_format, "Read/Write time format (i.e. HH:mm:ss xx)", nullptr, ARGTYPE_STRING, ARG_NOMINMAX, nullptr},
146 {"utc", &opt_utc, "Write timestamps with offset x to UTC time", nullptr, ARGTYPE_INT, "-23", "+23", nullptr},
147 };
148
149 struct info_t {
150 double length;
151 time_t start;
152 time_t time;
153 double speed;
154 double total;
155 int count;
156 const Waypoint* prev_wpt;
157 const Waypoint* first_wpt;
158 const Waypoint* last_wpt;
159 };
160
161 static info_t* route_info;
162 static int route_idx;
163 static info_t* cur_info;
164
165 static const char* headers[] = {
166 "Name\tDescription\tType\tPosition\tAltitude\tDepth\tProximity\tTemperature\t"
167 "Display Mode\tColor\tSymbol\tFacility\tCity\tState\tCountry\t"
168 "Date Modified\tLink\tCategories",
169 "Waypoint Name\tDistance\tLeg Length\tCourse",
170 "Position\tTime\tAltitude\tDepth\tTemperature\tLeg Length\tLeg Time\tLeg Speed\tLeg Course",
171 "Name\tLength\tCourse\tWaypoints\tLink",
172 "Name\tStart Time\tElapsed Time\tLength\tAverage Speed\tLink",
173 nullptr
174 };
175
176 /* helpers */
177
178 static const char*
get_option_val(const char * option,const char * def)179 get_option_val(const char* option, const char* def)
180 {
181 const char* c = (option != nullptr) ? option : def;
182 return c;
183 }
184
185 static void
init_date_and_time_format()186 init_date_and_time_format()
187 {
188 const char* f = get_option_val(opt_date_format, DEFAULT_DATE_FORMAT);
189 date_time_format = convert_human_date_format(f);
190
191 date_time_format = xstrappend(date_time_format, " ");
192
193 f = get_option_val(opt_time_format, DEFAULT_TIME_FORMAT);
194 const char* c = convert_human_time_format(f);
195 date_time_format = xstrappend(date_time_format, c);
196 xfree((void*) c);
197 }
198
199 static void
convert_datum(const Waypoint * wpt,double * dest_lat,double * dest_lon)200 convert_datum(const Waypoint* wpt, double* dest_lat, double* dest_lon)
201 {
202 double alt;
203
204 if (datum_index == DATUM_WGS84) {
205 *dest_lat = wpt->latitude;
206 *dest_lon = wpt->longitude;
207 } else GPS_Math_WGS84_To_Known_Datum_M(wpt->latitude, wpt->longitude, 0.0,
208 dest_lat, dest_lon, &alt, datum_index);
209 }
210
211 /* WRITER *****************************************************************/
212
213 /* Waypoint preparation */
214
215 static void
enum_waypt_cb(const Waypoint * wpt)216 enum_waypt_cb(const Waypoint* wpt)
217 {
218 garmin_fs_t* gmsd = garmin_fs_t::find(wpt);
219 int wpt_class = garmin_fs_t::get_wpt_class(gmsd, 0);
220 if (wpt_class < 0x80) {
221 if (gtxt_flags.enum_waypoints) { /* enumerate only */
222 waypoints++;
223 return;
224 }
225 for (int i = 0; i < wpt_a_ct; i++) { /* check for duplicates */
226 const Waypoint* tmp = wpt_a[i];
227 if (case_ignore_strcmp(tmp->shortname, wpt->shortname) == 0) {
228 wpt_a[i] = wpt;
229 waypoints--;
230 return;
231
232 }
233 }
234 wpt_a[wpt_a_ct++] = wpt;
235 }
236
237 }
238
239 static int
sort_waypt_cb(const void * a,const void * b)240 sort_waypt_cb(const void* a, const void* b)
241 {
242 const Waypoint* wa = *(Waypoint**)a;
243 const Waypoint* wb = *(Waypoint**)b;
244 return wa->shortname.compare(wb->shortname, Qt::CaseInsensitive);
245 }
246
247
248 /* common route and track pre-work */
249
250 static void
prework_hdr_cb(const route_head *)251 prework_hdr_cb(const route_head*)
252 {
253 cur_info = &route_info[route_idx];
254 cur_info->prev_wpt = nullptr;
255 cur_info->length = 0;
256 cur_info->time = 0;
257 }
258
259 static void
prework_tlr_cb(const route_head *)260 prework_tlr_cb(const route_head*)
261 {
262 cur_info->last_wpt = cur_info->prev_wpt;
263 route_idx++;
264 }
265
266 static void
prework_wpt_cb(const Waypoint * wpt)267 prework_wpt_cb(const Waypoint* wpt)
268 {
269 const Waypoint* prev = cur_info->prev_wpt;
270
271 if (prev != nullptr) {
272 cur_info->time += (wpt->GetCreationTime().toTime_t() - prev->GetCreationTime().toTime_t());
273 cur_info->length += waypt_distance_ex(prev, wpt);
274 } else {
275 cur_info->first_wpt = wpt;
276 cur_info->start = wpt->GetCreationTime().toTime_t();
277 }
278 cur_info->prev_wpt = wpt;
279 cur_info->count++;
280 routepoints++;
281 }
282
283
284 /* output helpers */
285
286 static void
print_position(const Waypoint * wpt)287 print_position(const Waypoint* wpt)
288 {
289 int valid = 1;
290 double lat, lon, north, east;
291 int zone;
292 char map[3], zonec;
293
294 convert_datum(wpt, &lat, &lon);
295
296 /* ----------------------------------------------------------------------------*/
297 /* the following code is from pretty_deg_format (util.c) */
298 /* ----------------------------------------------------------------------------*/
299 /* !ToDo! generate common code for calculating of degrees, minutes and seconds */
300 /* ----------------------------------------------------------------------------*/
301
302 char latsig = lat < 0 ? 'S':'N';
303 char lonsig = lon < 0 ? 'W':'E';
304 int latint = abs((int) lat);
305 int lonint = abs((int) lon);
306 double latmin = 60.0 * (fabs(lat) - latint);
307 double lonmin = 60.0 * (fabs(lon) - lonint);
308 double latsec = 60.0 * (latmin - floor(latmin));
309 double lonsec = 60.0 * (lonmin - floor(lonmin));
310
311 switch (grid_index) {
312
313 case grid_lat_lon_ddd:
314
315 *fout << QString::asprintf("%c%0.*f %c%0.*f\t",
316 latsig, precision, fabs(lat),
317 lonsig, precision, fabs(lon));
318 break;
319
320 case grid_lat_lon_dmm:
321
322 *fout << QString::asprintf("%c%d %0*.*f %c%d %0*.*f\t",
323 latsig, latint, precision + 3, precision, latmin,
324 lonsig, lonint, precision + 3, precision, lonmin);
325 break;
326
327 case grid_lat_lon_dms:
328
329 *fout << QString::asprintf("%c%d %d %.*f %c%d %d %.*f\t",
330 latsig, latint, (int)latmin, precision, latsec,
331 lonsig, lonint, (int)lonmin, precision, lonsec);
332 break;
333
334 case grid_bng:
335
336 valid = GPS_Math_WGS84_To_UKOSMap_M(wpt->latitude, wpt->longitude, &east, &north, map);
337 if (valid) {
338 *fout << QString::asprintf("%s %5.0f %5.0f\t", map, east, north);
339 }
340 break;
341
342 case grid_utm:
343
344 valid = GPS_Math_Known_Datum_To_UTM_EN(lat, lon,
345 &east, &north, &zone, &zonec, datum_index);
346 if (valid) {
347 *fout << QString::asprintf("%02d %c %.0f %.0f\t", zone, zonec, east, north);
348 }
349 break;
350
351 case grid_swiss:
352
353 valid = GPS_Math_WGS84_To_Swiss_EN(wpt->latitude, wpt->longitude, &east, &north);
354 if (valid) {
355 *fout << QString::asprintf("%.f %.f\t", east, north);
356 }
357 break;
358
359 default:
360 fatal("ToDo\n");
361 }
362
363 if (! valid) {
364 *fout << "#####\n";
365 fatal(MYNAME ": %s (%s) is outside of convertible area \"%s\"!\n",
366 wpt->shortname.isEmpty() ? "Waypoint" : qPrintable(wpt->shortname),
367 pretty_deg_format(wpt->latitude, wpt->longitude, 'd', nullptr, 0),
368 gt_get_mps_grid_longname(grid_index, MYNAME));
369 }
370 }
371
372 static void
print_date_and_time(const time_t time,const int time_only)373 print_date_and_time(const time_t time, const int time_only)
374 {
375 struct tm tm;
376 char tbuf[32];
377
378 if (time < 0) {
379 *fout << "\t";
380 return;
381 }
382 if (time_only) {
383 tm = *gmtime(&time);
384 snprintf(tbuf, sizeof(tbuf), "%d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec);
385 *fout << QString::asprintf("%s", tbuf);
386 } else if (time != 0) {
387 if (gtxt_flags.utc) {
388 time_t t = time + utc_offs;
389 tm = *gmtime(&t);
390 } else {
391 tm = *localtime(&time);
392 }
393 strftime(tbuf, sizeof(tbuf), date_time_format, &tm);
394 *fout << QString::asprintf("%s ", tbuf);
395 }
396 *fout << "\t";
397 }
398
399 static void
print_categories(uint16_t categories)400 print_categories(uint16_t categories)
401 {
402 if (categories == 0) {
403 return;
404 }
405
406 int count = 0;
407 for (int i = 0; i < 16; i++) {
408 if ((categories & 1) != 0) {
409 QString c;
410 if (global_opts.inifile != nullptr) {
411 QString key = QString::number(i + 1);
412 c = inifile_readstr(global_opts.inifile, GMSD_SECTION_CATEGORIES, key);
413 }
414
415 *fout << QString::asprintf("%s", (count++ > 0) ? "," : "");
416 if (c.isNull()) {
417 *fout << QString::asprintf("Category %d", i+1);
418 }
419 // *fout << QString::asprintf("%s", gps_categories[i]);
420 else {
421 *fout << c;
422 }
423
424 }
425 categories = categories >> 1;
426 }
427 }
428
429 static void
print_course(const Waypoint * A,const Waypoint * B)430 print_course(const Waypoint* A, const Waypoint* B) /* seems to be okay */
431 {
432 if ((A != nullptr) && (B != nullptr) && (A != B)) {
433 int course = si_round(waypt_course(A, B));
434 *fout << QString::asprintf("%d° true", course);
435 }
436 }
437
438 static void
print_distance(const double distance,const int no_scale,const int with_tab,const int decis)439 print_distance(const double distance, const int no_scale, const int with_tab, const int decis)
440 {
441 double dist = distance;
442
443 if (gtxt_flags.metric == 0) {
444 dist = METERS_TO_FEET(dist);
445
446 if ((dist < 5280) || no_scale) {
447 *fout << QString::asprintf("%.*f ft", decis, dist);
448 } else {
449 dist = METERS_TO_MILES(distance);
450 if (dist < 100.0) {
451 *fout << QString::asprintf("%.1f mi", dist);
452 } else {
453 *fout << QString::asprintf("%d mi", si_round(dist));
454 }
455 }
456 } else {
457 if ((dist < 1000) || no_scale) {
458 *fout << QString::asprintf("%.*f m", decis, dist);
459 } else {
460 dist = dist / 1000.0;
461 if (dist < 100.0) {
462 *fout << QString::asprintf("%.1f km", dist);
463 } else {
464 *fout << QString::asprintf("%d km", si_round(dist));
465 }
466 }
467 }
468 if (with_tab) {
469 *fout << "\t";
470 }
471 }
472
473 static void
print_speed(const double * distance,const time_t * time)474 print_speed(const double* distance, const time_t* time)
475 {
476 double dist = *distance;
477 const char* unit;
478
479 if (!gtxt_flags.metric) {
480 dist = METERS_TO_MILES(dist) * 1000.0;
481 unit = "mph";
482 } else {
483 unit = "kph";
484 }
485 int idist = si_round(dist);
486
487 if ((*time != 0) && (idist > 0)) {
488 double speed = MPS_TO_KPH(dist / (double)*time);
489 int ispeed = si_round(speed);
490
491 if (speed < 0.01) {
492 *fout << QString::asprintf("0 %s", unit);
493 } else if (ispeed < 2) {
494 *fout << QString::asprintf("%.1f %s", speed, unit);
495 } else {
496 *fout << QString::asprintf("%d %s", ispeed, unit);
497 }
498 } else {
499 *fout << QString::asprintf("0 %s", unit);
500 }
501 *fout << "\t";
502 }
503
504 static void
print_temperature(const float temperature)505 print_temperature(const float temperature)
506 {
507 if (gtxt_flags.celsius) {
508 *fout << QString::asprintf("%.f C", temperature);
509 } else {
510 *fout << QString::asprintf("%.f F", (temperature * 1.8) + 32);
511 }
512 }
513
514 static void
print_string(const char * fmt,const QString & string)515 print_string(const char* fmt, const QString& string)
516 {
517 /* remove unwanted characters from source string */
518 QString cleanstring;
519 for (const auto chr : string) {
520 if (chr.category() != QChar::Other_Control) {
521 cleanstring.append(chr);
522 } else {
523 cleanstring.append(' ');
524 }
525 }
526 *fout << QString::asprintf(fmt, CSTR(cleanstring));
527 }
528
529
530 /* main cb's */
531
532 static void
write_waypt(const Waypoint * wpt)533 write_waypt(const Waypoint* wpt)
534 {
535 const char* wpt_type;
536
537 garmin_fs_t* gmsd = garmin_fs_t::find(wpt);
538
539 int i = garmin_fs_t::get_display(gmsd, 0);
540 if (i > GT_DISPLAY_MODE_MAX) {
541 i = 0;
542 }
543 const char* dspl_mode = gt_display_mode_names[i];
544
545 unsigned char wpt_class = garmin_fs_t::get_wpt_class(gmsd, 0);
546 if (wpt_class <= gt_waypt_class_map_line) {
547 wpt_type = gt_waypt_class_names[wpt_class];
548 } else {
549 wpt_type = gt_waypt_class_names[0];
550 }
551
552 *fout << "Waypoint\t" << wpt->shortname << "\t";
553 if (wpt_class <= gt_waypt_class_airport_ndb) {
554 QString temp = wpt->notes;
555 if (temp.isEmpty()) {
556 if (wpt->description != wpt->shortname) {
557 temp = wpt->description;
558 } else {
559 temp = "";
560 }
561 }
562 print_string("%s\t", temp);
563 } else {
564 *fout << "\t";
565 }
566 *fout << QString::asprintf("%s\t", wpt_type);
567
568 print_position(wpt);
569
570 if IS_VALID_ALT(wpt->altitude) {
571 print_distance(wpt->altitude, 1, 0, 0);
572 }
573 *fout << "\t";
574
575 double x = WAYPT_GET(wpt, depth, unknown_alt);
576 if (x != unknown_alt) {
577 print_distance(x, 1, 0, 1);
578 }
579 *fout << "\t";
580
581 x = WAYPT_GET(wpt, proximity, unknown_alt);
582 if (x != unknown_alt) {
583 print_distance(x, 0, 0, 0);
584 }
585 *fout << "\t";
586
587 x = WAYPT_GET(wpt, temperature, -999);
588 if (x != -999) {
589 print_temperature(x);
590 }
591 *fout << QString::asprintf("\t%s\t", dspl_mode);
592
593 *fout << "Unknown\t"; /* Color is fixed: Unknown */
594
595 int icon = garmin_fs_t::get_icon(gmsd, -1);
596 if (icon == -1) {
597 icon = gt_find_icon_number_from_desc(wpt->icon_descr, GDB);
598 }
599 print_string("%s\t", gt_find_desc_from_icon_number(icon, GDB));
600
601 print_string("%s\t", garmin_fs_t::get_facility(gmsd, ""));
602 print_string("%s\t", garmin_fs_t::get_city(gmsd, ""));
603 print_string("%s\t", garmin_fs_t::get_state(gmsd, ""));
604 const char* country = gt_get_icao_country(garmin_fs_t::get_cc(gmsd, ""));
605 print_string("%s\t", (country != nullptr) ? country : "");
606 print_date_and_time(wpt->GetCreationTime().toTime_t(), 0);
607 if (wpt->HasUrlLink()) {
608 UrlLink l = wpt->GetUrlLink();
609 print_string("%s\t", l.url_);
610 } else {
611 print_string("%s\t", "");
612 }
613 print_categories(garmin_fs_t::get_category(gmsd, 0));
614
615 *fout << "\r\n";
616 }
617
618 static void
route_disp_hdr_cb(const route_head * rte)619 route_disp_hdr_cb(const route_head* rte)
620 {
621 cur_info = &route_info[route_idx];
622 cur_info->prev_wpt = nullptr;
623 cur_info->total = 0;
624 if (rte->rte_waypt_ct <= 0) {
625 return;
626 }
627
628 if (!gtxt_flags.route_header_written) {
629 gtxt_flags.route_header_written = 1;
630 *fout << QString::asprintf("\r\n\r\nHeader\t%s\r\n", headers[route_header]);
631 }
632 print_string("\r\nRoute\t%s\t", rte->rte_name);
633 print_distance(cur_info->length, 0, 1, 0);
634 print_course(cur_info->first_wpt, cur_info->last_wpt);
635 *fout << QString::asprintf("\t%d waypoints\t", cur_info->count);
636 if (rte->rte_urls.HasUrlLink()) {
637 print_string("%s\r\n", rte->rte_urls.GetUrlLink().url_);
638 } else {
639 print_string("%s\r\n", "");
640 }
641 *fout << QString::asprintf("\r\nHeader\t%s\r\n\r\n", headers[rtept_header]);
642 }
643
644 static void
route_disp_tlr_cb(const route_head *)645 route_disp_tlr_cb(const route_head*)
646 {
647 route_idx++;
648 }
649
650 static void
route_disp_wpt_cb(const Waypoint * wpt)651 route_disp_wpt_cb(const Waypoint* wpt)
652 {
653 const Waypoint* prev = cur_info->prev_wpt;
654
655 *fout << "Route Waypoint\t" << wpt->shortname << "\t";
656
657 if (prev != nullptr) {
658 double dist = waypt_distance_ex(prev, wpt);
659 cur_info->total += dist;
660 print_distance(cur_info->total, 0, 1, 0);
661 print_distance(dist, 0, 1, 0);
662 print_course(prev, wpt);
663 } else {
664 print_distance(0, 1, 0, 0);
665 }
666
667 *fout << "\r\n";
668
669 cur_info->prev_wpt = wpt;
670 }
671
672 static void
track_disp_hdr_cb(const route_head * track)673 track_disp_hdr_cb(const route_head* track)
674 {
675 cur_info = &route_info[route_idx];
676 cur_info->prev_wpt = nullptr;
677 cur_info->total = 0;
678 if (track->rte_waypt_ct <= 0) {
679 return;
680 }
681
682 if (!gtxt_flags.track_header_written) {
683 gtxt_flags.track_header_written = 1;
684 *fout << QString::asprintf("\r\n\r\nHeader\t%s\r\n", headers[track_header]);
685 }
686 print_string("\r\nTrack\t%s\t", track->rte_name);
687 print_date_and_time(cur_info->start, 0);
688 print_date_and_time(cur_info->time, 1);
689 print_distance(cur_info->length, 0, 1, 0);
690 print_speed(&cur_info->length, &cur_info->time);
691 if (track->rte_urls.HasUrlLink()) {
692 print_string("%s", track->rte_urls.GetUrlLink().url_);
693 } else {
694 print_string("%s", "");
695 }
696 *fout << QString::asprintf("\r\n\r\nHeader\t%s\r\n\r\n", headers[trkpt_header]);
697 }
698
699 static void
track_disp_tlr_cb(const route_head *)700 track_disp_tlr_cb(const route_head*)
701 {
702 route_idx++;
703 }
704
705 static void
track_disp_wpt_cb(const Waypoint * wpt)706 track_disp_wpt_cb(const Waypoint* wpt)
707 {
708 const Waypoint* prev = cur_info->prev_wpt;
709 time_t delta;
710 double dist;
711
712 *fout << "Trackpoint\t";
713
714 print_position(wpt);
715 print_date_and_time(wpt->GetCreationTime().toTime_t(), 0);
716 if IS_VALID_ALT(wpt->altitude) {
717 print_distance(wpt->altitude, 1, 0, 0);
718 }
719
720 *fout << "\t";
721 double depth = WAYPT_GET(wpt, depth, unknown_alt);
722 if (depth != unknown_alt) {
723 print_distance(depth, 1, 0, 1);
724 }
725
726 if (prev != nullptr) {
727 *fout << "\t";
728 delta = wpt->GetCreationTime().toTime_t() - prev->GetCreationTime().toTime_t();
729 float temp = WAYPT_GET(wpt, temperature, -999);
730 if (temp != -999) {
731 print_temperature(temp);
732 }
733 *fout << "\t";
734 dist = waypt_distance_ex(prev, wpt);
735 print_distance(dist, 0, 1, 0);
736 print_date_and_time(delta, 1);
737 print_speed(&dist, &delta);
738 print_course(prev, wpt);
739 }
740 *fout << "\r\n";
741
742 cur_info->prev_wpt = wpt;
743 }
744
745 /*******************************************************************************
746 * %%% global callbacks called by gpsbabel main process %%% *
747 *******************************************************************************/
748
749 static void
garmin_txt_wr_init(const QString & fname)750 garmin_txt_wr_init(const QString& fname)
751 {
752 memset(>xt_flags, 0, sizeof(gtxt_flags));
753
754 fout = new gpsbabel::TextStream;
755 fout->open(fname, QIODevice::WriteOnly, MYNAME, "Windows-1252");
756
757 gtxt_flags.metric = (toupper(*get_option_val(opt_dist, "m")) == 'M');
758 gtxt_flags.celsius = (toupper(*get_option_val(opt_temp, "c")) == 'C');
759 init_date_and_time_format();
760 if (opt_precision) {
761 precision = atoi(opt_precision);
762 is_fatal(precision < 0, MYNAME ": Invalid precision (%s)!", opt_precision);
763 }
764
765 datum_str = get_option_val(opt_datum, nullptr);
766 const char* grid_str = get_option_val(opt_grid, nullptr);
767
768 grid_index = grid_lat_lon_dmm;
769 if (grid_str != nullptr) {
770 int i;
771
772 if (sscanf(grid_str, "%d", &i)) {
773 grid_index = (grid_type) i;
774 if ((grid_index < GRID_INDEX_MIN) || (grid_index > GRID_INDEX_MAX))
775 fatal(MYNAME ": Grid index out of range (%d..%d)!",
776 (int)GRID_INDEX_MIN, (int)GRID_INDEX_MAX);
777 } else {
778 grid_index = gt_lookup_grid_type(grid_str, MYNAME);
779 }
780 }
781
782 switch (grid_index) {
783 case grid_bng: /* force datum to "Ord Srvy Grt Britn" */
784 datum_index = DATUM_OSGB36;
785 break;
786 case grid_swiss: /* force datum to WGS84 */
787 datum_index = DATUM_WGS84;
788 break;
789 default:
790 datum_index = gt_lookup_datum_index(datum_str, MYNAME);
791 }
792
793 if (opt_utc != nullptr) {
794 if (case_ignore_strcmp(opt_utc, "utc") == 0) {
795 utc_offs = 0;
796 } else {
797 utc_offs = atoi(opt_utc);
798 }
799 utc_offs *= (60 * 60);
800 gtxt_flags.utc = 1;
801 }
802 }
803
804 static void
garmin_txt_wr_deinit()805 garmin_txt_wr_deinit()
806 {
807 fout->close();
808 delete fout;
809 fout = nullptr;
810 xfree(date_time_format);
811 }
812
813 static void
garmin_txt_write()814 garmin_txt_write()
815 {
816 QString grid_str = gt_get_mps_grid_longname(grid_index, MYNAME);
817 grid_str = grid_str.replace('*', "°");
818 *fout << "Grid\t" << grid_str << "\r\n";
819
820 datum_str = gt_get_mps_datum_name(datum_index);
821 *fout << QString::asprintf("Datum\t%s\r\n\r\n", datum_str);
822
823 waypoints = 0;
824 gtxt_flags.enum_waypoints = 1; /* enum all waypoints */
825 waypt_disp_all(enum_waypt_cb);
826 route_disp_all(nullptr, nullptr, enum_waypt_cb);
827 gtxt_flags.enum_waypoints = 0;
828
829 if (waypoints > 0) {
830 wpt_a_ct = 0;
831 wpt_a = (const Waypoint**)xcalloc(waypoints, sizeof(*wpt_a));
832 waypt_disp_all(enum_waypt_cb);
833 route_disp_all(nullptr, nullptr, enum_waypt_cb);
834 qsort(wpt_a, waypoints, sizeof(*wpt_a), sort_waypt_cb);
835
836 *fout << QString::asprintf("Header\t%s\r\n\r\n", headers[waypt_header]);
837 for (int i = 0; i < waypoints; i++) {
838 const Waypoint* wpt = wpt_a[i];
839 write_waypt(wpt);
840 }
841 xfree(wpt_a);
842
843 route_idx = 0;
844 route_info = (info_t*) xcalloc(route_count(), sizeof(info_t));
845 routepoints = 0;
846 route_disp_all(prework_hdr_cb, prework_tlr_cb, prework_wpt_cb);
847 if (routepoints > 0) {
848 route_idx = 0;
849 route_disp_all(route_disp_hdr_cb, route_disp_tlr_cb, route_disp_wpt_cb);
850 }
851 xfree(route_info);
852 }
853
854 route_idx = 0;
855 route_info = (info_t*) xcalloc(track_count(), sizeof(info_t));
856 routepoints = 0;
857 track_disp_all(prework_hdr_cb, prework_tlr_cb, prework_wpt_cb);
858
859 if (routepoints > 0) {
860 route_idx = 0;
861 track_disp_all(track_disp_hdr_cb, track_disp_tlr_cb, track_disp_wpt_cb);
862 }
863 xfree(route_info);
864 }
865
866 /* READER *****************************************************************/
867
868 /* helpers */
869
870 static void
free_header(const header_type ht)871 free_header(const header_type ht)
872 {
873 for (int i = 0; i < MAX_HEADER_FIELDS; i++) {
874 char* c = header_lines[ht][i];
875 if (c != nullptr) {
876 xfree(c);
877 header_lines[ht][i] = nullptr;
878 }
879 }
880 header_ct[ht] = 0;
881 memset(header_fields[ht], 0, sizeof(header_fields[ht]));
882 }
883
884 /* data parsers */
885
886 static int
parse_date_and_time(char * str,time_t * value)887 parse_date_and_time(char* str, time_t* value)
888 {
889 struct tm tm;
890
891 memset(&tm, 0, sizeof(tm));
892 char* cin = lrtrim(str);
893 if (*cin == '\0') {
894 return 0;
895 }
896
897 char* cerr = strptime(cin, date_time_format, &tm);
898 if (cerr == nullptr) {
899 cerr = strptime(cin, "%m/%d/%Y %I:%M:%S %p", &tm);
900 is_fatal(cerr == nullptr, MYNAME ": Invalid date or/and time \"%s\" at line %d!", cin, current_line);
901 }
902
903 // printf(MYNAME "_parse_date_and_time: %02d.%02d.%04d, %02d:%02d:%02d\n",
904 // tm.tm_mday, tm.tm_mon+1, tm.tm_year+1900, tm.tm_hour, tm.tm_min, tm.tm_sec);
905
906 *value = mklocaltime(&tm);
907 return 1;
908 }
909
910 static uint16_t
parse_categories(const char * str)911 parse_categories(const char* str)
912 {
913 char buff[256];
914 uint16_t val;
915 uint16_t res = 0;
916 char* cx;
917
918 if (*str == '\0') {
919 return 0;
920 }
921
922 strncpy(buff, str, sizeof(buff));
923 char* cin = lrtrim(buff);
924 if (*cin == '\0') {
925 return 0;
926 }
927
928 strcat(cin, ",");
929
930 while ((cx = strchr(cin, ','))) {
931 *cx++ = '\0';
932 cin = lrtrim(cin);
933 if (*cin != '\0') {
934 if (!garmin_fs_convert_category(cin, &val)) {
935 warning(MYNAME ": Unable to convert category \"%s\" at line %d!\n", cin, current_line);
936 } else {
937 res = res | val;
938 }
939 }
940 cin = cx;
941 }
942 return res;
943 }
944
945 static int
parse_temperature(const char * str,double * temperature)946 parse_temperature(const char* str, double* temperature)
947 {
948 double value;
949 unsigned char unit;
950
951 if ((str == nullptr) || (*str == '\0')) {
952 return 0;
953 }
954
955 if (sscanf(str, "%lf %c", &value, &unit) == 2) {
956 unit = toupper(unit);
957 switch (unit) {
958 case 'C':
959 *temperature = value;
960 break;
961 case 'F':
962 *temperature = FAHRENHEIT_TO_CELSIUS(value);
963 break;
964 default:
965 fatal(MYNAME ": Unknown temperature unit \"%c\" at line %d!\n", unit, current_line);
966 }
967 return 1;
968 } else {
969 fatal(MYNAME ": Invalid temperature \"%s\" at line %d!\n", str, current_line);
970 }
971 return 0;
972 }
973
974 static void
parse_header()975 parse_header()
976 {
977 char* str;
978 int column = -1;
979
980 free_header(unknown_header);
981
982 while ((str = csv_lineparse(nullptr, "\t", "", column++))) {
983 header_lines[unknown_header][column] = strupper(xstrdup(str));
984 header_ct[unknown_header]++;
985 if (header_ct[unknown_header] >= MAX_HEADER_FIELDS) {
986 break;
987 }
988 }
989 }
990
991 static int
parse_display(const char * str,int * val)992 parse_display(const char* str, int* val)
993 {
994 if ((str == nullptr) || (*str == '\0')) {
995 return 0;
996 }
997
998 for (gt_display_modes_e i = GT_DISPLAY_MODE_MIN; i <= GT_DISPLAY_MODE_MAX; ++i) {
999 if (case_ignore_strcmp(str, gt_display_mode_names[i]) == 0) {
1000 *val = i;
1001 return 1;
1002 }
1003 }
1004 warning(MYNAME ": Unknown display mode \"%s\" at line %d.\n", str, current_line);
1005 return 0;
1006 }
1007
1008 static void
bind_fields(const header_type ht)1009 bind_fields(const header_type ht)
1010 {
1011 is_fatal((grid_index < 0) || (datum_index < 0), MYNAME ": Incomplete or invalid file header!");
1012
1013 if (header_ct[unknown_header] <= 0) {
1014 return;
1015 }
1016 free_header(ht);
1017
1018 /* make a copy of headers[ht], uppercase, replace "\t" with "\0" */
1019
1020 int i = strlen(headers[ht]);
1021 char* fields = (char*) xmalloc(i + 2);
1022 strcpy(fields, headers[ht]);
1023 strcat(fields, "\t");
1024 char* c = strupper(fields);
1025 while ((c = strchr(c, '\t'))) {
1026 *c++ = '\0';
1027 }
1028
1029 for (i = 0; i < header_ct[unknown_header]; i++) {
1030 char* name = header_lines[ht][i] = header_lines[unknown_header][i];
1031 header_lines[unknown_header][i] = nullptr;
1032
1033 c = fields;
1034 int field_no = 1;
1035 while (*c) {
1036 if (strcmp(c, name) == 0) {
1037 header_fields[ht][i] = field_no;
1038 #if 0
1039 printf("Binding field \"%s\" to internal number %d (%d,%d)\n", name, field_no, ht, i);
1040 #endif
1041 break;
1042 }
1043 field_no++;
1044 c = c + strlen(c) + 1;
1045 }
1046 }
1047 header_ct[unknown_header] = 0;
1048 xfree(fields);
1049 }
1050
1051 static void
parse_grid()1052 parse_grid()
1053 {
1054 char* str = csv_lineparse(nullptr, "\t", "", 1);
1055
1056 if (str != nullptr) {
1057 if (strstr(str, "dd.ddddd") != nullptr) {
1058 grid_index = grid_lat_lon_ddd;
1059 } else if (strstr(str, "mm.mmm") != nullptr) {
1060 grid_index = grid_lat_lon_dmm;
1061 } else if (strstr(str, "mm'ss.s") != nullptr) {
1062 grid_index = grid_lat_lon_dms;
1063 } else {
1064 grid_index = gt_lookup_grid_type(str, MYNAME);
1065 }
1066 } else {
1067 fatal(MYNAME ": Missing grid headline!\n");
1068 }
1069 }
1070
1071 static void
parse_datum()1072 parse_datum()
1073 {
1074 char* str = csv_lineparse(nullptr, "\t", "", 1);
1075
1076 if (str != nullptr) {
1077 datum_index = gt_lookup_datum_index(str, MYNAME);
1078 } else {
1079 fatal(MYNAME ": Missing GPS datum headline!\n");
1080 }
1081 }
1082
1083 static void
parse_waypoint()1084 parse_waypoint()
1085 {
1086 char* str;
1087 int column = -1;
1088
1089 bind_fields(waypt_header);
1090
1091 auto* wpt = new Waypoint;
1092 garmin_fs_t* gmsd = garmin_fs_alloc(-1);
1093 wpt->fs.FsChainAdd(gmsd);
1094
1095 while ((str = csv_lineparse(nullptr, "\t", "", column++))) {
1096 int i;
1097 double d;
1098 int field_no = header_fields[waypt_header][column];
1099
1100 switch (field_no) {
1101 case 1:
1102 wpt->shortname = DUPSTR(str);
1103 break;
1104 case 2:
1105 wpt->notes = DUPSTR(str);
1106 break;
1107 case 3:
1108 for (i = 0; i <= gt_waypt_class_map_line; i++) {
1109 if (case_ignore_strcmp(str, gt_waypt_class_names[i]) == 0) {
1110 garmin_fs_t::set_wpt_class(gmsd, i);
1111 break;
1112 }
1113 }
1114 break;
1115 case 4:
1116 parse_coordinates(str, datum_index, grid_index,
1117 &wpt->latitude, &wpt->longitude, MYNAME);
1118 break;
1119 case 5:
1120 if (parse_distance(str, &d, 1, MYNAME)) {
1121 wpt->altitude = d;
1122 }
1123 break;
1124 case 6:
1125 if (parse_distance(str, &d, 1, MYNAME)) {
1126 WAYPT_SET(wpt, depth, d);
1127 }
1128 break;
1129 case 7:
1130 if (parse_distance(str, &d, 1, MYNAME)) {
1131 WAYPT_SET(wpt, proximity, d);
1132 }
1133 break;
1134 case 8:
1135 if (parse_temperature(str, &d)) {
1136 WAYPT_SET(wpt, temperature, d);
1137 }
1138 break;
1139 case 9:
1140 if (parse_display(str, &i)) {
1141 garmin_fs_t::set_display(gmsd, i);
1142 }
1143 break;
1144 case 10:
1145 break; /* skip color */
1146 case 11:
1147 i = gt_find_icon_number_from_desc(str, GDB);
1148 garmin_fs_t::set_icon(gmsd, i);
1149 wpt->icon_descr = gt_find_desc_from_icon_number(i, GDB);
1150 break;
1151 case 12:
1152 garmin_fs_t::set_facility(gmsd, str);
1153 break;
1154 case 13:
1155 garmin_fs_t::set_city(gmsd, str);
1156 break;
1157 case 14:
1158 garmin_fs_t::set_state(gmsd, str);
1159 break;
1160 case 15:
1161 garmin_fs_t::set_country(gmsd, str);
1162 garmin_fs_t::set_cc(gmsd, gt_get_icao_cc(str, wpt->shortname));
1163 break;
1164 case 16: {
1165 time_t ct;
1166 if (parse_date_and_time(str, &ct)) {
1167 wpt->SetCreationTime(ct);
1168 }
1169 }
1170 break;
1171 case 17: {
1172 wpt->AddUrlLink(str);
1173 }
1174 break;
1175 case 18:
1176 garmin_fs_t::set_category(gmsd, parse_categories(str));
1177 break;
1178 default:
1179 break;
1180 }
1181 }
1182 waypt_add(wpt);
1183 }
1184
1185 static void
parse_route_header()1186 parse_route_header()
1187 {
1188 char* str;
1189 int column = -1;
1190
1191 auto* rte = new route_head;
1192
1193 bind_fields(route_header);
1194 while ((str = csv_lineparse(nullptr, "\t", "", column++))) {
1195 int field_no = header_fields[route_header][column];
1196 switch (field_no) {
1197 case 1:
1198 rte->rte_name = DUPSTR(str);
1199 break;
1200 case 5:
1201 rte->rte_urls.AddUrlLink(UrlLink(str));
1202 break;
1203 }
1204 }
1205 route_add_head(rte);
1206 current_rte = rte;
1207 }
1208
1209 static void
parse_track_header()1210 parse_track_header()
1211 {
1212 char* str;
1213 int column = -1;
1214
1215 bind_fields(track_header);
1216 auto* trk = new route_head;
1217 while ((str = csv_lineparse(nullptr, "\t", "", column++))) {
1218 int field_no = header_fields[track_header][column];
1219 switch (field_no) {
1220 case 1:
1221 trk->rte_name = DUPSTR(str);
1222 break;
1223 case 6:
1224 trk->rte_urls.AddUrlLink(UrlLink(str));
1225 break;
1226 }
1227 }
1228 track_add_head(trk);
1229 current_trk = trk;
1230 }
1231
1232 static void
parse_route_waypoint()1233 parse_route_waypoint()
1234 {
1235 char* str;
1236 int column = -1;
1237 Waypoint* wpt = nullptr;
1238
1239 bind_fields(rtept_header);
1240
1241 while ((str = csv_lineparse(nullptr, "\t", "", column++))) {
1242 int field_no = header_fields[rtept_header][column];
1243 switch (field_no) {
1244 case 1:
1245 is_fatal((*str == '\0'), MYNAME ": Route waypoint without name at line %d!\n", current_line);
1246 wpt = find_waypt_by_name(str);
1247 is_fatal((wpt == nullptr), MYNAME ": Route waypoint \"%s\" not in waypoint list (line %d)!\n", str, current_line);
1248 wpt = new Waypoint(*wpt);
1249 break;
1250 }
1251 }
1252 if (wpt != nullptr) {
1253 route_add_wpt(current_rte, wpt);
1254 }
1255 }
1256
1257 static void
parse_track_waypoint()1258 parse_track_waypoint()
1259 {
1260 char* str;
1261 int column = -1;
1262
1263 bind_fields(trkpt_header);
1264 auto* wpt = new Waypoint;
1265
1266 while ((str = csv_lineparse(nullptr, "\t", "", column++))) {
1267 double x;
1268
1269 if (! *str) {
1270 continue;
1271 }
1272
1273 int field_no = header_fields[trkpt_header][column];
1274 switch (field_no) {
1275 case 1:
1276 parse_coordinates(str, datum_index, grid_index,
1277 &wpt->latitude, &wpt->longitude, MYNAME);
1278 break;
1279 case 2: {
1280 time_t ct;
1281 if (parse_date_and_time(str, &ct)) {
1282 wpt->SetCreationTime(ct);
1283 }
1284 }
1285 break;
1286 case 3:
1287 if (parse_distance(str, &x, 1, MYNAME)) {
1288 wpt->altitude = x;
1289 }
1290 break;
1291 case 4:
1292 if (parse_distance(str, &x, 1, MYNAME)) {
1293 WAYPT_SET(wpt, depth, x);
1294 }
1295 break;
1296 case 5:
1297 if (parse_temperature(str, &x)) {
1298 WAYPT_SET(wpt, temperature, x);
1299 }
1300 break;
1301 case 8:
1302 if (parse_speed(str, &x, 1, MYNAME)) {
1303 WAYPT_SET(wpt, speed, x);
1304 }
1305 break;
1306 case 9:
1307 WAYPT_SET(wpt, course, atoi(str));
1308 break;
1309 }
1310 }
1311 track_add_wpt(current_trk, wpt);
1312 }
1313
1314 /***************************************************************/
1315
1316 static void
garmin_txt_rd_init(const QString & fname)1317 garmin_txt_rd_init(const QString& fname)
1318 {
1319 memset(>xt_flags, 0, sizeof(gtxt_flags));
1320
1321 fin = new gpsbabel::TextStream;
1322 fin->open(fname, QIODevice::ReadOnly, MYNAME, "Windows-1252");
1323 memset(&header_ct, 0, sizeof(header_ct));
1324
1325 datum_index = -1;
1326 grid_index = (grid_type) -1;
1327
1328 init_date_and_time_format();
1329 }
1330
1331 static void
garmin_txt_rd_deinit()1332 garmin_txt_rd_deinit()
1333 {
1334 for (header_type h = waypt_header; h <= unknown_header; ++h) {
1335 free_header(h);
1336 }
1337 fin->close();
1338 delete fin;
1339 fin = nullptr;
1340 xfree(date_time_format);
1341 }
1342
1343 static void
garmin_txt_read()1344 garmin_txt_read()
1345 {
1346 QString buff;
1347
1348 current_line = 0;
1349 while ((buff = fin->readLine(), !buff.isNull())) {
1350 ++current_line;
1351 buff = buff.trimmed();
1352
1353 if (buff.isEmpty()) {
1354 continue;
1355 }
1356
1357 /* bail back to a (utf-8) char string, this format isn't worth the conversion work */
1358 QByteArray utf8str = buff.toUtf8();
1359 char* cin = csv_lineparse(utf8str.constData(), "\t", "", 0);
1360
1361 if (cin == nullptr) {
1362 continue;
1363 }
1364
1365 if (case_ignore_strcmp(cin, "Header") == 0) {
1366 parse_header();
1367 } else if (case_ignore_strcmp(cin, "Grid") == 0) {
1368 parse_grid();
1369 } else if (case_ignore_strcmp(cin, "Datum") == 0) {
1370 parse_datum();
1371 } else if (case_ignore_strcmp(cin, "Waypoint") == 0) {
1372 parse_waypoint();
1373 } else if (case_ignore_strcmp(cin, "Route Waypoint") == 0) {
1374 parse_route_waypoint();
1375 } else if (case_ignore_strcmp(cin, "Trackpoint") == 0) {
1376 parse_track_waypoint();
1377 } else if (case_ignore_strcmp(cin, "Route") == 0) {
1378 parse_route_header();
1379 } else if (case_ignore_strcmp(cin, "Track") == 0) {
1380 parse_track_header();
1381 } else if (case_ignore_strcmp(cin, "Map") == 0) /* do nothing */ ;
1382 else {
1383 fatal(MYNAME ": Unknown identifier (%s) at line %d!\n", cin, current_line);
1384 }
1385
1386 /* flush pending data */
1387 while (csv_lineparse(nullptr, "\t", "", 0));
1388 }
1389 }
1390
1391 ff_vecs_t garmin_txt_vecs = {
1392 ff_type_file,
1393 FF_CAP_RW_ALL,
1394 garmin_txt_rd_init,
1395 garmin_txt_wr_init,
1396 garmin_txt_rd_deinit,
1397 garmin_txt_wr_deinit,
1398 garmin_txt_read,
1399 garmin_txt_write,
1400 nullptr,
1401 &garmin_txt_args,
1402 /*
1403 * The file encoding is Windows-1252, a.k.a CET_CHARSET_MS_ANSI.
1404 * Conversion between Windows-1252 and utf-16 is handled by the stream.
1405 * Conversion between utf-16 and utf-8 is handled by this format.
1406 * Let main know char strings have already been converted to utf-8
1407 * so it doesn't attempt to re-convert any char strings including gmsd data.
1408 */
1409 CET_CHARSET_UTF8, 0
1410 , NULL_POS_OPS,
1411 nullptr
1412 };
1413
1414 #endif // CSVFMTS_ENABLED
1415