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