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