Fix build with GTK 3
[aweather] / src / plugins / gps-plugin.c
1 /*
2  * Copyright (C) 2012 Adam Boggs <boggs@aircrafter.org>
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 /* TODO:
19  *    If gpsd connection fails, try to connect again periodically.
20  *    If gps stops sending data there should be an indication that it's stale.
21  */
22
23 #define _XOPEN_SOURCE
24 #include <config.h>
25
26 #include <stdio.h>
27 #include <fcntl.h>
28 #include <errno.h>
29 #include <time.h>
30 #include <string.h>
31 #include <glib/gstdio.h>
32 #include <gtk/gtk.h>
33 #include <gio/gio.h>
34 #include <math.h>
35
36 #include <grits.h>
37 #include <gps.h>
38
39 #include "gps-plugin.h"
40 #include "level2.h"
41 #include "../aweather-location.h"
42
43 #include "compat.h"
44
45 /* interval to update map with new gps data in seconds. */
46 #define GPS_UPDATE_INTERVAL     (2)
47
48 /* Filename and search path to use for gps marker, should be configurable */
49 #define GPS_MARKER_ICON_PATH    ".:" PKGDATADIR
50 #define GPS_MARKER_ICON         "arrow.png"
51
52 /* number of track points per group and number of groups to maintain */
53 #define GPS_TRACK_POINTS        (256)
54 #define GPS_TRACK_GROUPS        (16)
55 #define GPS_TRACK_POINTS_FACTOR (1.5)
56
57 /* interval to update log file in seconds (default value for slider) */
58 #define GPS_LOG_DEFAULT_UPDATE_INTERVAL (30)
59 #define GPS_LOG_EXT             "csv"
60
61 /* For updating the status bar conveniently */
62 #define GPS_STATUSBAR_CONTEXT   "GPS"
63
64 #if 0
65 #define GPS_STATUS(gps, format, args...) \
66         do { \
67                 gchar *buf = g_strdup_printf(format, ##args); \
68                 gtk_statusbar_push(GTK_STATUSBAR(gps->status_bar), \
69                     gtk_statusbar_get_context_id( \
70                       GTK_STATUSBAR(gps->status_bar), \
71                       GPS_STATUSBAR_CONTEXT), \
72                 buf); \
73         } while (0)
74 #endif
75
76 #define GPS_STATUS(gps, format, args...) \
77         do { \
78                 gchar *buf = g_strdup_printf(format, ##args); \
79                 g_debug("STATUS: %s", buf); \
80         } while (0)
81
82
83 /********************
84  * Helper functions *
85  ********************/
86
87 /* Find a readable file in a colon delimeted path */
88 static gchar *find_path(const gchar *path, const gchar *filename)
89 {
90         gchar *end_ptr, *fullpath;
91
92         end_ptr = (gchar *)path;
93
94         /* find first : */
95         while (*end_ptr != ':' && *end_ptr != '\0')
96                 end_ptr++;
97         fullpath = g_strdup_printf("%.*s/%s", (int)(end_ptr-path), path,
98                                filename);
99         g_debug("GritsPluginGps: find_path - searching %s", fullpath);
100         if (access(fullpath, R_OK) == 0) {
101                 g_debug("GritsPluginGps: find_path - found %s", fullpath);
102                 return fullpath;        /* caller frees */
103         }
104
105         g_free(fullpath);
106         /* recurse */
107         if (*end_ptr == '\0') {
108                 return NULL;
109         } else {
110                 return find_path(end_ptr + 1, filename);
111         }
112 }
113
114
115 /********************
116  * GPS Status Table *
117  ********************/
118
119 static gchar *gps_get_status(struct gps_data_t *gps_data)
120 {
121         gchar *status_color;
122         gchar *status_text;
123
124         switch (gps_data->fix.mode) {
125         case MODE_NOT_SEEN:
126                 status_color = "red";
127                 status_text = "No Signal";
128                 break;
129         case MODE_NO_FIX:
130                 status_color = "red";
131                 status_text = "No Fix";
132                 break;
133         case MODE_2D:
134                 status_color = "yellow";
135                 status_text = "2D Mode";
136                 break;
137         case MODE_3D:
138                 status_color = "green";
139                 status_text = "3D Mode";
140                 break;
141         default:
142                 status_color = "black";
143                 status_text = "Unknown";
144                 break;
145         }
146         return g_strdup_printf("<span foreground=\"%s\">%s</span>",
147                                 status_color, status_text);
148 }
149
150 #if 0
151 static gchar *gps_get_online(struct gps_data_t *gps_data)
152 {
153         gchar *status_str;
154         gchar *online_str;
155
156         if (gps_data->online == -1.0) {
157                 online_str = "Offline";
158         } else {
159                 online_str = "Online";
160         }
161
162         switch (gps_data->status) {
163         case 0:
164                 status_str = "No Fix";
165                 break;
166         case 1:
167                 status_str = "Fix Acquired";
168                 break;
169         case 2:
170                 status_str = "DGPS Fix";
171                 break;
172         default:
173                 status_str = "Unknown Status";
174                 break;
175         }
176
177         return g_strdup_printf("%lf,%s,%s", gps_data->online, online_str, status_str);
178 }
179 #endif
180
181 static gchar *gps_get_latitude(struct gps_data_t *gps_data)
182 {
183         return g_strdup_printf("%10.4f", gps_data->fix.latitude);
184 }
185
186 static gchar *gps_get_longitude(struct gps_data_t *gps_data)
187 {
188         return g_strdup_printf("%10.4f", gps_data->fix.longitude);
189 }
190
191 static gchar *gps_get_elevation(struct gps_data_t *gps_data)
192 {
193         /* XXX Make units (m/ft) settable */
194         return g_strdup_printf("%5.0lf %4s",
195                     (gps_data->fix.altitude * METERS_TO_FEET), "ft");
196 }
197
198 static gchar *gps_get_heading(struct gps_data_t *gps_data)
199 {
200         /* XXX Make units (m/ft) settable */
201         return g_strdup_printf("%5.0lf %4s", gps_data->fix.track, "deg");
202 }
203
204 static gchar *gps_get_speed(struct gps_data_t *gps_data)
205 {
206         /* XXX Make units (m/ft) settable */
207         return g_strdup_printf("%5.0f %4s",
208                 (gps_data->fix.speed*3600.0*METERS_TO_FEET/5280.0), "mph");
209 }
210
211 struct {
212         const gchar *label;
213         const gchar *initial_val;
214         gchar     *(*get_data)(struct gps_data_t *);
215         guint        font_size;
216         GtkWidget   *label_widget;
217         GtkWidget   *value_widget;
218 } gps_table[] = {
219         {"Status:",    "No Data", gps_get_status,    14, NULL, NULL},
220 //      {"Online:",    "No Data", gps_get_online,    14, NULL, NULL},
221         {"Latitude:",  "No Data", gps_get_latitude,  14, NULL, NULL},
222         {"Longitude:", "No Data", gps_get_longitude, 14, NULL, NULL},
223         {"Elevation:", "No Data", gps_get_elevation, 14, NULL, NULL},
224         {"Heading:",   "No Data", gps_get_heading,   14, NULL, NULL},
225         {"Speed:",     "No Data", gps_get_speed,     14, NULL, NULL},
226 };
227
228
229 /******************
230  * Track handling *
231  ******************/
232
233 static void gps_track_init(GpsTrack *track)
234 {
235         /* Save a spot at the end for the NULL termination */
236         track->points = (gpointer)g_new0(double*, GPS_TRACK_GROUPS + 1);
237         track->cur_point  = 0;
238         track->cur_group  = 0;
239         track->num_points = 1;  /* starts at 1 so realloc logic works */
240         track->line = NULL;
241 }
242
243 static void gps_track_clear(GpsTrack *track)
244 {
245         gint pi;
246         for (pi = 0; pi < GPS_TRACK_GROUPS; pi++) {
247                 if (track->points[pi] != NULL) {
248                         g_free(track->points[pi]);
249                         track->points[pi] = NULL;
250                 }
251         }
252         track->cur_point  = 0;
253         track->cur_group  = 0;
254         track->num_points = 1;  /* starts at 1 so realloc logic works */
255 }
256
257 static void gps_track_free(GpsTrack *track)
258 {
259         gps_track_clear(track);
260         g_free(track->points);
261 }
262
263 /* add a new track group (points in a track group are connected, and
264  * separated from points in other track groups).  */
265 static void gps_track_group_incr(GpsTrack *track)
266 {
267         gdouble (**points)[3] = track->points; /* for simplicity */
268
269         /* Just return if they increment it again before any points have
270          * been added.
271          */
272         if (points[track->cur_group] == NULL) {
273                 return;
274         }
275
276         g_debug("GritsPluginGps: track_group_incr - track group %u->%u.",
277                 track->cur_group, track->cur_group + 1);
278
279         track->cur_group++;
280         track->cur_point  = 0;
281         track->num_points = 1;  /* starts at 1 so realloc logic works */
282
283         if (track->cur_group >= GPS_TRACK_GROUPS) {
284                 g_debug("GritsPluginGps: track_group_incr - track group %u "
285                         "is at max %u, shifting groups",
286                         track->cur_group, GPS_TRACK_GROUPS);
287
288                 /* Free the oldest one which falls off the end */
289                 g_free(points[0]);
290
291                 /* shift the rest down, last one should be NULL already */
292                 /* note we alloc GPS_TRACK_GROUPS+1 */
293                 for (int pi = 0; pi < GPS_TRACK_GROUPS; pi++) {
294                         points[pi] = points[pi+1];
295                 }
296
297                 /* always write into the last group */
298                 track->cur_group = GPS_TRACK_GROUPS - 1;
299         }
300 }
301
302 static void gps_track_add_point(GpsTrack *track,
303     gdouble lat, gdouble lon, gdouble elevation)
304 {
305         gdouble (**points)[3] = track->points; /* for simplicity */
306
307         g_debug("GritsPluginGps: track_add_point");
308
309         g_assert(track->cur_group < GPS_TRACK_GROUPS &&
310                 (track->cur_point <= track->num_points));
311
312         /* resize/allocate the point group if the current one is full */
313         if (track->cur_point >= track->num_points - 1) {
314                 guint new_size = track->num_points == 1 ?
315                             GPS_TRACK_POINTS :
316                             track->num_points * GPS_TRACK_POINTS_FACTOR;
317                 g_debug("GritsPluginGps: track_add_point - reallocating points "
318                         "array from %u points to %u points.\n",
319                         track->num_points, new_size);
320                 points[track->cur_group] = (gpointer)g_renew(gdouble,
321                             points[track->cur_group], 3*(new_size+1));
322                 track->num_points = new_size;
323         }
324
325         g_assert(points[track->cur_group] != NULL);
326
327         /* Add the coordinate */
328         lle2xyz(lat, lon, elevation,
329             &points[track->cur_group][track->cur_point][0],
330             &points[track->cur_group][track->cur_point][1],
331             &points[track->cur_group][track->cur_point][2]);
332
333         track->cur_point++;
334
335         /* make sure last point is always 0s so the line drawing stops. */
336         points[track->cur_group][track->cur_point][0] = 0.0;
337         points[track->cur_group][track->cur_point][1] = 0.0;
338         points[track->cur_group][track->cur_point][2] = 0.0;
339 }
340
341
342 /*****************
343  * Track Logging *
344  *****************/
345
346 static gchar *gps_get_date_string(double gps_time)
347 {
348         static gchar    buf[256];
349         time_t int_time = (time_t)gps_time;
350         struct tm       tm_time;
351
352         gmtime_r(&int_time, &tm_time);
353
354         snprintf(buf, sizeof(buf), "%04d-%02d-%02d",
355                  tm_time.tm_year+1900, tm_time.tm_mon+1, tm_time.tm_mday);
356
357         return buf;
358 }
359
360 static gchar *gps_get_time_string(time_t gps_time)
361 {
362         static gchar buf[256];
363         time_t int_time = (time_t)gps_time;
364         struct tm tm_time;
365
366         gmtime_r(&int_time, &tm_time);
367
368         snprintf(buf, sizeof(buf), "%02d:%02d:%02dZ",
369                  tm_time.tm_hour, tm_time.tm_min, tm_time.tm_sec);
370
371         return buf;
372 }
373
374 static gboolean gps_write_log(gpointer data)
375 {
376         GritsPluginGps *gps = (GritsPluginGps *)data;
377         struct gps_data_t *gps_data = &gps->gps_data;
378         gchar buf[256];
379         gchar filename[256];
380         gint fd;
381         gboolean new_file = FALSE;
382
383         if (gps_data == NULL) {
384                 g_warning("Skipped write to GPS log file: "
385                           "can not get GPS coordinates.");
386                 GPS_STATUS(gps, "Skipped write to GPS log file: "
387                           "can not get GPS coordinates.");
388                 return TRUE;
389         }
390
391         /* get filename from text entry box.  If empty, generate a name from
392          * the date and time and set it. */
393         if (strlen(gtk_entry_get_text(
394                       GTK_ENTRY(gps->ui.gps_log_filename_entry))) == 0) {
395                 snprintf(filename, sizeof(filename),
396                             "%sT%s.%s",
397                             gps_get_date_string(gps->gps_data.fix.time),
398                             gps_get_time_string(gps->gps_data.fix.time),
399                             GPS_LOG_EXT);
400                 gtk_entry_set_text(GTK_ENTRY(gps->ui.gps_log_filename_entry),
401                             filename);
402         }
403
404         strncpy(filename,
405             gtk_entry_get_text(GTK_ENTRY(gps->ui.gps_log_filename_entry)),
406             sizeof(filename));
407
408         if (!g_file_test(filename, G_FILE_TEST_EXISTS)) {
409                 new_file = TRUE;
410         }
411
412         if ((fd = open(filename, O_CREAT|O_APPEND|O_WRONLY, 0644)) == -1) {
413                 g_warning("Error opening log file %s: %s",
414                                 filename, strerror(errno));
415                 return FALSE;
416         }
417
418         if (new_file) {
419                 /* write header and reset record counter */
420                 snprintf(buf, sizeof(buf),
421                         "No,Date,Time,Lat,Lon,Ele,Head,Speed,RTR\n");
422                 if (write(fd, buf, strlen(buf)) == -1) {
423                     g_warning("Error writing header to log file %s: %s",
424                                     filename, strerror(errno));
425                 }
426                 gps->ui.gps_log_number = 1;
427         }
428
429         /* Write log entry.  Make sure this matches the header */
430         /* "No,Date,Time,Lat,Lon,Ele,Head,Speed,Fix,RTR\n" */
431         /* RTR values: T=time, B=button push, S=speed, D=distance */
432         snprintf(buf, sizeof(buf), "%d,%s,%s,%lf,%lf,%lf,%lf,%lf,%c\n",
433                         gps->ui.gps_log_number++,
434                         gps_get_date_string(gps->gps_data.fix.time),
435                         gps_get_time_string(gps->gps_data.fix.time),
436                         //gps_data->fix.time,
437                         gps_data->fix.latitude,
438                         gps_data->fix.longitude,
439                         gps_data->fix.altitude * METERS_TO_FEET,
440                         gps_data->fix.track,
441                         gps_data->fix.speed * METERS_TO_FEET,
442                         'T'); /* position due to timer expired  */
443
444         if (write(fd, buf, strlen(buf)) == -1) {
445                 g_warning("Could not write log number %d to log file %s: %s",
446                                 gps->ui.gps_log_number-1, filename, strerror(errno));
447         }
448         close(fd);
449
450         GPS_STATUS(gps, "Updated GPS log file %s.", filename);
451
452         /* reschedule */
453         return TRUE;
454 }
455
456
457 /****************
458  * Main drawing *
459  ****************/
460
461 static gboolean gps_data_is_valid(struct gps_data_t *gps_data)
462 {
463         if (gps_data != NULL && gps_data->online != -1.0 &&
464                 gps_data->fix.mode >= MODE_2D &&
465                 gps_data->status > STATUS_NO_FIX) {
466                 return TRUE;
467         }
468
469         return FALSE;
470 }
471
472 static void gps_update_status(GritsPluginGps *gps)
473 {
474         struct gps_data_t *gps_data = &gps->gps_data;
475
476         /* gps table update */
477         for (gint i = 0; i < G_N_ELEMENTS(gps_table); i++) {
478                 gchar *str = gps_table[i].get_data(gps_data);
479                 gtk_label_set_markup(GTK_LABEL(gps_table[i].value_widget), str);
480                 g_free(str);
481         }
482 }
483
484 /* external interface to update UI from latest GPS data. */
485 gboolean gps_redraw_all(gpointer data)
486 {
487         GritsPluginGps *gps = (GritsPluginGps *)data;
488         g_assert(gps);
489
490         struct gps_data_t *gps_data = &gps->gps_data;
491
492         g_debug("GritsPluginGps: redraw_all");
493
494         g_assert(gps_data);
495         if (!gps_data_is_valid(gps_data)) {
496                 g_debug("GritsPluginGps: redraw_all - gps_data is not valid.");
497                 /* XXX Change marker to indicate data is not valid */
498                 return TRUE;
499         }
500
501         /* update position labels */
502         gps_update_status(gps);
503
504         /* Update track and marker position */
505         if (gps_data_is_valid(gps_data) && gps->track.active) {
506                 g_debug("GritsPluginGps: redraw_all - updating track group %u "
507                         "point %u at lat = %f, long = %f, track = %f",
508                         gps->track.cur_group,
509                         gps->track.cur_point,
510                         gps_data->fix.latitude,
511                         gps_data->fix.longitude,
512                         gps_data->fix.track);
513
514                 gps_track_add_point(&gps->track,
515                           gps_data->fix.latitude, gps_data->fix.longitude, 0.0);
516
517                 if (gps->track.line) {
518                         grits_viewer_remove(gps->viewer,
519                             GRITS_OBJECT(gps->track.line));
520                         gps->track.line = NULL;
521                 }
522
523                 gps->track.line = grits_line_new(gps->track.points);
524                 gps->track.line->color[0]  = 1.0;
525                 gps->track.line->color[1]  = 0;
526                 gps->track.line->color[2]  = 0.1;
527                 gps->track.line->color[3]  = 1.0;
528                 gps->track.line->width     = 3;
529
530                 grits_viewer_add(gps->viewer, GRITS_OBJECT(gps->track.line),
531                             GRITS_LEVEL_OVERLAY, FALSE);
532                 grits_object_queue_draw(GRITS_OBJECT(gps->track.line));
533         }
534
535         if (gps_data_is_valid(gps_data)) {
536                 if (gps->marker) {
537                         grits_viewer_remove(gps->viewer,
538                             GRITS_OBJECT(gps->marker));
539                         gps->marker = NULL;
540                 }
541
542                 gchar *path = find_path(GPS_MARKER_ICON_PATH, GPS_MARKER_ICON);
543                 if (path) {
544                         gps->marker = grits_marker_icon_new("GPS", path,
545                                       gps_data->fix.track, TRUE, GRITS_MARKER_DMASK_ICON);
546                         g_free(path);
547                 } else {
548                         /* if icon not found just use a point */
549                         g_warning("Could not find GPS marker icon %s in path %s.",
550                                   GPS_MARKER_ICON, GPS_MARKER_ICON_PATH);
551                         gps->marker = grits_marker_icon_new("GPS", NULL,
552                                       gps_data->fix.track, FALSE,
553                                       GRITS_MARKER_DMASK_POINT |
554                                       GRITS_MARKER_DMASK_LABEL);
555                 }
556
557                 GRITS_OBJECT(gps->marker)->center.lat  = gps_data->fix.latitude;
558                 GRITS_OBJECT(gps->marker)->center.lon  = gps_data->fix.longitude;
559                 GRITS_OBJECT(gps->marker)->center.elev = 0.0;
560                 GRITS_OBJECT(gps->marker)->lod         = EARTH_R * 5;
561                 GRITS_MARKER(gps->marker)->ortho       = FALSE;
562
563                 grits_viewer_add(gps->viewer, GRITS_OBJECT(gps->marker),
564                         GRITS_LEVEL_OVERLAY+1, FALSE);
565                 grits_object_queue_draw(GRITS_OBJECT(gps->marker));
566         }
567
568         if (gps->follow_gps && gps_data_is_valid(gps_data)) {
569                 /* Center map at current GPS position. */
570                 g_debug("GritsPluginGps: redraw_all - centering map at "
571                         "lat = %f, long = %f, track = %f",
572                             gps_data->fix.latitude,
573                             gps_data->fix.longitude,
574                             gps_data->fix.track);
575
576                 double lat, lon, elev;
577                 grits_viewer_get_location(gps->viewer, &lat, &lon, &elev);
578                 grits_viewer_set_location(gps->viewer, gps_data->fix.latitude,
579                                           gps_data->fix.longitude, elev);
580                 //grits_viewer_set_rotation(gps->viewer, 0, 0, 0);
581         }
582
583         /* reschedule */
584         return TRUE;
585 }
586
587
588 /***************
589  * Config Area *
590  ***************/
591
592 /* GPS Data Frame */
593 static void gps_init_status_info(GritsPluginGps *gps, GtkWidget *gbox)
594 {
595         gps->ui.gps_status_frame = gtk_frame_new("<b>GPS Data</b>");
596         GtkWidget *label = gtk_frame_get_label_widget(
597                            GTK_FRAME(gps->ui.gps_status_frame));
598         gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
599         gtk_frame_set_shadow_type(GTK_FRAME(gps->ui.gps_status_frame),
600                            GTK_SHADOW_NONE);
601         gps->ui.gps_status_table = gtk_table_new(5, 2, FALSE);
602         gtk_container_add(GTK_CONTAINER(gps->ui.gps_status_frame),
603                    gps->ui.gps_status_table);
604
605
606         /* gps data table setup */
607         gint i;
608         for (i = 0; i < G_N_ELEMENTS(gps_table); i++) {
609                 gps_table[i].label_widget = gtk_label_new(gps_table[i].label);
610                 gps_table[i].value_widget = gtk_label_new(gps_table[i].initial_val);
611
612                 gtk_misc_set_alignment(GTK_MISC(gps_table[i].label_widget), 0, 0);
613                 gtk_misc_set_alignment(GTK_MISC(gps_table[i].value_widget), 1, 0);
614
615                 gtk_table_attach(GTK_TABLE(gps->ui.gps_status_table),
616                                 gps_table[i].label_widget, 0, 1, i, i+1,
617                                 GTK_EXPAND|GTK_FILL, GTK_EXPAND|GTK_FILL, 0, 0);
618                 gtk_table_attach(GTK_TABLE(gps->ui.gps_status_table),
619                                 gps_table[i].value_widget, 1, 2, i, i+1,
620                                 GTK_EXPAND|GTK_FILL, GTK_EXPAND|GTK_FILL, 0, 0);
621
622                 PangoFontDescription *roman = pango_font_description_new();
623                 PangoFontDescription *mono  = pango_font_description_from_string("monospace");
624                 pango_font_description_set_size(roman, gps_table[i].font_size*PANGO_SCALE);
625                 pango_font_description_set_size(mono,  gps_table[i].font_size*PANGO_SCALE);
626                 gtk_widget_modify_font(gps_table[i].label_widget, roman);
627                 gtk_widget_modify_font(gps_table[i].value_widget, mono);
628                 pango_font_description_free(roman);
629                 pango_font_description_free(mono);
630         }
631         gtk_box_pack_start(GTK_BOX(gbox), gps->ui.gps_status_frame,
632                             FALSE, FALSE, 0);
633
634         /* Start UI refresh task, which will reschedule itgps. */
635         gps_redraw_all(gps);
636         gps->gps_update_timeout_id = g_timeout_add(
637                     GPS_UPDATE_INTERVAL*1000,
638                     gps_redraw_all, gps);
639
640 }
641
642 /* GPS Control Frame */
643 static gboolean on_gps_follow_clicked_event(GtkWidget *widget, gpointer user_data)
644 {
645         GritsPluginGps *gps = (GritsPluginGps *)user_data;
646
647         g_debug("GritsPluginGps: follow_clicked_event - button status %d",
648                 gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget)));
649         if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget))) {
650                 gps->follow_gps = TRUE;
651         } else {
652                 gps->follow_gps = FALSE;
653         }
654
655         return FALSE;
656 }
657
658 static gboolean on_gps_track_enable_clicked_event(GtkWidget *widget, gpointer user_data)
659 {
660         GritsPluginGps *gps = (GritsPluginGps *)user_data;
661
662         g_debug("GritsPluginGps: track_enable_clicked_event");
663
664         if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget))) {
665                 /* start logging trip history */
666                 GPS_STATUS(gps, "Enabled GPS track.");
667                 gps->track.active = TRUE;
668         } else {
669                 /* stop logging trip history */
670                 GPS_STATUS(gps, "Disabled GPS track.");
671                 gps->track.active = FALSE;
672                 /* advance to the next track group, moving everything down if
673                  * it's full. */
674                 gps_track_group_incr(&gps->track);
675         }
676
677         return FALSE;
678 }
679
680 static gboolean on_gps_track_clear_clicked_event(GtkWidget *widget, gpointer user_data)
681 {
682         GritsPluginGps *gps = (GritsPluginGps *)user_data;
683
684         g_debug("GritsPluginGps: track_clear_clicked_event");
685         GPS_STATUS(gps, "Cleared GPS track.");
686         gps_track_clear(&gps->track);
687
688         return FALSE;
689 }
690
691 static void gps_init_control_frame(GritsPluginGps *gps, GtkWidget *gbox)
692 {
693         /* Control checkboxes */
694         GtkWidget *gps_control_frame = gtk_frame_new("<b>GPS Control</b>");
695         GtkWidget *label = gtk_frame_get_label_widget(
696                            GTK_FRAME(gps_control_frame));
697         gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
698         gtk_frame_set_shadow_type(GTK_FRAME(gps_control_frame),
699                            GTK_SHADOW_NONE);
700         GtkWidget *cbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
701         gtk_container_add(GTK_CONTAINER(gps_control_frame), cbox);
702         gtk_box_pack_start(GTK_BOX(gbox), gps_control_frame, FALSE, FALSE, 0);
703
704         gps->ui.gps_follow_checkbox =
705                       gtk_check_button_new_with_label("Follow GPS");
706         g_signal_connect(G_OBJECT(gps->ui.gps_follow_checkbox), "clicked",
707                       G_CALLBACK(on_gps_follow_clicked_event),
708                       (gpointer)gps);
709         gtk_box_pack_start(GTK_BOX(cbox), gps->ui.gps_follow_checkbox,
710                        FALSE, FALSE, 0);
711
712         gps->ui.gps_track_checkbox =
713                        gtk_check_button_new_with_label("Record Track");
714         g_signal_connect(G_OBJECT(gps->ui.gps_track_checkbox), "clicked",
715                        G_CALLBACK(on_gps_track_enable_clicked_event),
716                        (gpointer)gps);
717         gtk_box_pack_start(GTK_BOX(cbox), gps->ui.gps_track_checkbox,
718                        FALSE, FALSE, 0);
719
720         gps->ui.gps_clear_button = gtk_button_new_with_label("Clear Track");
721         g_signal_connect(G_OBJECT(gps->ui.gps_clear_button), "clicked",
722                       G_CALLBACK(on_gps_track_clear_clicked_event),
723                       (gpointer)gps);
724         gtk_box_pack_start(GTK_BOX(cbox), gps->ui.gps_clear_button,
725                        FALSE, FALSE, 0);
726 }
727
728 /* Track Log Frame */
729 static gboolean on_gps_log_clicked_event(GtkWidget *widget, gpointer user_data)
730 {
731         GritsPluginGps *gps = (GritsPluginGps *)user_data;
732
733         g_debug("GritsPluginGps: log_clicked_event");
734
735         if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON (widget)))  {
736                 gps_write_log(gps);
737
738                 /* Schedule log file write */
739                 gps->ui.gps_log_timeout_id = g_timeout_add(
740                       gtk_range_get_value(
741                         GTK_RANGE(gps->ui.gps_log_interval_slider))*1000,
742                         gps_write_log, gps);
743         } else {
744                 /* button unchecked */
745                 g_source_remove(gps->ui.gps_log_timeout_id);
746                 gps->ui.gps_log_timeout_id = 0;
747                 g_debug("GritsPluginGps: log_clicked_event - closed log file.");
748         }
749
750         return FALSE;
751 }
752
753 static gboolean on_gps_log_interval_changed_event(GtkWidget *widget, gpointer user_data)
754 {
755         GritsPluginGps *gps = (GritsPluginGps *)user_data;
756
757         g_assert(gps);
758
759         g_debug("GritsPluginGps: log_interval_changed_event - value = %f",
760         gtk_range_get_value(GTK_RANGE(widget)));
761
762         if (gtk_toggle_button_get_active(
763                         GTK_TOGGLE_BUTTON(gps->ui.gps_log_checkbox))) {
764                 g_assert(gps->ui.gps_log_timeout_id != 0);
765
766                 /* disable old timeout */
767                 g_source_remove(gps->ui.gps_log_timeout_id);
768                 gps->ui.gps_log_timeout_id = 0;
769
770                 /* Schedule new log file write */
771                 gps->ui.gps_log_timeout_id = g_timeout_add(
772                          gtk_range_get_value(GTK_RANGE(widget))*1000,
773                          gps_write_log, gps);
774                 gps_write_log(gps);
775         }
776
777         return FALSE;
778 }
779
780 static void gps_init_track_log_frame(GritsPluginGps *gps, GtkWidget *gbox)
781 {
782         /* Track log box with enable checkbox and filename entry */
783         GtkWidget *gps_log_frame = gtk_frame_new("<b>Track Log</b>");
784         GtkWidget *label = gtk_frame_get_label_widget(GTK_FRAME(gps_log_frame));
785         gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
786         gtk_frame_set_shadow_type(GTK_FRAME(gps_log_frame), GTK_SHADOW_NONE);
787         GtkWidget *lbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
788         gtk_container_add(GTK_CONTAINER(gps_log_frame), lbox);
789         gtk_box_pack_start(GTK_BOX(gbox), gps_log_frame,
790                         FALSE, FALSE, 0);
791
792         gps->ui.gps_log_checkbox =
793                gtk_check_button_new_with_label("Log Position to File");
794         g_signal_connect(G_OBJECT(gps->ui.gps_log_checkbox), "clicked",
795                G_CALLBACK(on_gps_log_clicked_event),
796                (gpointer)gps);
797         gtk_box_pack_start(GTK_BOX(lbox), gps->ui.gps_log_checkbox,
798                FALSE, FALSE, 0);
799
800         /* Set up filename entry box */
801         GtkWidget *fbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
802         GtkWidget *filename_label = gtk_label_new("Filename:");
803         gtk_box_pack_start(GTK_BOX(fbox), filename_label, FALSE, FALSE, 0);
804         gps->ui.gps_log_filename_entry = gtk_entry_new();
805         gtk_box_pack_start(GTK_BOX(fbox), gps->ui.gps_log_filename_entry,
806                TRUE, TRUE, 0);
807         gtk_box_pack_start(GTK_BOX(lbox), fbox, FALSE, FALSE, 0);
808
809         /* set up gps log interval slider */
810         GtkWidget *ubox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
811         GtkWidget *interval_label = gtk_label_new("Update Interval:");
812         gtk_box_pack_start(GTK_BOX(ubox), interval_label, FALSE, FALSE, 0);
813         gps->ui.gps_log_interval_slider =
814                     gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, 1.0, 600.0, 30.0);
815         gtk_range_set_value(GTK_RANGE(gps->ui.gps_log_interval_slider),
816                     GPS_LOG_DEFAULT_UPDATE_INTERVAL);
817         g_signal_connect(G_OBJECT(gps->ui.gps_log_interval_slider),
818                     "value-changed",
819                     G_CALLBACK(on_gps_log_interval_changed_event),
820                     (gpointer)gps);
821         gtk_range_set_increments(
822                     GTK_RANGE(gps->ui.gps_log_interval_slider),
823                     10.0 /* step */, 30.0 /* page up/down */);
824 #if ! GTK_CHECK_VERSION(3,0,0)
825         gtk_range_set_update_policy(
826                     GTK_RANGE(gps->ui.gps_log_interval_slider),
827                     GTK_UPDATE_DELAYED);
828 #endif
829         gtk_box_pack_start(GTK_BOX(ubox), gps->ui.gps_log_interval_slider,
830                     TRUE, TRUE, 0);
831         gtk_box_pack_start(GTK_BOX(lbox), ubox, FALSE, FALSE, 0);
832 }
833
834
835 /*******************
836  * GPSD interfaces *
837  *******************/
838
839 static gboolean process_gps(GIOChannel *source, GIOCondition condition, gpointer data)
840 {
841         struct gps_data_t *gps_data = (struct gps_data_t *)data;
842
843         g_debug("GritsPluginGps: process_gps");
844
845         /* Process any data from the gps and call the hook function */
846         if (gps_data != NULL) {
847                 gdouble track = gps_data->fix.track;
848                 gint result = gps_read(gps_data);
849                 if (isnan(gps_data->fix.track))
850                         gps_data->fix.track = track;
851                 g_debug("GritsPluginGps: process_gps - gps_read returned %d, "
852                         "position %f, %f.", result,
853                         gps_data->fix.latitude, gps_data->fix.longitude);
854         } else {
855                 g_warning("GritsPluginGps: process_gps - gps_data == NULL.");
856         }
857         return TRUE;
858 }
859
860 static gint initialize_gpsd(char *server, gchar *port, struct gps_data_t *gps_data)
861 {
862 #if GPSD_API_MAJOR_VERSION < 5
863 #error "GPSD protocol version 5 or greater required."
864 #endif
865         gint result = gps_open(server, port, gps_data);
866         if (result > 0) {
867                 g_warning("Unable to open gpsd connection to %s:%s: %d, %d, %s",
868                                 server, port, result, errno, gps_errstr(errno));
869                 return 0;
870         }
871
872         (void)gps_stream(gps_data, WATCH_ENABLE|WATCH_JSON, NULL);
873         g_debug("GritsPluginGps: initialize_gpsd - gpsd fd %u.", gps_data->gps_fd);
874         GIOChannel *channel = g_io_channel_unix_new(gps_data->gps_fd);
875         gint input_tag = g_io_add_watch(channel, G_IO_IN, process_gps, gps_data);
876         g_io_channel_unref(channel);
877         return input_tag;
878 }
879
880
881 /**********************
882  * GPS Plugin Methods *
883  **********************/
884
885 /* Methods */
886 GritsPluginGps *grits_plugin_gps_new(GritsViewer *viewer, GritsPrefs *prefs)
887 {
888         /* TODO: move to constructor if possible */
889         g_debug("GritsPluginGps: new");
890         GritsPluginGps *gps = g_object_new(GRITS_TYPE_PLUGIN_GPS, NULL);
891         gps->viewer  = g_object_ref(viewer);
892         gps->prefs   = g_object_ref(prefs);
893
894         gps->input_tag = initialize_gpsd("localhost", DEFAULT_GPSD_PORT, &gps->gps_data);
895         gps->follow_gps = FALSE;
896
897         gps_track_init(&gps->track);
898         gps_init_status_info(gps, gps->config);
899         gps_init_control_frame(gps, gps->config);
900         gps_init_track_log_frame(gps, gps->config);
901
902         return gps;
903 }
904
905 static GtkWidget *grits_plugin_gps_get_config(GritsPlugin *_gps)
906 {
907         GritsPluginGps *gps = GRITS_PLUGIN_GPS(_gps);
908         return gps->config;
909 }
910
911 /* GObject code */
912 static void grits_plugin_gps_plugin_init(GritsPluginInterface *iface);
913 G_DEFINE_TYPE_WITH_CODE(GritsPluginGps, grits_plugin_gps, G_TYPE_OBJECT,
914                 G_IMPLEMENT_INTERFACE(GRITS_TYPE_PLUGIN,
915                         grits_plugin_gps_plugin_init));
916
917 static void grits_plugin_gps_plugin_init(GritsPluginInterface *iface)
918 {
919         g_debug("GritsPluginGps: plugin_init");
920         /* Add methods to the interface */
921         iface->get_config = grits_plugin_gps_get_config;
922 }
923
924 static void grits_plugin_gps_init(GritsPluginGps *gps)
925 {
926         g_debug("GritsPluginGps: gps_init");
927
928         gps->config = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 15);
929 }
930
931 static void grits_plugin_gps_dispose(GObject *gobject)
932 {
933         GritsPluginGps *gps = GRITS_PLUGIN_GPS(gobject);
934
935         g_debug("GritsPluginGps: dispose");
936
937         if (gps->gps_update_timeout_id) {
938                 g_source_remove(gps->gps_update_timeout_id);
939                 gps->gps_update_timeout_id = 0;
940         }
941         if (gps->ui.gps_log_timeout_id) {
942                 g_source_remove(gps->ui.gps_log_timeout_id);
943                 gps->ui.gps_log_timeout_id = 0;
944         }
945         if (gps->input_tag) {
946                 g_source_remove(gps->input_tag);
947                 gps->input_tag = 0;
948         }
949         if (gps->viewer) {
950                 GritsViewer *viewer = gps->viewer;
951                 gps->viewer = NULL;
952                 if (gps->marker)
953                         grits_viewer_remove(viewer,
954                                GRITS_OBJECT(gps->marker));
955                 g_object_unref(gps->prefs);
956                 g_object_unref(viewer);
957         }
958
959         /* Drop references */
960         G_OBJECT_CLASS(grits_plugin_gps_parent_class)->dispose(gobject);
961 }
962
963 static void grits_plugin_gps_finalize(GObject *gobject)
964 {
965         GritsPluginGps *gps = GRITS_PLUGIN_GPS(gobject);
966
967         g_debug("GritsPluginGps: finalize");
968
969         /* Free data */
970         gps_track_free(&gps->track);
971         G_OBJECT_CLASS(grits_plugin_gps_parent_class)->finalize(gobject);
972 }
973
974 static void grits_plugin_gps_class_init(GritsPluginGpsClass *klass)
975 {
976         g_debug("GritsPluginGps: class_init");
977         GObjectClass *gobject_class = (GObjectClass*)klass;
978         gobject_class->dispose  = grits_plugin_gps_dispose;
979         gobject_class->finalize = grits_plugin_gps_finalize;
980 }