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