1 /*
2     Access to Garmin PCX5 files.
3     Format described in: http://www.garmin.com/manuals/PCX5_OwnersManual.pdf
4 
5     Copyright (C) 2002-2017 Robert Lipe, robertlipe+source@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 #include "defs.h"
23 #include "cet_util.h"
24 #include "csv_util.h"
25 #include "garmin_tables.h"
26 #include <QtCore/QDebug>
27 #include <cmath>
28 #include <cstdio>
29 #include <cstdlib>
30 
31 static gbfile* file_in, *file_out;
32 static short_handle mkshort_handle;
33 static short_handle mkshort_handle2; /* for track and route names */
34 static char* deficon = nullptr;
35 static char* cartoexploreur;
36 static int read_as_degrees;
37 static int read_gpsu;
38 static int route_ctr;
39 static int comment_col = 60; /* This has a default */
40 static int sym_col;
41 static int lat_col;
42 static int lon_col;
43 
44 #define MYNAME "PCX"
45 
46 static QVector<arglist_t> pcx_args = {{"deficon", &deficon, "Default icon name",
47                                 "Waypoint", ARGTYPE_STRING, ARG_NOMINMAX, nullptr},
48                                {"cartoexploreur", &cartoexploreur,
49                                 "Write tracks compatible with Carto Exploreur",
50                                 nullptr, ARGTYPE_BOOL, ARG_NOMINMAX, nullptr},
51                                };
52 
rd_init(const QString & fname)53 static void rd_init(const QString& fname) {
54   file_in = gbfopen(fname, "rb", MYNAME);
55 }
56 
rd_deinit()57 static void rd_deinit() { gbfclose(file_in); }
58 
wr_init(const QString & fname)59 static void wr_init(const QString& fname) {
60   file_out = gbfopen(fname, "w", MYNAME);
61   mkshort_handle = mkshort_new_handle();
62   mkshort_handle2 = mkshort_new_handle();
63 }
64 
wr_deinit()65 static void wr_deinit() {
66   gbfclose(file_out);
67   mkshort_del_handle(&mkshort_handle);
68   mkshort_del_handle(&mkshort_handle2);
69 }
70 
71 // Find the first token in string |in| when there may be leading whitespace.
72 // These files have weird mixtures of spaces and tabs.
FirstTokenAt(const QString & in,int index)73 static QString FirstTokenAt(const QString& in, int index) {
74   static const QRegExp sep("\\s+");
75   return in.mid(index, -1).section(sep, 0, 0, QString::SectionSkipEmpty);
76 }
77 
78 // Centralize Date/Time parsing between Waypoint and Trackpoint readers.
SetWaypointTime(Waypoint * wpt,const QString & date,const QString & time)79 static void  SetWaypointTime(Waypoint* wpt, const QString& date, const QString& time) {
80   QDate qdate = QDate::fromString(date, "dd-MMM-yy");
81   QTime qtime = QTime::fromString(time, "hh:mm:ss");
82   if (qdate.isValid() && qtime.isValid()) {
83     wpt->SetCreationTime(QDateTime(qdate, qtime, Qt::UTC));
84   }
85 }
86 
87 // Loop and parse all the lines of the file. This is complicated by the
88 // variety of programs in the wild that loosely use this format and that
89 // there are two distinct versions of PCX.
90 // In the simplest form, white spaces are disallowed in the individual
91 // fields and everything is just whitespace separated and the fields have
92 // a fixed order.
93 // The presence of an "F" or "H" record changes the precedence of parse
94 // to allow fields in any order and length, based on their position in
95 // these header lines. Oddly, we've seen only 'W' records take this form.
96 
data_read()97 static void data_read() {
98   int symnum;
99   Waypoint* wpt_tmp;
100   char* buff;
101   route_head* track = nullptr;
102   route_head* route = nullptr;
103   int line_number = 0;
104 
105   read_as_degrees = 0;
106   int points = 0;
107 
108   // Each line is both |buff| as a C string and |line| as a QString.
109   while ((buff = gbfgetstr(file_in))) {
110     const QString line = QString(buff).trimmed();
111     char* ibuf = lrtrim(buff);
112     if ((line_number++ == 0) && file_in->unicode) {
113       cet_convert_init(CET_CHARSET_UTF8, 1);
114     }
115 
116     switch (ibuf[0]) {
117       case 'W': {
118         QStringList tokens =
119             line.split(QRegExp("\\s+"), QString::KeepEmptyParts);
120         if (tokens.size() < 6) {
121           fatal(MYNAME
122                 ": Unable to parse waypoint, not all required columns "
123                 "contained\n");
124         }
125         // tokens[0] = "W".
126         QString name = tokens[1];
127         QString tbuf = tokens[2];
128         QString nbuf = tokens[3];
129         QString date = tokens[4];
130         QString time = tokens[5];
131         long alt = unknown_alt;
132         if (tokens.size() == 7) {
133           alt = tokens[6].toDouble();
134         }
135 
136         QString desc;
137         if (comment_col > 0) {
138           desc = line.mid(comment_col, -1);
139         }
140 
141         symnum = 18;
142         if (sym_col > 0) {
143           symnum = atoi(&ibuf[sym_col]);
144         }
145 
146         // If we have explicit columns for lat and lon,
147         // copy those entire words (warning: no spaces)
148         // into the respective coord buffers.
149         if (lat_col > 0) {
150           tbuf = FirstTokenAt(line, lat_col);
151         }
152         if (lon_col > 0) {
153           nbuf = FirstTokenAt(line, lon_col);
154         }
155 
156         wpt_tmp = new Waypoint;
157         wpt_tmp->altitude = alt;
158         SetWaypointTime(wpt_tmp, date, time);
159         wpt_tmp->shortname = name.trimmed();
160         wpt_tmp->description = desc.trimmed();
161         wpt_tmp->icon_descr = gt_find_desc_from_icon_number(symnum, PCX);
162 
163         double lat = 0;
164         double lon = 0;
165         if (read_as_degrees || read_gpsu) {
166           human_to_dec(tbuf, &lat, &lon, 1);
167           human_to_dec(nbuf, &lat, &lon, 2);
168           wpt_tmp->longitude = lon;
169           wpt_tmp->latitude = lat;
170         } else {
171           lat = tbuf.midRef(1, -1).toDouble();
172           lon = nbuf.midRef(1, -1).toDouble();
173           if (tbuf[0] == 'S') {
174             lat = -lat;
175           }
176           if (nbuf[0] == 'W') {
177             lon = -lon;
178           }
179           wpt_tmp->longitude = ddmm2degrees(lon);
180           wpt_tmp->latitude = ddmm2degrees(lat);
181         }
182 
183         if (route != nullptr) {
184           route_add_wpt(route, new Waypoint(*wpt_tmp));
185         }
186         waypt_add(wpt_tmp);
187         points++;
188         break;
189       }
190       case 'H':
191         /* Garmap2 has headers
192         "H(2 spaces)LATITUDE(some spaces)LONGITUDE(etc... followed by);track
193           everything else is
194           H(2 chars)TN(trackname\0)
195           */
196         if (points > 0) {
197           track = nullptr;
198           points = 0;
199         }
200         if (track == nullptr) {
201           if (ibuf[3] == 'L' && ibuf[4] == 'A') {
202             track = new route_head;
203             track->rte_name = "track";
204             track_add_head(track);
205           } else if (ibuf[3] == 'T' && ibuf[4] == 'N') {
206             track = new route_head;
207             track->rte_name = &ibuf[6];
208             track_add_head(track);
209           }
210         }
211         break;
212       case 'R':
213         route = new route_head;
214         route->rte_name = QString(&ibuf[1]).trimmed();
215         route_add_head(route);
216         break;
217       case 'T': {
218         QStringList tokens =
219             line.split(QRegExp("\\s+"), QString::KeepEmptyParts);
220         if (tokens.size() < 6) {
221           fatal(MYNAME
222                 ": Unable to parse trackpoint, not all required columns "
223                 "contained\n");
224         }
225 
226         // tokens[0] = "W".
227         QString tbuf = tokens[1];
228         QString nbuf = tokens[2];
229         QString date = tokens[3];
230         QString time = tokens[4];
231         double alt = tokens[5].toDouble();
232 
233         wpt_tmp = new Waypoint;
234         SetWaypointTime(wpt_tmp, date, time);
235 
236         double lat, lon;
237         if (read_as_degrees) {
238           human_to_dec(tbuf, &lat, &lon, 1);
239           human_to_dec(nbuf, &lat, &lon, 2);
240 
241           wpt_tmp->longitude = lon;
242           wpt_tmp->latitude = lat;
243         } else {
244           lat = tbuf.midRef(1, -1).toDouble();
245           lon = nbuf.midRef(1, -1).toDouble();
246           if (tbuf[0] == 'S') {
247             lat = -lat;
248           }
249           if (nbuf[0] == 'W') {
250             lon = -lon;
251           }
252           wpt_tmp->longitude = ddmm2degrees(lon);
253           wpt_tmp->latitude = ddmm2degrees(lat);
254         }
255         wpt_tmp->altitude = alt;
256 
257         /* Did we get a track point before a track header? */
258         if (track == nullptr) {
259           track = new route_head;
260           track->rte_name = "Default";
261           track_add_head(track);
262         }
263         track_add_wpt(track, wpt_tmp);
264         points++;
265         break;
266       }
267       case 'U':
268         read_as_degrees = !strncmp("LAT LON DEG", ibuf + 3, 11);
269         if (strstr(ibuf, "UTM")) {
270           fatal(MYNAME ": UTM is not supported.\n");
271         }
272         break;
273       // GPSU is apparently PCX but with a different definition
274       // of "LAT LON DM" - unlike the other, it actually IS decimal
275       // minutes.
276       case 'I':
277         read_gpsu = !(strstr(ibuf, "GPSU") == nullptr);
278         break;
279       // This is a format specifier.  Use this line to figure out
280       // where our other columns start.
281       case 'F': {
282         comment_col = line.indexOf("comment", 0, Qt::CaseInsensitive);
283         sym_col = line.indexOf("symbol", 0, Qt::CaseInsensitive);
284         lat_col = line.indexOf("latitude", 0, Qt::CaseInsensitive);
285         lon_col = line.indexOf("longitude", 0, Qt::CaseInsensitive);
286       } break;
287       default:
288         break;
289     }
290   }
291 }
292 
gpsutil_disp(const Waypoint * wpt)293 static void gpsutil_disp(const Waypoint* wpt) {
294   int icon_token = 0;
295 
296   double lon = degrees2ddmm(wpt->longitude);
297   double lat = degrees2ddmm(wpt->latitude);
298 
299   QDateTime dt = wpt->GetCreationTime().toUTC();
300   const QString ds = dt.toString("dd-MMM-yy hh:mm:ss").toUpper();
301 
302   if (deficon) {
303     icon_token = atoi(deficon);
304   } else {
305     icon_token = gt_find_icon_number_from_desc(wpt->icon_descr, PCX);
306     if (get_cache_icon(wpt)) {
307       icon_token = gt_find_icon_number_from_desc(get_cache_icon(wpt), PCX);
308     }
309   }
310 
311   gbfprintf(file_out, "W  %-6.6s %c%08.5f %c%011.5f %s %5.f %-40.40s %5e  %d\n",
312             global_opts.synthesize_shortnames
313                 ? CSTRc(mkshort_from_wpt(mkshort_handle, wpt))
314                 : CSTRc(wpt->shortname),
315             lat < 0.0 ? 'S' : 'N', fabs(lat), lon < 0.0 ? 'W' : 'E', fabs(lon),
316             CSTR(ds), (wpt->altitude == unknown_alt) ? -9999 : wpt->altitude,
317             (wpt->description != nullptr) ? CSTRc(wpt->description) : "", 0.0,
318             icon_token);
319 }
320 
pcx_track_hdr(const route_head * trk)321 static void pcx_track_hdr(const route_head* trk) {
322   route_ctr++;
323 
324   QString default_name = QString::asprintf("Trk%03d", route_ctr);
325   QString name =
326       mkshort(mkshort_handle2,
327               trk->rte_name.isEmpty() ? CSTR(default_name) : trk->rte_name);
328   /* Carto Exploreur (popular in France) chokes on trackname headers,
329    * so provide option to suppress these.
330    */
331   if (!cartoexploreur) {
332     gbfprintf(file_out, "\n\nH  TN %s\n", CSTR(name));
333   }
334   gbfprintf(file_out,
335             "H  LATITUDE    LONGITUDE    DATE      TIME     ALT  ;track\n");
336 }
337 
pcx_route_hdr(const route_head * rte)338 static void pcx_route_hdr(const route_head* rte) {
339   route_ctr++;
340   QString default_name = QString::asprintf("Rte%03d", route_ctr);
341 
342   QString name = mkshort(
343       mkshort_handle2, rte->rte_name.isEmpty() ? default_name : rte->rte_name);
344 
345   /* see pcx_track_hdr */
346   if (!cartoexploreur) {
347     gbfprintf(file_out, "\n\nR  %s\n", CSTR(name));
348   }
349   gbfprintf(file_out,
350             "\n"
351             "H  IDNT   LATITUDE    LONGITUDE    DATE      TIME     ALT   "
352             "DESCRIPTION                              PROXIMITY     SYMBOL "
353             ";waypts\n");
354 }
355 
pcx_track_disp(const Waypoint * wpt)356 static void pcx_track_disp(const Waypoint* wpt) {
357   double lon = degrees2ddmm(wpt->longitude);
358   double lat = degrees2ddmm(wpt->latitude);
359 
360   QDateTime dt = wpt->GetCreationTime().toUTC();
361   const QString ds = dt.toString("dd-MMM-yy hh:mm:ss").toUpper();
362 
363   gbfprintf(file_out, "T  %c%08.5f %c%011.5f %s %.f\n", lat < 0.0 ? 'S' : 'N',
364             fabs(lat), lon < 0.0 ? 'W' : 'E', fabs(lon), CSTR(ds),
365             wpt->altitude);
366 }
367 
data_write()368 static void data_write() {
369   gbfprintf(file_out,
370             "H  SOFTWARE NAME & VERSION\n"
371             "I  PCX5 2.09\n"
372             "\n"
373             "H  R DATUM                IDX DA            DF            DX      "
374             "      DY            DZ\n"
375             "M  G WGS 84               121 +0.000000e+00 +0.000000e+00 "
376             "+0.000000e+00 +0.000000e+00 +0.000000e+00\n"
377             "\n"
378             "H  COORDINATE SYSTEM\n"
379             "U  LAT LON DM\n");
380 
381   setshort_length(mkshort_handle, 6);
382 
383   setshort_length(mkshort_handle2, 20); /* for track and route names */
384   setshort_whitespace_ok(mkshort_handle2, 0);
385   setshort_mustuniq(mkshort_handle2, 0);
386 
387   if (global_opts.objective == wptdata) {
388     gbfprintf(file_out,
389               "\n"
390               "H  IDNT   LATITUDE    LONGITUDE    DATE      TIME     ALT   "
391               "DESCRIPTION                              PROXIMITY     SYMBOL "
392               ";waypts\n");
393 
394     waypt_disp_all(gpsutil_disp);
395   } else if (global_opts.objective == trkdata) {
396     route_ctr = 0;
397     track_disp_all(pcx_track_hdr, nullptr, pcx_track_disp);
398   } else if (global_opts.objective == rtedata) {
399     route_ctr = 0;
400     route_disp_all(pcx_route_hdr, nullptr, gpsutil_disp);
401   }
402 }
403 
404 ff_vecs_t pcx_vecs = {
405     ff_type_file,      FF_CAP_RW_ALL, rd_init,    wr_init, rd_deinit,
406     wr_deinit,         data_read,     data_write, nullptr,    &pcx_args,
407     CET_CHARSET_ASCII, 1 /* CET-REVIEW */
408   , NULL_POS_OPS,
409   nullptr
410 };
411