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 "common/math.h"
22 #include <glib.h>
23 #include <inttypes.h>
24 
25 /* GPX XML parser */
26 typedef enum _gpx_parser_element_t
27 {
28   GPX_PARSER_ELEMENT_NONE = 0,
29   GPX_PARSER_ELEMENT_TRKPT = 1 << 0,
30   GPX_PARSER_ELEMENT_TIME = 1 << 1,
31   GPX_PARSER_ELEMENT_ELE = 1 << 2,
32   GPX_PARSER_ELEMENT_NAME = 1 << 3
33 } _gpx_parser_element_t;
34 
35 typedef struct dt_gpx_t
36 {
37   /* the list of track records parsed */
38   GList *trkpts;
39   GList *trksegs;
40 
41   /* currently parsed track point */
42   dt_gpx_track_point_t *current_track_point;
43   _gpx_parser_element_t current_parser_element;
44   gboolean invalid_track_point;
45   gboolean parsing_trk;
46   uint32_t segid;
47   char *seg_name;
48 } dt_gpx_t;
49 
50 static void _gpx_parser_start_element(GMarkupParseContext *ctx, const gchar *element_name,
51                                       const gchar **attribute_names, const gchar **attribute_values,
52                                       gpointer ueer_data, GError **error);
53 static void _gpx_parser_end_element(GMarkupParseContext *context, const gchar *element_name,
54                                     gpointer user_data, GError **error);
55 static void _gpx_parser_text(GMarkupParseContext *context, const gchar *text, gsize text_len,
56                              gpointer user_data, GError **error);
57 
58 static GMarkupParser _gpx_parser
59     = { _gpx_parser_start_element, _gpx_parser_end_element, _gpx_parser_text, NULL, NULL };
60 
61 
_sort_track(gconstpointer a,gconstpointer b)62 static gint _sort_track(gconstpointer a, gconstpointer b)
63 {
64   const dt_gpx_track_point_t *pa = (const dt_gpx_track_point_t *)a;
65   const dt_gpx_track_point_t *pb = (const dt_gpx_track_point_t *)b;
66   return g_date_time_compare(pa->time, pb->time);
67 }
68 
_sort_segment(gconstpointer a,gconstpointer b)69 static gint _sort_segment(gconstpointer a, gconstpointer b)
70 {
71   const dt_gpx_track_segment_t *pa = (const dt_gpx_track_segment_t *)a;
72   const dt_gpx_track_segment_t *pb = (const dt_gpx_track_segment_t *)b;
73   return g_date_time_compare(pa->start_dt, pb->start_dt);
74 }
75 
dt_gpx_new(const gchar * filename)76 dt_gpx_t *dt_gpx_new(const gchar *filename)
77 {
78   GError *err = NULL;
79   gint bom_offset = 0;
80   GMarkupParseContext *ctx = NULL;
81   dt_gpx_t *gpx = NULL;
82 
83   /* map gpx file to parse into memory */
84   GMappedFile *gpxmf = g_mapped_file_new(filename, FALSE, &err);
85   if(err) goto error;
86 
87   gchar *gpxmf_content = g_mapped_file_get_contents(gpxmf);
88   const gint gpxmf_size = g_mapped_file_get_length(gpxmf);
89   if(!gpxmf_content || gpxmf_size < 10) goto error;
90 
91   /* allocate new dt_gpx_t context */
92   gpx = g_malloc0(sizeof(dt_gpx_t));
93 
94   /* skip UTF-8 BOM */
95   if(gpxmf_content[0] == '\xef' && gpxmf_content[1] == '\xbb' && gpxmf_content[2] == '\xbf')
96     bom_offset = 3;
97 
98   /* initialize the parser and start parse gpx xml data */
99   ctx = g_markup_parse_context_new(&_gpx_parser, 0, gpx, NULL);
100   g_markup_parse_context_parse(ctx, gpxmf_content + bom_offset, gpxmf_size - bom_offset, &err);
101   if(err) goto error;
102 
103   /* cleanup and return gpx context */
104   g_markup_parse_context_free(ctx);
105   g_mapped_file_unref(gpxmf);
106 
107   gpx->trkpts = g_list_sort(gpx->trkpts, _sort_track);
108   gpx->trksegs = g_list_sort(gpx->trksegs, _sort_segment);
109 
110   return gpx;
111 
112 error:
113   if(err)
114   {
115     fprintf(stderr, "dt_gpx_new: %s\n", err->message);
116     g_error_free(err);
117   }
118 
119   if(ctx) g_markup_parse_context_free(ctx);
120 
121   g_free(gpx);
122 
123   if(gpxmf) g_mapped_file_unref(gpxmf);
124 
125   return NULL;
126 }
127 
_track_seg_free(dt_gpx_track_segment_t * trkseg)128 void _track_seg_free(dt_gpx_track_segment_t *trkseg)
129 {
130   g_free(trkseg->name);
131   g_free(trkseg);
132 }
133 
_track_pts_free(dt_gpx_track_point_t * trkpt)134 void _track_pts_free(dt_gpx_track_point_t *trkpt)
135 {
136   g_date_time_unref(trkpt->time);
137   g_free(trkpt);
138 }
139 
dt_gpx_destroy(struct dt_gpx_t * gpx)140 void dt_gpx_destroy(struct dt_gpx_t *gpx)
141 {
142   g_assert(gpx != NULL);
143 
144   if(gpx->trkpts) g_list_free_full(gpx->trkpts, (GDestroyNotify)_track_pts_free);
145   if(gpx->trksegs) g_list_free_full(gpx->trksegs, (GDestroyNotify)_track_seg_free);
146 
147   g_free(gpx);
148 }
149 
dt_gpx_get_location(struct dt_gpx_t * gpx,GDateTime * timestamp,dt_image_geoloc_t * geoloc)150 gboolean dt_gpx_get_location(struct dt_gpx_t *gpx, GDateTime *timestamp, dt_image_geoloc_t *geoloc)
151 {
152   g_assert(gpx != NULL);
153 
154   /* verify that we got at least 2 trackpoints */
155   if(g_list_shorter_than(gpx->trkpts,2)) return FALSE;
156 
157   for(GList *item = gpx->trkpts; item; item = g_list_next(item))
158   {
159     dt_gpx_track_point_t *tp = (dt_gpx_track_point_t *)item->data;
160 
161     /* if timestamp is out of time range return false but fill
162        closest location value start or end point */
163     const gint cmp = g_date_time_compare(timestamp, tp->time);
164     if((!item->next && cmp >= 0) || (cmp <= 0))
165     {
166       geoloc->longitude = tp->longitude;
167       geoloc->latitude = tp->latitude;
168       geoloc->elevation = tp->elevation;
169       return FALSE;
170     }
171 
172     dt_gpx_track_point_t *tp_next = (dt_gpx_track_point_t *)item->next->data;
173     /* check if timestamp is within current and next trackpoint */
174     const gint cmp_n = g_date_time_compare(timestamp, tp_next->time);
175     if(item->next && cmp_n <= 0)
176     {
177       GTimeSpan seg_diff = g_date_time_difference(tp_next->time, tp->time);
178       GTimeSpan diff = g_date_time_difference(timestamp, tp->time);
179       if(seg_diff == 0 || diff == 0)
180       {
181         geoloc->longitude = tp->longitude;
182         geoloc->latitude = tp->latitude;
183         geoloc->elevation = tp->elevation;
184       }
185       else
186       {
187         /* get the point by interpolation according to timestamp
188 
189         We assume that the maximum difference in longitude is less or equal 180º:
190         since the bigger use case is that of an airplane, never an airplane flies more than 180º in longitude */
191 
192         const double lat1 = tp->latitude;
193         const double lon1 = tp->longitude;
194         const double lat2 = tp_next->latitude;
195         const double lon2 = tp_next->longitude;
196 
197         double lat, lon;
198 
199         const double f = (double)diff / (double)seg_diff; /* the fraction of the distance */
200 
201         if(fabs(lat2 - lat1) < DT_MINIMUM_ANGULAR_DELTA_FOR_GEODESIC
202             && fabs(lon2 - lon1) < DT_MINIMUM_ANGULAR_DELTA_FOR_GEODESIC)
203         {
204           /* short distance (< 10 km), no need for geodesic interpolation */
205           lon = lon1 + (lon2 - lon1) * f;
206           lat = lat1 + (lat2 - lat1) * f;
207         }
208         else
209         {
210           /* interpolation on the earth surface
211              formulas from http://www.movable-type.co.uk/scripts/latlong.html
212 
213              the formulas are correct even if the two point are across the day line, e.g [(0, -179), (0,179)]
214              TO DO: in this case the line which is drawn is incorrect, but this should be a osm_gps issue
215           */
216 
217           /* first, calculate the distance on the earth surface */
218           double d, delta;
219           dt_gpx_geodesic_distance(lat1, lon1,
220                                    lat2, lon2,
221                                    &d, &delta);
222           /* d is the distance on the surface in metres,
223              delta is the angle defined by the two points*/
224 
225           /* then, calculate the intermediate point */
226           dt_gpx_geodesic_intermediate_point(lat1, lon1,
227                                              lat2, lon2,
228                                              delta,
229                                              TRUE,
230                                              f,
231                                              &lat, &lon);
232         }
233 
234         geoloc->latitude = lat;
235         geoloc->longitude = lon;
236 
237         /* make a simple linear interpolation on elevation */
238         if(tp_next->elevation == NAN || tp->elevation == NAN)
239           geoloc->elevation = NAN;
240         else
241           geoloc->elevation = tp->elevation + (tp_next->elevation - tp->elevation) * f;
242       }
243       return TRUE;
244     }
245   }
246 
247   /* should not reach this point */
248   return FALSE;
249 }
250 
251 /*
252  * GPX XML parser code
253  */
_gpx_parser_start_element(GMarkupParseContext * ctx,const gchar * element_name,const gchar ** attribute_names,const gchar ** attribute_values,gpointer user_data,GError ** error)254 void _gpx_parser_start_element(GMarkupParseContext *ctx, const gchar *element_name,
255                                const gchar **attribute_names, const gchar **attribute_values,
256                                gpointer user_data, GError **error)
257 {
258   dt_gpx_t *gpx = (dt_gpx_t *)user_data;
259 
260   if(gpx->parsing_trk == FALSE)
261   {
262     // we only parse tracks and its points, nothing else
263     if(strcmp(element_name, "trk") == 0)
264     {
265       gpx->parsing_trk = TRUE;
266     }
267     goto end;
268   }
269 
270   /* from here on, parse wpType data from track points */
271   if(strcmp(element_name, "trkpt") == 0)
272   {
273     if(gpx->current_track_point)
274     {
275       fprintf(stderr, "broken gpx file, new trkpt element before the previous ended.\n");
276       g_free(gpx->current_track_point);
277     }
278 
279     const gchar **attribute_name = attribute_names;
280     const gchar **attribute_value = attribute_values;
281 
282     gpx->invalid_track_point = FALSE;
283 
284     if(*attribute_name)
285     {
286       gpx->current_track_point = g_malloc0(sizeof(dt_gpx_track_point_t));
287       gpx->current_track_point->segid = gpx->segid;
288 
289       /* initialize with NAN for validation check */
290       gpx->current_track_point->longitude = NAN;
291       gpx->current_track_point->latitude = NAN;
292       gpx->current_track_point->elevation = NAN;
293 
294       /* go thru the attributes to find and get values of lon / lat*/
295       while(*attribute_name)
296       {
297         if(strcmp(*attribute_name, "lon") == 0)
298           gpx->current_track_point->longitude = g_ascii_strtod(*attribute_value, NULL);
299         else if(strcmp(*attribute_name, "lat") == 0)
300           gpx->current_track_point->latitude = g_ascii_strtod(*attribute_value, NULL);
301 
302         attribute_name++;
303         attribute_value++;
304       }
305 
306       /* validate that we actually got lon / lat attribute values */
307       if(isnan(gpx->current_track_point->longitude) || isnan(gpx->current_track_point->latitude))
308       {
309         fprintf(stderr, "broken gpx file, failed to get lon/lat attribute values for trkpt\n");
310         gpx->invalid_track_point = TRUE;
311       }
312     }
313     else
314       fprintf(stderr, "broken gpx file, trkpt element doesn't have lon/lat attributes\n");
315 
316     gpx->current_parser_element = GPX_PARSER_ELEMENT_TRKPT;
317   }
318   else if(strcmp(element_name, "time") == 0)
319   {
320     if(!gpx->current_track_point) goto element_error;
321 
322     gpx->current_parser_element = GPX_PARSER_ELEMENT_TIME;
323   }
324   else if(strcmp(element_name, "ele") == 0)
325   {
326     if(!gpx->current_track_point) goto element_error;
327 
328     gpx->current_parser_element = GPX_PARSER_ELEMENT_ELE;
329   }
330   else if(strcmp(element_name, "name") == 0)
331   {
332     gpx->current_parser_element = GPX_PARSER_ELEMENT_NAME;
333   }
334   else if(strcmp(element_name, "trkseg") == 0)
335   {
336     dt_gpx_track_segment_t *ts = g_malloc0(sizeof(dt_gpx_track_segment_t));
337     ts->name = gpx->seg_name;
338     ts->id = gpx->segid;
339     gpx->seg_name = NULL;
340     gpx->trksegs = g_list_prepend(gpx->trksegs, ts);
341   }
342 
343 end:
344 
345   return;
346 
347 element_error:
348   fprintf(stderr, "broken gpx file, element '%s' found outside of trkpt.\n", element_name);
349 }
350 
_gpx_parser_end_element(GMarkupParseContext * context,const gchar * element_name,gpointer user_data,GError ** error)351 void _gpx_parser_end_element(GMarkupParseContext *context, const gchar *element_name, gpointer user_data,
352                              GError **error)
353 {
354   dt_gpx_t *gpx = (dt_gpx_t *)user_data;
355 
356   /* closing trackpoint lets take care of data parsed */
357   if(gpx->parsing_trk == TRUE)
358   {
359     if(strcmp(element_name, "trk") == 0)
360     {
361       gpx->parsing_trk = FALSE;
362     }
363     else if(strcmp(element_name, "trkpt") == 0)
364     {
365       if(!gpx->invalid_track_point)
366         gpx->trkpts = g_list_prepend(gpx->trkpts, gpx->current_track_point);
367       else
368         g_free(gpx->current_track_point);
369 
370       gpx->current_track_point = NULL;
371     }
372     else if(strcmp(element_name, "trkseg") == 0)
373     {
374       gpx->segid++;
375     }
376 
377     /* clear current parser element */
378     gpx->current_parser_element = GPX_PARSER_ELEMENT_NONE;
379   }
380 }
381 
_gpx_parser_text(GMarkupParseContext * context,const gchar * text,gsize text_len,gpointer user_data,GError ** error)382 void _gpx_parser_text(GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data,
383                       GError **error)
384 {
385   dt_gpx_t *gpx = (dt_gpx_t *)user_data;
386 
387   if(gpx->current_parser_element == GPX_PARSER_ELEMENT_NAME)
388   {
389     if(gpx->seg_name) g_free(gpx->seg_name);
390     gpx->seg_name =  g_strdup(text);
391   }
392 
393   if(!gpx->current_track_point) return;
394 
395   if(gpx->current_parser_element == GPX_PARSER_ELEMENT_TIME)
396   {
397     gpx->current_track_point->time = g_date_time_new_from_iso8601(text, NULL);
398     if(!gpx->current_track_point->time)
399     {
400       gpx->invalid_track_point = TRUE;
401       fprintf(stderr, "broken gpx file, failed to pars is8601 time '%s' for trackpoint\n", text);
402     }
403     dt_gpx_track_segment_t *ts = (dt_gpx_track_segment_t *)gpx->trksegs->data;
404     if(ts)
405     {
406       ts->nb_trkpt++;
407       if(!ts->start_dt)
408       {
409         ts->start_dt = gpx->current_track_point->time;
410         ts->trkpt = gpx->current_track_point;
411       }
412       ts->end_dt = gpx->current_track_point->time;
413     }
414   }
415   else if(gpx->current_parser_element == GPX_PARSER_ELEMENT_ELE)
416     gpx->current_track_point->elevation = g_ascii_strtod(text, NULL);
417 }
418 
dt_gpx_get_trkseg(struct dt_gpx_t * gpx)419 GList *dt_gpx_get_trkseg(struct dt_gpx_t *gpx)
420 {
421   return gpx->trksegs;
422 }
423 
dt_gpx_get_trkpts(struct dt_gpx_t * gpx,const guint segid)424 GList *dt_gpx_get_trkpts(struct dt_gpx_t *gpx, const guint segid)
425 {
426   GList *pts = NULL;
427   GList *ts = g_list_nth(gpx->trksegs, segid);
428   if(!ts) return pts;
429   dt_gpx_track_segment_t *tsd = (dt_gpx_track_segment_t *)ts->data;
430   GList *tps = g_list_find(gpx->trkpts, tsd->trkpt);
431   if(!tps) return pts;
432   for(GList *tp = tps; tp; tp = g_list_next(tp))
433   {
434     dt_gpx_track_point_t *tpd = (dt_gpx_track_point_t *)tp->data;
435     if(tpd->segid != segid) return pts;
436     dt_geo_map_display_point_t *p = g_malloc0(sizeof(dt_geo_map_display_point_t));
437     p->lat = tpd->latitude;
438     p->lon = tpd->longitude;
439     pts = g_list_prepend(pts, p);
440   }
441   return pts;
442 }
443 
444 /* --------------------------------------------------------------------------
445  * Geodesic interpolation functions
446  * ------------------------------------------------------------------------*/
447 
dt_gpx_geodesic_distance(double lat1,double lon1,double lat2,double lon2,double * d,double * delta)448 void dt_gpx_geodesic_distance(double lat1, double lon1,
449                               double lat2, double lon2,
450                               double *d, double *delta)
451 {
452   const double lat_rad_1 = lat1 * M_PI / 180;
453   const double lat_rad_2 = lat2 * M_PI / 180;
454   const double lon_rad_1 = lon1 * M_PI / 180;
455   const double lon_rad_2 = lon2 * M_PI / 180;
456   const double delta_lat_rad = lat_rad_2 - lat_rad_1;
457   const double delta_lon_rad = lon_rad_2 - lon_rad_1;
458   const double sin_delta_lat_rad = sin(delta_lat_rad / 2);
459   const double sin_delta_lon_rad = sin(delta_lon_rad / 2);
460 
461   const double a = sin_delta_lat_rad * sin_delta_lat_rad +
462                    cos(lat_rad_1) * cos(lat_rad_2) *
463                    sin_delta_lon_rad * sin_delta_lon_rad;
464   *delta = 2 * atan2(sqrt(a), sqrt(1 - a)); /* angular distance between the points in radians */
465 
466   *d = *delta * EARTH_RADIUS;               /* distance on the surface in metres */
467 }
468 
dt_gpx_geodesic_intermediate_point(const double lat1,const double lon1,const double lat2,const double lon2,const double delta,const gboolean first_time,double f,double * lat,double * lon)469 void dt_gpx_geodesic_intermediate_point(const double lat1, const double lon1,
470                                         const double lat2, const double lon2,
471                                         const double delta,
472                                         const gboolean first_time,
473                                         double f,
474                                         double *lat, double *lon)
475 {
476   static double lat_rad_1;
477   static double sin_lat_rad_1;
478   static double cos_lat_rad_1;
479   static double lat_rad_2;
480   static double sin_lat_rad_2;
481   static double cos_lat_rad_2;
482   static double lon_rad_1;
483   static double sin_lon_rad_1;
484   static double cos_lon_rad_1;
485   static double lon_rad_2;
486   static double sin_lon_rad_2;
487   static double cos_lon_rad_2;
488   static double sin_delta;
489 
490   if(first_time)
491   {
492     lat_rad_1 = lat1 * M_PI / 180;
493     sin_lat_rad_1 = sin(lat_rad_1);
494     cos_lat_rad_1 = cos(lat_rad_1);
495     lat_rad_2 = lat2 * M_PI / 180;
496     sin_lat_rad_2 = sin(lat_rad_2);
497     cos_lat_rad_2 = cos(lat_rad_2);
498     lon_rad_1 = lon1 * M_PI / 180;
499     sin_lon_rad_1 = sin(lon_rad_1);
500     cos_lon_rad_1 = cos(lon_rad_1);
501     lon_rad_2 = lon2 * M_PI / 180;
502     sin_lon_rad_2 = sin(lon_rad_2);
503     cos_lon_rad_2 = cos(lon_rad_2);
504     sin_delta = sin(delta);
505   }
506 
507   const double a = sin((1 - f) * delta) / sin_delta;
508   const double b = sin(f * delta) / sin_delta;
509   const double x = a * cos_lat_rad_1 * cos_lon_rad_1 + b * cos_lat_rad_2 * cos_lon_rad_2;
510   const double y = a * cos_lat_rad_1 * sin_lon_rad_1 + b * cos_lat_rad_2 * sin_lon_rad_2;
511   const double z = a * sin_lat_rad_1 + b * sin_lat_rad_2;
512   const double lat_rad = atan2(z, sqrt(x * x + y * y)); /* latitude of intermediate point in radians */
513   const double lon_rad = atan2(y, x);                   /* longitude of intermediate point in radians */
514 
515   *lat = lat_rad / M_PI * 180;
516   *lon = lon_rad / M_PI * 180;
517 }
518 /* -------- end of Geodesic interpolation functions -----------------------*/
519 
520 
521 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
522 // vim: shiftwidth=2 expandtab tabstop=2 cindent
523 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
524