1 /*
2 Support for Columbus/Visiontac V900 csv format
3 This format pads fields with NULL up to a fixed per field length.
4 Because of that, and because xcsv does not allows a regex as a field delimiter,
5 a special c module is required.
6
7 Copyright (C) 2009 Tal Benavidor
8
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation; either version 2 of the License, or
12 (at your option) any later version.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22
23 TODO:
24 - QUESTION: course = heading ??
25 - HEIGHT: Altitude in meters (not corrected to WGS84...) ??
26 */
27
28 /******************************************************************************
29 FILE FORMAT INFO
30 =================
31
32 File has csv extension, and is somewhat csv like creature...
33 All lines end with \r\n
34 First line is a header line. It contains no nulls.
35 Following lines are record lines. They are comma separated, but fields always
36 have the exact same length (per field), and therefore, the commas are always
37 at the exact same position on the line. Fields are padded with nulls, in case
38 they have shorter value then the fixed field length.
39 Two modes are available: basic and advanced.
40
41 The following two examples show "*" where null appears.
42
43 ------basic mode - start-------------------------
44 INDEX,TAG,DATE,TIME,LATITUDE N/S,LONGITUDE E/W,HEIGHT,SPEED,HEADING,VOX
45 1*****,T,090404,063401,31.765931N,035.206969E,821**,0***,0**,*********
46 2*****,T,090404,063402,31.765931N,035.206969E,821**,0***,0**,*********
47 3*****,T,090404,063403,31.765933N,035.206971E,821**,0***,0**,*********
48 4*****,T,090404,063404,31.765933N,035.206971E,822**,0***,0**,*********
49 5*****,T,090404,063407,31.765934N,035.206971E,824**,0***,0**,*********
50 ------basic mode - end---------------------------
51
52
53 ------advanced mode - start-------------------------
54 INDEX,TAG,DATE,TIME,LATITUDE N/S,LONGITUDE E/W,HEIGHT,SPEED,HEADING,FIX MODE,VALID,PDOP,HDOP,VDOP,VOX
55 1*****,T,090204,055722,31.768380N,035.209656E,149**,0***,0**,3D,SPS ,2.6**,2.4**,1.0**,*********
56 2*****,T,090204,055723,31.768380N,035.209656E,149**,0***,0**,3D,SPS ,2.5**,2.3**,0.9**,*********
57 3*****,T,090204,055724,31.768378N,035.209658E,149**,0***,0**,3D,SPS ,2.5**,2.3**,0.9**,*********
58 4*****,T,090204,055725,31.768378N,035.209658E,149**,0***,0**,3D,SPS ,2.5**,2.3**,0.9**,*********
59 5*****,T,090204,055728,31.768376N,035.209660E,150**,0***,0**,3D,SPS ,2.5**,2.3**,0.9**,*********
60 6*****,T,090204,055729,31.768376N,035.209660E,150**,0***,0**,3D,SPS ,4.0**,2.8**,2.9**,*********
61 7*****,T,090204,055730,31.768376N,035.209661E,150**,0***,0**,3D,SPS ,2.5**,2.3**,0.9**,*********
62 8*****,T,090204,055731,31.768376N,035.209661E,150**,0***,0**,3D,SPS ,2.5**,2.3**,0.9**,*********
63 9*****,T,090204,055737,31.768326N,035.209993E,150**,0***,0**,3D,SPS ,2.5**,2.3**,0.9**,*********
64 10****,T,090204,055738,31.768339N,035.209976E,153**,0***,0**,3D,SPS ,2.5**,2.3**,0.9**,*********
65 11****,T,090204,055739,31.768338N,035.209991E,155**,0***,0**,3D,SPS ,2.5**,2.3**,0.9**,*********
66 42****,C,090724,162320,31.763841N,035.205461E,788**,9***,344,3D,SPS ,1.2**,0.9**,0.8**,*********
67 121***,V,090724,162502,31.769619N,035.208964E,786**,16**,306,3D,SPS ,1.1**,0.8**,0.8**,VOX00003*
68 ------advanced mode - end---------------------------
69
70 for a little more info, see structures:
71 one_line_advanced_mode, one_line_basic_mode, one_line_common_start.
72 ******************************************************************************/
73
74 #include "defs.h"
75 #include <cassert>
76 #include <cstdio>
77 #include <cstdlib> // atoi
78
79 /* the start of each record (line) is common to both advanced and basic mode.
80 it will be parsed by a single common code. hence, it will be easier and clearer
81 to have a common structure for it.
82 */
83 struct one_line_common_start {
84 char index[6]; /* record number */
85 char comma1; /* ',' */
86 char tag; /* tag type. T=trackpoint. TODO: more options??? */
87 char comma2; /* ',' */
88 char date[6]; /* YYMMDD. YY=09 is 2009. */
89 char comma3; /* ',' */
90 char time[6]; /* HHMMSS */
91 char comma4; /* ',' */
92 char latitude_num[9]; /* example: "31.768380" */
93 char latitude_NS; /* 'N' or 'S' */
94 char comma5; /* ',' */
95 char longitude_num[10]; /* example: "035.209656" */
96 char longitude_EW; /* 'E' or 'W' */
97 char comma6; /* ',' */
98 char height[5]; /* Altitude in meters.
99 * (not corrected to WGS84 ??) */
100 char comma7; /* ',' */
101 char speed[4]; /* speed in km/h. no decimal point. */
102 char comma8; /* ',' */
103 char heading[3]; /* heading in degrees */
104 char comma9; /* ',' */
105 };
106
107 /* this structure holds one record (line) in advanced logging mode.
108 advanced mode lines looks like this ('*' means NULL):
109 1717**,T,090204,062634,31.765528N,035.207730E,772**,0***,0**,2D,SPS ,2.1**,1.9**,1.0**,*********
110 */
111 struct one_line_advanced_mode {
112 struct one_line_common_start common;
113 char fixmode[2]; /* "2D" or "3D" */
114 char comma10; /* ',' */
115 char valid[4]; /* "SPS " or "DGPS" */
116 char comma11; /* ',' */
117 char pdop[5];
118 char comma12; /* ',' */
119 char hdop[5];
120 char comma13; /* ',' */
121 char vdop[5];
122 char comma14; /* ',' */
123 char vox[9]; /* voicetag recorded */
124 char cr; /* '\r' */
125 char lf; /* '\n' */
126 };
127
128 /* this structure holds one record (line) in basic logging mode.
129 basic mode lines looks like this ('*' means NULL):
130 1*****,T,090404,063401,31.765931N,035.206969E,821**,0***,0**,*********
131 */
132 struct one_line_basic_mode {
133 struct one_line_common_start common;
134 char vox[9]; /* voicetag recorded */
135 char cr; /* '\r' */
136 char lf; /* '\n' */
137 };
138
139
140 static FILE* fin = nullptr;
141
142 /* copied from dg-100.cpp */
143 static void
v900_log(const char * fmt,...)144 v900_log(const char* fmt, ...)
145 {
146 va_list ap;
147
148 if (global_opts.debug_level < 1) {
149 return;
150 }
151
152 va_start(ap, fmt);
153 vfprintf(stderr, fmt, ap);
154 va_end(ap);
155 }
156
157 static void
v900_rd_init(const QString & fname)158 v900_rd_init(const QString& fname)
159 {
160 v900_log("%s(%s)\n",__func__,qPrintable(fname));
161 /* note: file is opened in binary mode, since lines end with \r\n, and in windows text mode
162 that will be translated to a single \n, making the line len one character shorter than
163 on linux machines.
164 */
165 fin = ufopen(fname, "rb");
166 if (!fin) {
167 fatal("v900: could not open '%s'.\n", qPrintable(fname));
168 }
169 }
170
171 static void
v900_rd_deinit()172 v900_rd_deinit()
173 {
174 v900_log("%s\n",__func__);
175 if (fin) {
176 fclose(fin);
177 }
178 }
179
180 /* copied from dg-100.c - slight (incompatible) modification to how the date parameter is used */
181 static QDateTime
bintime2utc(int date,int time)182 bintime2utc(int date, int time) {
183 int secs = time % 100;
184 time /= 100;
185 int mins = time % 100;
186 time /= 100;
187 // What's left in 'time' is hours, ranged 0-23.
188 QTime tm(time, mins, secs);
189
190 // 'date' starts at 2000 and is YYMMDD
191 int day = date % 100;
192 date /= 100;
193 int month = date % 100;
194 date /= 100;
195 // What's left in 'date' is year.
196 QDate dt(date + 2000, month, day);
197
198 return QDateTime(dt, tm, Qt::UTC);
199 }
200
201 static void
v900_read()202 v900_read()
203 {
204 /* use line buffer large enough to hold either basic or advanced mode lines. */
205 union {
206 struct one_line_basic_mode bas;
207 struct one_line_advanced_mode adv;
208 char text[200]; /* used to read the header line, which is normal text */
209 } line;
210 int lc = 0;
211
212 v900_log("%s\n",__func__);
213
214 /*
215 Basic mode: INDEX,TAG,DATE,TIME,LATITUDE N/S,LONGITUDE E/W,HEIGHT,SPEED,HEADING,VOX
216 Advanced mode: INDEX,TAG,DATE,TIME,LATITUDE N/S,LONGITUDE E/W,HEIGHT,SPEED,HEADING,FIX MODE,VALID,PDOP,HDOP,VDOP,VOX
217 */
218 /* first, determine if this is advanced mode by reading the first line.
219 since the first line does not contain any nulls, it can be safely read by fgets(). */
220 if (!fgets(line.text, sizeof(line), fin)) {
221 fatal("v900: error reading header (first) line from input file\n");
222 }
223 int is_advanced_mode = (nullptr != strstr(line.text,"PDOP")); /* PDOP field appears only in advanced mode */
224
225 v900_log("header line: %s",line.text);
226 v900_log("is_advance_mode=%d\n",is_advanced_mode);
227
228 auto* track = new route_head;
229 track->rte_name = "V900 tracklog";
230 track->rte_desc = "V900 GPS tracklog data";
231 track_add_head(track);
232
233 while (true) {
234 int bad = 0;
235 int record_len = is_advanced_mode ? sizeof(line.adv) : sizeof(line.bas);
236 if (fread(&line, record_len, 1, fin) != 1) {
237 break;
238 }
239 lc++;
240
241 /* change all "," characters to NULLs.
242 so every field is null terminated.
243 */
244 bad |= (line.bas.common.comma1 != ',');
245 bad |= (line.bas.common.comma2 != ',');
246 bad |= (line.bas.common.comma3 != ',');
247 bad |= (line.bas.common.comma4 != ',');
248 bad |= (line.bas.common.comma5 != ',');
249 bad |= (line.bas.common.comma6 != ',');
250 bad |= (line.bas.common.comma7 != ',');
251 bad |= (line.bas.common.comma8 != ',');
252 bad |= (line.bas.common.comma9 != ',');
253
254 if (bad) {
255 warning("v900: skipping malformed record at line %d\n", lc);
256 }
257
258 line.bas.common.comma1 = 0;
259 line.bas.common.comma2 = 0;
260 line.bas.common.comma3 = 0;
261 line.bas.common.comma4 = 0;
262 line.bas.common.comma5 = 0;
263 line.bas.common.comma6 = 0;
264 line.bas.common.comma7 = 0;
265 line.bas.common.comma8 = 0;
266 line.bas.common.comma9 = 0;
267 if (is_advanced_mode) {
268 /* change all "," characters to NULLs.
269 so every field is null terminated.
270 */
271 assert(line.adv.comma10==','); // TODO: abort with fatal()
272 assert(line.adv.comma11==',');
273 assert(line.adv.comma12==',');
274 assert(line.adv.comma13==',');
275 assert(line.adv.comma14==',');
276 assert(line.adv.cr=='\r');
277 assert(line.adv.lf=='\n');
278 line.adv.comma10 = 0;
279 line.adv.comma11 = 0;
280 line.adv.comma12 = 0;
281 line.adv.comma13 = 0;
282 line.adv.comma14 = 0;
283 line.adv.cr = 0; /* null terminate vox field */
284
285 } else {
286 assert(line.bas.cr=='\r');
287 assert(line.bas.lf=='\n');
288 line.bas.cr = 0; /* null terminate vox field */
289 }
290
291 auto* wpt = new Waypoint;
292
293 /* lat is a string in the form: 31.768380N */
294 char c = line.bas.common.latitude_NS; /* N/S */
295 assert(c == 'N' || c == 'S');
296 wpt->latitude = atof(line.bas.common.latitude_num);
297 if (c == 'S') {
298 wpt->latitude = -wpt->latitude;
299 }
300
301 /* lon is a string in the form: 035.209656E */
302 c = line.bas.common.longitude_EW; /* get E/W */
303 assert(c == 'E' || c == 'W');
304 line.bas.common.longitude_EW = 0; /* the E will confuse atof(), if not removed */
305 wpt->longitude = atof(line.bas.common.longitude_num);
306 if (c == 'W') {
307 wpt->longitude = -wpt->longitude;
308 }
309
310 wpt->altitude = atoi(line.bas.common.height);
311
312 /* handle date/time fields */
313 {
314 int date = atoi(line.bas.common.date);
315 int time = atoi(line.bas.common.time);
316 wpt->SetCreationTime(bintime2utc(date, time));
317 }
318
319 wpt->speed = KPH_TO_MPS(atoi(line.bas.common.speed));
320 wpt->wpt_flags.speed = 1;
321
322 wpt->course = atoi(line.bas.common.heading);
323 wpt->wpt_flags.course = 1;
324
325 if (is_advanced_mode) {
326 wpt->hdop = atof(line.adv.hdop);
327 wpt->vdop = atof(line.adv.vdop);
328 wpt->pdop = atof(line.adv.pdop);
329
330 /* handle fix mode (2d, 3d, etc.) */
331 if (!strncmp(line.adv.valid,"DGPS", sizeof line.adv.valid)) {
332 wpt->fix = fix_dgps;
333 } else if (!strncmp(line.adv.fixmode,"3D", sizeof line.adv.fixmode)) {
334 wpt->fix = fix_3d;
335 } else if (!strncmp(line.adv.fixmode,"2D", sizeof line.adv.fixmode)) {
336 wpt->fix = fix_2d;
337 } else
338 /* possible values: fix_unknown,fix_none,fix_2d,fix_3d,fix_dgps,fix_pps */
339 {
340 wpt->fix = fix_unknown;
341 }
342 }
343
344 track_add_wpt(track, wpt);
345 if (line.bas.common.tag != 'T') {
346 // A 'G' tag appears to be a 'T' tag, but generated on the trailing
347 // edge of a DGPS fix as it decays to an SPS fix. See 1/13/13 email
348 // thread on gpsbabel-misc with Jamie Robertson.
349 assert(line.bas.common.tag == 'C' || line.bas.common.tag == 'G' ||
350 line.bas.common.tag == 'V');
351 auto* wpt2 = new Waypoint(*wpt);
352 if (line.bas.common.tag == 'V') { // waypoint with voice recording?
353 char vox_file_name[sizeof(line.adv.vox)+5];
354 const char* vox = is_advanced_mode ? line.adv.vox : line.bas.vox;
355 assert(vox[0] != '\0');
356 strcpy(vox_file_name,vox);
357 strcat(vox_file_name,".WAV");
358 wpt2->shortname = vox_file_name;
359 wpt2->description = vox_file_name;
360 waypt_add_url(wpt2, vox_file_name, vox_file_name);
361 }
362 waypt_add(wpt2);
363 }
364 }
365 }
366
367 ff_vecs_t v900_vecs = {
368 ff_type_file,
369 {ff_cap_read, ff_cap_read, ff_cap_none}, /* Read only format. May only read trackpoints and waypoints. */
370 v900_rd_init,
371 nullptr, /* wr_init */
372 v900_rd_deinit,
373 nullptr, /* wr_deinit */
374 v900_read,
375 nullptr, /* write */
376 nullptr,
377 nullptr, /* args */
378 CET_CHARSET_UTF8, 1, /* Could be US-ASCII, since we only read "0-9,A-Z\n\r" */
379 {nullptr,nullptr,nullptr,nullptr,nullptr,nullptr},
380 nullptr
381 };
382