1 /*
2     This file is part of darktable,
3     Copyright (C) 2011-2021 darktable developers.
4 
5     darktable is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9 
10     darktable is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with darktable.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 #include "common/gpx.h"
19 #include "common/geo.h"
20 #include "common/darktable.h"
21 #include <glib.h>
22 #include <inttypes.h>
23 
24 /* GPX XML parser */
25 typedef enum _gpx_parser_element_t
26 {
27   GPX_PARSER_ELEMENT_NONE = 0,
28   GPX_PARSER_ELEMENT_TRKPT = 1 << 0,
29   GPX_PARSER_ELEMENT_TIME = 1 << 1,
30   GPX_PARSER_ELEMENT_ELE = 1 << 2,
31   GPX_PARSER_ELEMENT_NAME = 1 << 3
32 } _gpx_parser_element_t;
33 
34 typedef struct dt_gpx_t
35 {
36   /* the list of track records parsed */
37   GList *trkpts;
38   GList *trksegs;
39 
40   /* currently parsed track point */
41   dt_gpx_track_point_t *current_track_point;
42   _gpx_parser_element_t current_parser_element;
43   gboolean invalid_track_point;
44   gboolean parsing_trk;
45   uint32_t segid;
46   char *seg_name;
47 } dt_gpx_t;
48 
49 static void _gpx_parser_start_element(GMarkupParseContext *ctx, const gchar *element_name,
50                                       const gchar **attribute_names, const gchar **attribute_values,
51                                       gpointer ueer_data, GError **error);
52 static void _gpx_parser_end_element(GMarkupParseContext *context, const gchar *element_name,
53                                     gpointer user_data, GError **error);
54 static void _gpx_parser_text(GMarkupParseContext *context, const gchar *text, gsize text_len,
55                              gpointer user_data, GError **error);
56 
57 static GMarkupParser _gpx_parser
58     = { _gpx_parser_start_element, _gpx_parser_end_element, _gpx_parser_text, NULL, NULL };
59 
60 
_sort_track(gconstpointer a,gconstpointer b)61 static gint _sort_track(gconstpointer a, gconstpointer b)
62 {
63   const dt_gpx_track_point_t *pa = (const dt_gpx_track_point_t *)a;
64   const dt_gpx_track_point_t *pb = (const dt_gpx_track_point_t *)b;
65   return g_date_time_compare(pa->time, pb->time);
66 }
67 
_sort_segment(gconstpointer a,gconstpointer b)68 static gint _sort_segment(gconstpointer a, gconstpointer b)
69 {
70   const dt_gpx_track_segment_t *pa = (const dt_gpx_track_segment_t *)a;
71   const dt_gpx_track_segment_t *pb = (const dt_gpx_track_segment_t *)b;
72   return g_date_time_compare(pa->start_dt, pb->start_dt);
73 }
74 
dt_gpx_new(const gchar * filename)75 dt_gpx_t *dt_gpx_new(const gchar *filename)
76 {
77   dt_gpx_t *gpx = NULL;
78   GMarkupParseContext *ctx = NULL;
79   GError *err = NULL;
80   GMappedFile *gpxmf = NULL;
81   gchar *gpxmf_content = NULL;
82   gint gpxmf_size = 0;
83   gint bom_offset = 0;
84 
85 
86   /* map gpx file to parse into memory */
87   gpxmf = g_mapped_file_new(filename, FALSE, &err);
88   if(err) goto error;
89 
90   gpxmf_content = g_mapped_file_get_contents(gpxmf);
91   gpxmf_size = g_mapped_file_get_length(gpxmf);
92   if(!gpxmf_content || gpxmf_size < 10) goto error;
93 
94   /* allocate new dt_gpx_t context */
95   gpx = g_malloc0(sizeof(dt_gpx_t));
96 
97   /* skip UTF-8 BOM */
98   if(gpxmf_size > 3 && gpxmf_content[0] == '\xef' && gpxmf_content[1] == '\xbb' && gpxmf_content[2] == '\xbf')
99     bom_offset = 3;
100 
101   /* initialize the parser and start parse gpx xml data */
102   ctx = g_markup_parse_context_new(&_gpx_parser, 0, gpx, NULL);
103   g_markup_parse_context_parse(ctx, gpxmf_content + bom_offset, gpxmf_size - bom_offset, &err);
104   if(err) goto error;
105 
106 
107   /* cleanup and return gpx context */
108   g_markup_parse_context_free(ctx);
109   g_mapped_file_unref(gpxmf);
110 
111   gpx->trkpts = g_list_sort(gpx->trkpts, _sort_track);
112   gpx->trksegs = g_list_sort(gpx->trksegs, _sort_segment);
113 
114   return gpx;
115 
116 error:
117   if(err)
118   {
119     fprintf(stderr, "dt_gpx_new: %s\n", err->message);
120     g_error_free(err);
121   }
122 
123   if(ctx) g_markup_parse_context_free(ctx);
124 
125   g_free(gpx);
126 
127   if(gpxmf) g_mapped_file_unref(gpxmf);
128 
129   return NULL;
130 }
131 
_track_seg_free(dt_gpx_track_segment_t * trkseg)132 void _track_seg_free(dt_gpx_track_segment_t *trkseg)
133 {
134   g_free(trkseg->name);
135   g_free(trkseg);
136 }
137 
_track_pts_free(dt_gpx_track_point_t * trkpt)138 void _track_pts_free(dt_gpx_track_point_t *trkpt)
139 {
140   g_date_time_unref(trkpt->time);
141   g_free(trkpt);
142 }
143 
dt_gpx_destroy(struct dt_gpx_t * gpx)144 void dt_gpx_destroy(struct dt_gpx_t *gpx)
145 {
146   g_assert(gpx != NULL);
147 
148   if(gpx->trkpts) g_list_free_full(gpx->trkpts, (GDestroyNotify)_track_pts_free);
149   if(gpx->trksegs) g_list_free_full(gpx->trksegs, (GDestroyNotify)_track_seg_free);
150 
151   g_free(gpx);
152 }
153 
dt_gpx_get_location(struct dt_gpx_t * gpx,GDateTime * timestamp,dt_image_geoloc_t * geoloc)154 gboolean dt_gpx_get_location(struct dt_gpx_t *gpx, GDateTime *timestamp, dt_image_geoloc_t *geoloc)
155 {
156   g_assert(gpx != NULL);
157 
158   /* verify that we got at least 2 trackpoints */
159   if(g_list_shorter_than(gpx->trkpts,2)) return FALSE;
160 
161   for(GList *item = gpx->trkpts; item; item = g_list_next(item))
162   {
163     dt_gpx_track_point_t *tp = (dt_gpx_track_point_t *)item->data;
164 
165     /* if timestamp is out of time range return false but fill
166        closest location value start or end point */
167     const gint cmp = g_date_time_compare(timestamp, tp->time);
168     if((!item->next && cmp >= 0) || (cmp <= 0))
169     {
170       geoloc->longitude = tp->longitude;
171       geoloc->latitude = tp->latitude;
172       geoloc->elevation = tp->elevation;
173       return FALSE;
174     }
175 
176     /* check if timestamp is within current and next trackpoint */
177     const gint cmp_n = g_date_time_compare(timestamp, ((dt_gpx_track_point_t *)item->next->data)->time);
178     if((cmp >= 0) && (item->next && cmp_n <= 0))
179     {
180       geoloc->longitude = tp->longitude;
181       geoloc->latitude = tp->latitude;
182       geoloc->elevation = tp->elevation;
183       return TRUE;
184     }
185   }
186 
187   /* should not reach this point */
188   return FALSE;
189 }
190 
191 /*
192  * GPX XML parser code
193  */
_gpx_parser_start_element(GMarkupParseContext * ctx,const gchar * element_name,const gchar ** attribute_names,const gchar ** attribute_values,gpointer user_data,GError ** error)194 void _gpx_parser_start_element(GMarkupParseContext *ctx, const gchar *element_name,
195                                const gchar **attribute_names, const gchar **attribute_values,
196                                gpointer user_data, GError **error)
197 {
198   dt_gpx_t *gpx = (dt_gpx_t *)user_data;
199 
200   if(gpx->parsing_trk == FALSE)
201   {
202     // we only parse tracks and its points, nothing else
203     if(strcmp(element_name, "trk") == 0)
204     {
205       gpx->parsing_trk = TRUE;
206     }
207     goto end;
208   }
209 
210   /* from here on, parse wpType data from track points */
211   if(strcmp(element_name, "trkpt") == 0)
212   {
213     if(gpx->current_track_point)
214     {
215       fprintf(stderr, "broken gpx file, new trkpt element before the previous ended.\n");
216       g_free(gpx->current_track_point);
217     }
218 
219     const gchar **attribute_name = attribute_names;
220     const gchar **attribute_value = attribute_values;
221 
222     gpx->invalid_track_point = FALSE;
223 
224     if(*attribute_name)
225     {
226       gpx->current_track_point = g_malloc0(sizeof(dt_gpx_track_point_t));
227       gpx->current_track_point->segid = gpx->segid;
228 
229       /* initialize with NAN for validation check */
230       gpx->current_track_point->longitude = NAN;
231       gpx->current_track_point->latitude = NAN;
232       gpx->current_track_point->elevation = NAN;
233 
234       /* go thru the attributes to find and get values of lon / lat*/
235       while(*attribute_name)
236       {
237         if(strcmp(*attribute_name, "lon") == 0)
238           gpx->current_track_point->longitude = g_ascii_strtod(*attribute_value, NULL);
239         else if(strcmp(*attribute_name, "lat") == 0)
240           gpx->current_track_point->latitude = g_ascii_strtod(*attribute_value, NULL);
241 
242         attribute_name++;
243         attribute_value++;
244       }
245 
246       /* validate that we actually got lon / lat attribute values */
247       if(isnan(gpx->current_track_point->longitude) || isnan(gpx->current_track_point->latitude))
248       {
249         fprintf(stderr, "broken gpx file, failed to get lon/lat attribute values for trkpt\n");
250         gpx->invalid_track_point = TRUE;
251       }
252     }
253     else
254       fprintf(stderr, "broken gpx file, trkpt element doesn't have lon/lat attributes\n");
255 
256     gpx->current_parser_element = GPX_PARSER_ELEMENT_TRKPT;
257   }
258   else if(strcmp(element_name, "time") == 0)
259   {
260     if(!gpx->current_track_point) goto element_error;
261 
262     gpx->current_parser_element = GPX_PARSER_ELEMENT_TIME;
263   }
264   else if(strcmp(element_name, "ele") == 0)
265   {
266     if(!gpx->current_track_point) goto element_error;
267 
268     gpx->current_parser_element = GPX_PARSER_ELEMENT_ELE;
269   }
270   else if(strcmp(element_name, "name") == 0)
271   {
272     gpx->current_parser_element = GPX_PARSER_ELEMENT_NAME;
273   }
274   else if(strcmp(element_name, "trkseg") == 0)
275   {
276     dt_gpx_track_segment_t *ts = g_malloc0(sizeof(dt_gpx_track_segment_t));
277     ts->name = gpx->seg_name;
278     ts->id = gpx->segid;
279     gpx->seg_name = NULL;
280     gpx->trksegs = g_list_prepend(gpx->trksegs, ts);
281   }
282 
283 end:
284 
285   return;
286 
287 element_error:
288   fprintf(stderr, "broken gpx file, element '%s' found outside of trkpt.\n", element_name);
289 }
290 
_gpx_parser_end_element(GMarkupParseContext * context,const gchar * element_name,gpointer user_data,GError ** error)291 void _gpx_parser_end_element(GMarkupParseContext *context, const gchar *element_name, gpointer user_data,
292                              GError **error)
293 {
294   dt_gpx_t *gpx = (dt_gpx_t *)user_data;
295 
296   /* closing trackpoint lets take care of data parsed */
297   if(gpx->parsing_trk == TRUE)
298   {
299     if(strcmp(element_name, "trk") == 0)
300     {
301       gpx->parsing_trk = FALSE;
302     }
303     else if(strcmp(element_name, "trkpt") == 0)
304     {
305       if(!gpx->invalid_track_point)
306         gpx->trkpts = g_list_prepend(gpx->trkpts, gpx->current_track_point);
307       else
308         g_free(gpx->current_track_point);
309 
310       gpx->current_track_point = NULL;
311     }
312     else if(strcmp(element_name, "trkseg") == 0)
313     {
314       gpx->segid++;
315     }
316 
317     /* clear current parser element */
318     gpx->current_parser_element = GPX_PARSER_ELEMENT_NONE;
319   }
320 }
321 
_gpx_parser_text(GMarkupParseContext * context,const gchar * text,gsize text_len,gpointer user_data,GError ** error)322 void _gpx_parser_text(GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data,
323                       GError **error)
324 {
325   dt_gpx_t *gpx = (dt_gpx_t *)user_data;
326 
327   if(gpx->current_parser_element == GPX_PARSER_ELEMENT_NAME)
328   {
329     if(gpx->seg_name) g_free(gpx->seg_name);
330     gpx->seg_name =  g_strdup(text);
331   }
332 
333   if(!gpx->current_track_point) return;
334 
335   if(gpx->current_parser_element == GPX_PARSER_ELEMENT_TIME)
336   {
337     gpx->current_track_point->time = g_date_time_new_from_iso8601(text, NULL);
338     if(!gpx->current_track_point->time)
339     {
340       gpx->invalid_track_point = TRUE;
341       fprintf(stderr, "broken gpx file, failed to pars is8601 time '%s' for trackpoint\n", text);
342     }
343     dt_gpx_track_segment_t *ts = (dt_gpx_track_segment_t *)gpx->trksegs->data;
344     if(ts)
345     {
346       ts->nb_trkpt++;
347       if(!ts->start_dt)
348       {
349         ts->start_dt = gpx->current_track_point->time;
350         ts->trkpt = gpx->current_track_point;
351       }
352       ts->end_dt = gpx->current_track_point->time;
353     }
354   }
355   else if(gpx->current_parser_element == GPX_PARSER_ELEMENT_ELE)
356     gpx->current_track_point->elevation = g_ascii_strtod(text, NULL);
357 }
358 
dt_gpx_get_trkseg(struct dt_gpx_t * gpx)359 GList *dt_gpx_get_trkseg(struct dt_gpx_t *gpx)
360 {
361   return gpx->trksegs;
362 }
363 
dt_gpx_get_trkpts(struct dt_gpx_t * gpx,const guint segid)364 GList *dt_gpx_get_trkpts(struct dt_gpx_t *gpx, const guint segid)
365 {
366   GList *pts = NULL;
367   GList *ts = g_list_nth(gpx->trksegs, segid);
368   if(!ts) return pts;
369   dt_gpx_track_segment_t *tsd = (dt_gpx_track_segment_t *)ts->data;
370   GList *tps = g_list_find(gpx->trkpts, tsd->trkpt);
371   if(!tps) return pts;
372   for(GList *tp = tps; tp; tp = g_list_next(tp))
373   {
374     dt_gpx_track_point_t *tpd = (dt_gpx_track_point_t *)tp->data;
375     if(tpd->segid != segid) return pts;
376     dt_geo_map_display_point_t *p = g_malloc0(sizeof(dt_geo_map_display_point_t));
377     p->lat = tpd->latitude;
378     p->lon = tpd->longitude;
379     pts = g_list_prepend(pts, p);
380   }
381   return pts;
382 }
383 
384 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
385 // vim: shiftwidth=2 expandtab tabstop=2 cindent
386 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
387