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