802d5d336ae6d710dc6eada8812dc8cf6376c6f6
[belltower] / belltower.c
1 /*
2  * belltower
3  * an app to find belltowers under Maemo 5
4  *
5  * Copyright (c) 2009 Thomas Thurman <tthurman@gnome.org>
6  * Released under the GPL
7  */
8
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <glib.h>
13 #include <hildon/hildon.h>
14 #include <gtk/gtk.h>
15 #include <location/location-gps-device.h>
16 #include <location/location-distance-utils.h>
17 #include <dbus/dbus-glib.h>
18
19 #define MAX_FIELDS 50
20 #define MAX_RECENT 5
21 #define CONFIG_GENERAL_GROUP "General"
22 #define CONFIG_BOOKMARK_GROUP "Bookmarks"
23 #define CONFIG_RECENT_GROUP "Recent"
24 #define CONFIG_SEEN_CREDITS_KEY "seen_credits"
25 #define CONFIG_DIRECTORY "/home/user/.config/belltower"
26 #define CONFIG_FILENAME CONFIG_DIRECTORY "/belltower.ini"
27
28 /**
29  * Somewhat arbitrary minimum number of belltowers in
30  * one country for the country to be considered to have
31  * "many" belltowers.
32  */
33 #define MANY_BELLTOWERS 10
34
35 GtkWidget *window;
36 LocationGPSDevice *device;
37 GKeyFile *static_content;
38 GKeyFile *config;
39
40 typedef enum {
41   /** stop scanning the database */
42   FILTER_STOP,
43   /** ignore this one */
44   FILTER_IGNORE,
45   /** add this one to the list */
46   FILTER_ACCEPT
47 } FilterResult;
48
49 /*
50   FIXME:
51   We should really do this by looking at the header row of the table.
52   They might decide to put in new columns some day.
53 */
54 typedef enum {
55   FieldPrimaryKey,
56   FieldNationalGrid,
57   FieldAccRef,
58   FieldSNLat,
59   FieldSNLong,
60   FieldPostcode,
61   FieldTowerBase,
62   FieldCounty,
63   FieldCountry,
64   FieldDiocese,
65   FieldPlace,
66   FieldPlace2,
67   FieldPlaceCL,
68   FieldDedication,
69   FieldBells,
70   FieldWt,
71   FieldApp,
72   FieldNote,
73   FieldHz,
74   FieldDetails,
75   FieldGF,
76   FieldToilet,
77   FieldUR,
78   FieldPDNo,
79   FieldPracticeNight,
80   FieldPSt,
81   FieldPrXF,
82   FieldOvhaulYr,
83   FieldContractor,
84   FieldExtraInfo,
85   FieldWebPage,
86   FieldUpdated,
87   FieldAffiliations,
88   FieldAltName,
89   FieldLat,
90   FieldLong,
91   FieldSimulator
92 } field;
93
94 typedef struct {
95   int serial;
96
97   /* the raw data */
98
99   char* fields[MAX_FIELDS];
100   int n_fields;
101 } tower;
102
103 static void show_towers_from_list (GSList *list, gchar *list_name);
104 static void free_tower_list (GSList *list);
105
106 static void
107 show_message (char *message)
108 {
109   HildonNote* note = HILDON_NOTE
110     (hildon_note_new_information (GTK_WINDOW (window),
111                                   message?message:
112                                   "Some message was supposed to be here."));
113   gtk_dialog_run (GTK_DIALOG (note));
114   gtk_widget_destroy (GTK_WIDGET (note));
115 }
116
117 /**
118  * Loads the content of the static and dynamic data files.
119  * Possibly puts up a warning if we can't load the static file.
120  */
121 static void
122 load_config (void)
123 {
124   static_content = g_key_file_new ();
125
126   if (!g_key_file_load_from_file (static_content,
127                                   "/usr/share/belltower/static.ini",
128                                   G_KEY_FILE_NONE,
129                                   NULL))
130     {
131       show_message ("Could not load static content.  Attempting to continue.");
132     }
133
134   config = g_key_file_new ();
135   /* it doesn't matter if this fails */
136   g_key_file_load_from_file (config,
137                              CONFIG_FILENAME,
138                              G_KEY_FILE_KEEP_COMMENTS,
139                              NULL);
140 }
141
142 /**
143  * Saves the dynamic data file to disk.
144  * Puts up a message if there was any error.
145  */
146 static void
147 save_config (void)
148 {
149   gchar *data;
150
151   g_mkdir_with_parents (CONFIG_DIRECTORY, 0700);
152
153   data = g_key_file_to_data (config, NULL, NULL);
154
155   if (!g_file_set_contents (CONFIG_FILENAME,
156                             data,
157                             -1,
158                             NULL))
159     {
160       show_message ("Could not write config file.");
161     }
162
163   g_free (data);
164 }
165
166 static gint
167 distance_to_tower (tower *details)
168 {
169   char *endptr;
170   double tower_lat;
171   double tower_long;
172   double km_distance;
173   const double km_to_miles = 1.609344;
174
175   tower_lat = strtod(details->fields[FieldLat], &endptr);
176   if (*endptr) return -1;
177   tower_long = strtod(details->fields[FieldLong], &endptr);
178   if (*endptr) return -1;
179
180   km_distance = location_distance_between (device->fix->latitude,
181                                            device->fix->longitude,
182                                            tower_lat,
183                                            tower_long);
184
185   return (int) (km_distance / km_to_miles);
186 }
187
188 static gchar*
189 distance_to_tower_str (tower *details)
190 {
191   int miles = distance_to_tower (details);
192
193   if (miles==-1)
194     {
195       return g_strdup ("unknown");
196     }
197   else
198     {
199       return g_strdup_printf("%dmi", (int) miles);
200     }
201 }
202
203 static void
204 call_dbus (DBusBusType type,
205            char *name,
206            char *path,
207            char *interface,
208            char *method,
209            char *parameter)
210 {
211   DBusGConnection *connection;
212   GError *error = NULL;
213
214   DBusGProxy *proxy;
215
216   connection = dbus_g_bus_get (type,
217                                &error);
218   if (connection == NULL)
219     {
220       show_message (error->message);
221       g_error_free (error);
222       return;
223     }
224
225   proxy = dbus_g_proxy_new_for_name (connection, name, path, interface);
226
227   error = NULL;
228   if (!dbus_g_proxy_call (proxy, method, &error,
229                           G_TYPE_STRING, parameter,
230                           G_TYPE_INVALID,
231                           G_TYPE_INVALID))
232     {
233       show_message (error->message);
234       g_error_free (error);
235     }
236 }
237
238 static void
239 show_browser (gchar *url)
240 {
241   call_dbus (DBUS_BUS_SESSION,
242              "com.nokia.osso_browser",
243              "/com/nokia/osso_browser/request",
244              "com.nokia.osso_browser",
245              "load_url",
246              url);
247 }
248
249 typedef FilterResult (*ParseDoveCallback)(tower *details, gpointer data);
250 typedef void (*ButtonCallback)(void);
251
252 GtkWidget *tower_window, *buttons, *tower_table;
253 HildonAppMenu *menu;
254
255 static void
256 add_table_field (char *name,
257                  char *value)
258 {
259   int row;
260   GtkLabel *label;
261   gchar *str;
262
263   g_object_get(tower_table, "n-rows", &row);
264
265   row++;
266
267   gtk_table_resize (GTK_TABLE (tower_table), row, 2);
268
269   label = GTK_LABEL (gtk_label_new (NULL));
270   str = g_strdup_printf("<b>%s</b>", name);
271   gtk_label_set_markup (label, str);
272   g_free (str);
273   gtk_label_set_justify (label, GTK_JUSTIFY_RIGHT);
274   gtk_table_attach_defaults (GTK_TABLE (tower_table),
275                              GTK_WIDGET (label),
276                              0, 1, row, row+1);
277
278   label = GTK_LABEL (gtk_label_new (value));
279   gtk_label_set_justify (label, GTK_JUSTIFY_LEFT);
280   gtk_table_attach_defaults (GTK_TABLE (tower_table),
281                              GTK_WIDGET (label),
282                              1, 2, row, row+1);
283 }
284
285 static void
286 add_button (char *label,
287             ButtonCallback callback)
288 {
289   GtkWidget *button;
290
291   button = gtk_button_new_with_label (label);
292   g_signal_connect (button, "clicked", G_CALLBACK (callback), NULL);
293   hildon_app_menu_append (menu, GTK_BUTTON (button));
294   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
295                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
296                                         label, NULL);
297   g_signal_connect (button, "clicked", G_CALLBACK (callback), NULL);
298   gtk_box_pack_end (GTK_BOX (buttons), button, FALSE, FALSE, 0);
299 }
300
301
302 char *tower_displayed = NULL;
303 char *tower_website = NULL;
304 char *tower_map = NULL;
305 char *tower_directions = NULL;
306 char *peals_list = NULL;
307
308 #define BUTTON_BOOKMARKED_YES "Remove from bookmarks"
309 #define BUTTON_BOOKMARKED_NO "Bookmark"
310
311 static void
312 bookmark_toggled (GtkButton *button,
313                   gpointer dummy)
314 {
315   if (g_key_file_get_boolean (config,
316                               CONFIG_BOOKMARK_GROUP,
317                               tower_displayed,
318                               NULL))
319     {
320
321       /* it's bookmarked; remove the bookmark */
322
323       if (!g_key_file_remove_key (config,
324                                   CONFIG_BOOKMARK_GROUP,
325                                   tower_displayed,
326                                   NULL))
327         {
328           show_message ("Could not remove bookmark.");
329           return;
330         }
331
332       save_config ();
333       gtk_button_set_label (button,
334                             BUTTON_BOOKMARKED_NO);
335     }
336   else
337     {
338       /* it's not bookmarked; add a bookmark */
339
340       g_key_file_set_boolean (config,
341                               CONFIG_BOOKMARK_GROUP,
342                               tower_displayed,
343                               TRUE);
344
345       save_config ();
346       gtk_button_set_label (button,
347                             BUTTON_BOOKMARKED_YES);
348     }
349 }
350
351 static void
352 show_tower_website (void)
353 {
354   show_browser (tower_website);
355 }
356
357 static void
358 show_tower_map (void)
359 {
360   show_browser (tower_map);
361 }
362
363 static void
364 show_tower_directions (void)
365 {
366   if (tower_directions)
367     {
368       show_browser (tower_directions);
369     }
370   else
371     {
372       show_message ("I don't know where you are!");
373     }
374 }
375
376 static void
377 show_peals_list (void)
378 {
379   show_browser (peals_list);
380 }
381
382 static FilterResult
383 get_countries_cb (tower *details,
384                   gpointer data)
385 {
386   GHashTable *hash = (GHashTable *)data;
387   gpointer value;
388
389   if (details->serial==0)
390     return TRUE; /* header row */
391
392   if (!g_hash_table_lookup_extended (hash,
393                                      details->fields[FieldCountry],
394                                      NULL, &value))
395     {
396       g_hash_table_insert (hash,
397                            g_strdup(details->fields[FieldCountry]),
398                            GINT_TO_POINTER (0));
399     }
400   else
401     {
402       g_hash_table_replace (hash,
403                             g_strdup(details->fields[FieldCountry]),
404                             GINT_TO_POINTER (GPOINTER_TO_INT (value)+1));
405     }
406
407   return FILTER_IGNORE;
408 }
409
410 typedef struct {
411   GHashTable *hash;
412   gchar *country_name;
413 } country_cb_data;
414
415 typedef struct {
416   char *country;
417   char *county;
418 } country_and_county;
419
420 static FilterResult
421 get_counties_cb (tower *details,
422                  gpointer data)
423 {
424   country_cb_data *d = (country_cb_data *)data;
425
426   if (details->serial==0)
427     return FILTER_IGNORE; /* header row */
428
429   if (strcmp(details->fields[FieldCountry], d->country_name)!=0)
430     return FILTER_IGNORE; /* wrong country */
431
432   if (!g_hash_table_lookup_extended (d->hash,
433                                     details->fields[FieldCounty],
434                                      NULL, NULL))
435     {
436       g_hash_table_insert (d->hash,
437                            g_strdup(details->fields[FieldCounty]),
438                            g_strdup (details->fields[FieldCounty]));
439     }
440
441   return FILTER_IGNORE;
442 }
443
444 static FilterResult
445 get_nearby_towers_cb (tower *details,
446                       gpointer data)
447 {
448   if (details->serial==0)
449     return FILTER_IGNORE; /* header row */
450
451   if (distance_to_tower (details) < 50)
452     {
453       return FILTER_ACCEPT;
454     }
455   else
456     {
457       return FILTER_IGNORE;
458     }
459 }
460
461 static FilterResult
462 get_towers_by_county_cb (tower *details,
463                          gpointer data)
464 {
465   country_and_county *cac = (country_and_county *) data;
466
467   if ((!cac->county || strcmp (cac->county, details->fields[FieldCounty])==0) &&
468       (!cac->country || strcmp (cac->country, details->fields[FieldCountry])==0))
469     {
470       return FILTER_ACCEPT;
471     }
472   else
473     {
474       return FILTER_IGNORE;
475     }
476 }
477
478 static FilterResult
479 get_towers_by_search_cb (tower *details,
480                          gpointer data)
481 {
482   char *s = (char *) data;
483
484   if (strcasestr(details->fields[FieldCountry], s) ||
485       strcasestr(details->fields[FieldCounty], s) ||
486       strcasestr(details->fields[FieldDedication], s) ||
487       strcasestr(details->fields[FieldPlace], s))
488     {
489       return FILTER_ACCEPT;
490     }
491   else
492     {
493       return FILTER_IGNORE;
494     }
495 }
496
497 /**
498  * A filter which accepts towers based on whether they
499  * appear in a particular group in the config file.
500  *
501  * \param details  the candidate tower
502  * \param data     pointer to a char* which names the group
503  */
504 static FilterResult
505 get_group_of_towers_cb (tower *details,
506                           gpointer data)
507 {
508   if (g_key_file_has_key (config,
509                           (char*) data,
510                           details->fields[FieldPrimaryKey],
511                           NULL))
512     {
513       return FILTER_ACCEPT;
514     }
515   else
516     {
517       return FILTER_IGNORE;
518     }
519 }
520
521 /**
522  * Removes the oldest entry from the [Recent] group in the config
523  * file until there are only five entries left.  Does not save
524  * the file; you have to do that.
525  */
526 static void
527 remove_old_recent_entries (void)
528 {
529   gint count;
530
531   do
532     {
533       gchar **towers;
534       gint oldest_date = 0;
535       gchar *oldest_tower = NULL;
536       gint i;
537
538       /* It is a bit inefficient to do this every
539        * time we go around the loop.  However, it
540        * makes the code far simpler, and we almost
541        * never go around more than once.
542        */
543       towers = g_key_file_get_keys (config,
544                                     CONFIG_RECENT_GROUP,
545                                     &count,
546                                     NULL);
547
548       if (count <= MAX_RECENT)
549         /* everything's fine */
550         return;
551
552       for (i=0; i<count; i++)
553         {
554           gint date = g_key_file_get_integer (config,
555                                               CONFIG_RECENT_GROUP,
556                                               towers[i],
557                                               NULL);
558
559           if (date==0)
560             continue;
561
562           if (oldest_date==0 ||
563               date < oldest_date)
564             {
565               oldest_tower = towers[i];
566               oldest_date = date;
567             }
568         }
569
570       if (oldest_tower)
571         {
572           g_key_file_remove_key (config,
573                                  CONFIG_RECENT_GROUP,
574                                  oldest_tower,
575                                  NULL);
576           count --;
577         }
578       g_strfreev (towers);
579     }
580   while (count > MAX_RECENT);
581 }
582
583 static FilterResult
584 single_tower_cb (tower *details,
585                  gpointer data)
586 {
587
588   GtkWidget *hbox, *button;
589   gchar *str;
590   gint tenor_weight;
591   gchar *primary_key = (gchar*) data;
592   gchar *miles;
593
594   if (strcmp(details->fields[FieldPrimaryKey], primary_key)!=0)
595     {
596       /* not this one; keep going */
597       return FILTER_IGNORE;
598     }
599
600   tower_window = hildon_stackable_window_new ();
601
602   str = g_strdup_printf("%s, %s",
603                         details->fields[FieldDedication],
604                         details->fields[FieldPlace]);
605
606   hildon_window_set_markup (HILDON_WINDOW (tower_window),
607                             str);
608   g_free (str);
609
610   hbox = gtk_hbox_new (FALSE, 0);
611   tower_table = gtk_table_new (0, 2, FALSE);
612   buttons = gtk_vbox_new (TRUE, 0);
613   menu = HILDON_APP_MENU (hildon_app_menu_new ());
614
615   miles = distance_to_tower_str(details);
616
617   add_table_field ("Distance", miles);
618   add_table_field ("Postcode", details->fields[FieldPostcode]);
619   add_table_field ("County", details->fields[FieldCounty]);
620   add_table_field ("Country", details->fields[FieldCountry]);
621   add_table_field ("Diocese", details->fields[FieldDiocese]);
622   add_table_field ("Practice night", details->fields[FieldPracticeNight]);
623   add_table_field ("Bells", details->fields[FieldBells]);
624
625   g_free (miles);
626
627   tenor_weight = atoi (details->fields[FieldWt]);
628   str = g_strdup_printf("%dcwt %dqr %dlb in %s",
629                         tenor_weight/112,
630                         (tenor_weight % 112)/28,
631                         tenor_weight % 28,
632                         details->fields[FieldNote]
633                         );
634   add_table_field ("Tenor", str);
635   g_free (str);
636
637   if (strcmp(details->fields[FieldWebPage], "")!=0)
638     {
639       add_button ("Tower website", show_tower_website);
640     }
641   add_button ("Peals", show_peals_list);
642   add_button ("Map", show_tower_map);
643   add_button ("Directions", show_tower_directions);
644
645   /* don't use a toggle button: it looks stupid */
646   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
647                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
648                                         g_key_file_get_boolean (config,
649                                                                 CONFIG_BOOKMARK_GROUP,
650                                                                 details->fields[FieldPrimaryKey],
651                                                                 NULL)?
652                                         BUTTON_BOOKMARKED_YES: BUTTON_BOOKMARKED_NO,
653                                         NULL);
654   g_signal_connect (button, "clicked", G_CALLBACK (bookmark_toggled), NULL);
655   gtk_box_pack_start (GTK_BOX (buttons), button, FALSE, FALSE, 0);
656
657   gtk_widget_show_all (GTK_WIDGET (menu));
658   hildon_window_set_app_menu (HILDON_WINDOW (tower_window), menu);
659
660   gtk_box_pack_end (GTK_BOX (hbox), buttons, TRUE, TRUE, 0);
661   gtk_box_pack_end (GTK_BOX (hbox), tower_table, TRUE, TRUE, 0);
662
663   gtk_container_add (GTK_CONTAINER (tower_window), hbox);
664
665   g_free (tower_website);
666   tower_website = g_strdup_printf ("http://%s", details->fields[FieldWebPage]);
667   g_free (peals_list);
668   peals_list = g_strdup_printf ("http://www.pealbase.ismysite.co.uk/felstead/tbid.php?tid=%s",
669        details->fields[FieldTowerBase]);
670   g_free (tower_map);
671   tower_map = g_strdup_printf ("http://maps.google.com/maps?q=%s,%s",
672         details->fields[FieldLat],
673         details->fields[FieldLong]);
674   g_free (tower_directions);
675   if (device->fix->fields & LOCATION_GPS_DEVICE_LATLONG_SET)
676     {
677       tower_directions = g_strdup_printf ("http://maps.google.com/maps?q=%f,%f+to+%s,%s",
678                                           device->fix->latitude,
679                                           device->fix->longitude,
680                                           details->fields[FieldLat],
681                                           details->fields[FieldLong]);
682     }
683   g_free (tower_displayed);
684   tower_displayed = g_strdup (details->fields[FieldPrimaryKey]);
685
686   g_key_file_set_integer (config,
687                           CONFIG_RECENT_GROUP,
688                           tower_displayed,
689                           time (NULL));
690   remove_old_recent_entries ();
691   save_config ();
692
693   gtk_widget_show_all (GTK_WIDGET (tower_window));
694
695   return FILTER_STOP;
696 }
697
698 /**
699  * A tower that was accepted by a filter.
700  */
701 typedef struct {
702   char *sortkey;
703   char *primarykey;
704   char *displayname;
705 } FoundTower;
706
707 static FoundTower *
708 found_tower_new (tower *basis)
709 {
710   FoundTower* result = g_new (FoundTower, 1);
711
712   result->sortkey = g_strdup (basis->fields[FieldPrimaryKey]);
713   result->primarykey = g_strdup (basis->fields[FieldPrimaryKey]);
714
715   if (device->fix->fields & LOCATION_GPS_DEVICE_LATLONG_SET)
716     {
717       gchar *distance = distance_to_tower_str (basis);
718       result->displayname = g_strdup_printf ("%s, %s (%s, %s) (%s)",
719                                              basis->fields[FieldDedication],
720                                              basis->fields[FieldPlace],
721                                              basis->fields[FieldBells],
722                                              basis->fields[FieldPracticeNight],
723                                              distance);
724       g_free (distance);
725     }
726   else
727     {
728       result->displayname = g_strdup_printf ("%s, %s (%s, %s)",
729                                              basis->fields[FieldDedication],
730                                              basis->fields[FieldPlace],
731                                              basis->fields[FieldBells],
732                                              basis->fields[FieldPracticeNight]);
733     }
734
735   return result;
736 }
737
738 static void
739 found_tower_free (FoundTower *tower)
740 {
741   g_free (tower->sortkey);
742   g_free (tower->primarykey);
743   g_free (tower->displayname);
744   g_free (tower);
745 }
746
747 /**
748  * Calls a given function once for each tower in the world.
749  * (The first call, however, is a header row.)
750  *
751  * \param callback       The function to call.
752  * \param data           Arbitrary data to pass to the callback.
753  * \param dialogue_title If non-NULL, a list will be displayed
754  *                       with the results.  This is the title
755  *                       used for that dialogue.  (The dialogue
756  *                       will automatically free filter_results.)
757  */
758 static void
759 parse_dove (ParseDoveCallback callback,
760             gpointer data,
761             gchar *dialogue_title)
762 {
763   FILE *dove = fopen("/usr/share/belltower/dove.txt", "r");
764   char tower_rec[4096];
765   tower result;
766   char *i;
767   gboolean seen_newline;
768   GSList *filter_results = NULL;
769
770   if (!dove)
771     {
772       show_message ("Cannot open Dove database!");
773       exit (255);
774     }
775
776   result.serial = 0;
777
778   while (fgets (tower_rec, sizeof (tower_rec), dove))
779     {
780       seen_newline = FALSE;
781       result.fields[0] = tower_rec;
782       result.n_fields = 0;
783       for (i=tower_rec; *i; i++) {
784         if (*i=='\n')
785           {
786             seen_newline = TRUE;
787           }
788         if (*i=='\\' || *i=='\n')
789           {
790             *i = 0;
791             result.n_fields++;
792             result.fields[result.n_fields] = i+1;
793           }
794       }
795
796       if (!seen_newline)
797         {
798           /* keep it simple, stupid */
799           show_message ("Line too long, cannot continue.");
800           exit (255);
801         }
802
803       if (strcmp (result.fields[FieldCountry], "")==0)
804         {
805           result.fields[FieldCountry] = "England";
806         }
807
808       switch (callback (&result, data))
809         {
810         case FILTER_IGNORE:
811           /* nothing */
812           break;
813
814         case FILTER_STOP:
815           fclose (dove);
816           return;
817
818         case FILTER_ACCEPT:
819           filter_results = g_slist_append (filter_results,
820                                            found_tower_new (&result));
821         }
822
823       result.serial++;
824     }
825
826   fclose (dove);
827
828   if (dialogue_title)
829     {
830       show_towers_from_list (filter_results,
831                              dialogue_title);
832     }
833   else
834     {
835       free_tower_list (filter_results);
836     }
837 }
838
839 static void
840 show_tower (char *primary_key)
841 {
842   parse_dove (single_tower_cb, primary_key, NULL);
843 }
844
845 static void
846 free_tower_list (GSList *list)
847 {
848   GSList *cursor = list;
849
850   while (cursor)
851     {
852       found_tower_free ((FoundTower*) cursor->data);
853       cursor = cursor->next;
854     }
855
856   g_slist_free (list);
857 }
858
859 /**
860  * Displays a list of towers for the user to choose from.
861  * When one is chosen, we go to the display page for that tower.
862  * If there are none, this will tell the user there were none.
863  * If there is only one, we go straight to its display page.
864  *
865  * \param list       a GSList of FoundTower objects.
866  * \param list_name  the title for the dialogue.
867  */
868 static void
869 show_towers_from_list (GSList *list,
870                        gchar *list_name)
871 {
872   GtkWidget *dialog;
873   GtkWidget *selector;
874   gint result = -1;
875   GSList *cursor;
876
877   if (!list)
878     {
879       hildon_banner_show_information(window,
880                                      NULL,
881                                      "No towers found.");
882       return;
883     }
884
885   if (!list->next)
886     {
887       /* only one; don't bother showing the list */
888       FoundTower* found = (FoundTower*) list->data;
889
890       hildon_banner_show_information(window,
891                                      NULL,
892                                      "One tower found.");
893       show_tower (found->primarykey);
894
895       free_tower_list (list);
896       return;
897     }
898
899   dialog = hildon_picker_dialog_new (GTK_WINDOW (window));
900   selector = hildon_touch_selector_new_text ();
901   gtk_window_set_title (GTK_WINDOW (dialog), list_name);
902
903   for (cursor=list; cursor; cursor=cursor->next)
904     {
905       FoundTower* found = (FoundTower*) cursor->data;
906       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
907                                          found->displayname);
908     }
909
910   hildon_picker_dialog_set_selector (HILDON_PICKER_DIALOG (dialog),
911                                      HILDON_TOUCH_SELECTOR (selector));
912
913   gtk_widget_show_all (GTK_WIDGET (dialog));
914
915   if (gtk_dialog_run (GTK_DIALOG (dialog))==GTK_RESPONSE_OK)
916     {
917       GList *rows = hildon_touch_selector_get_selected_rows (HILDON_TOUCH_SELECTOR (selector),
918                                                              0);
919       GtkTreePath *path = (GtkTreePath*) rows->data;
920       gint *indices = gtk_tree_path_get_indices (path);
921
922       result = *indices;
923     }
924
925   gtk_widget_destroy (GTK_WIDGET (dialog));
926
927   if (result!=-1)
928     {
929       FoundTower *found = (FoundTower *) g_slist_nth_data (list, result);
930       show_tower (found->primarykey);
931     }
932
933   free_tower_list (list);
934 }
935
936 static gint strcmp_f (gconstpointer a,
937                       gconstpointer b)
938 {
939   return strcmp ((char*)a, (char*)b);
940 }
941
942 static void
943 put_areas_into_list (gpointer key,
944                      gpointer value,
945                      gpointer data)
946 {
947   GSList **list = (GSList **)data;
948   *list = g_slist_insert_sorted (*list,
949                                  value,
950                                  strcmp_f);
951 }
952
953 static void
954 nearby_towers (void)
955 {
956   if (!(device->fix->fields & LOCATION_GPS_DEVICE_LATLONG_SET))
957     {
958       show_message ("I don't know where you are!");
959       return;
960     }
961
962   parse_dove (get_nearby_towers_cb,
963               NULL,
964               "Towers within fifty miles of you");
965 }
966
967 static void
968 towers_by_subarea (gchar *area)
969 {
970   GtkWidget *dialog = hildon_picker_dialog_new (GTK_WINDOW (window));
971   GtkWidget *selector = hildon_touch_selector_new_text ();
972   GHashTable *hash = g_hash_table_new_full (g_str_hash,
973                                             g_str_equal,
974                                             g_free,
975                                             g_free);
976   GSList *list=NULL, *cursor;
977   gchar *title = g_strdup_printf ("Areas of %s", area);
978   country_cb_data d = { hash, area };
979   country_and_county cac = { area, NULL };
980
981   gtk_window_set_title (GTK_WINDOW (dialog), title);
982   g_free (title);
983
984   parse_dove (get_counties_cb, &d, NULL);
985
986   g_hash_table_foreach (hash,
987                         put_areas_into_list,
988                         &list);
989
990   for (cursor=list; cursor; cursor=cursor->next)
991     {
992       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
993                                          cursor->data);
994     }
995
996   hildon_picker_dialog_set_selector (HILDON_PICKER_DIALOG (dialog),
997                                      HILDON_TOUCH_SELECTOR (selector));
998
999   gtk_widget_show_all (GTK_WIDGET (dialog));
1000
1001   if (gtk_dialog_run (GTK_DIALOG (dialog))==GTK_RESPONSE_OK)
1002     {
1003       gchar *title;
1004       cac.county = g_strdup (hildon_touch_selector_get_current_text (HILDON_TOUCH_SELECTOR (selector)));
1005       title = g_strdup_printf ("Towers in %s",
1006                                cac.county);
1007
1008       parse_dove (get_towers_by_county_cb,
1009                   &cac,
1010                   title);
1011       g_free (cac.county);
1012       g_free (title);
1013     }
1014   g_hash_table_unref (hash);
1015   gtk_widget_destroy (GTK_WIDGET (dialog));
1016 }
1017
1018 /**
1019  * Maps a hash table from country names to counts of belltowers to a
1020  * newly-created hash table mapping country names to display
1021  * names, containing only those countries which have many
1022  * (or few) belltowers.
1023  *
1024  * \param source    the source table
1025  * \param want_many true if you want countries with many belltowers;
1026  *                  false if you want countries with few.
1027  */
1028 static GHashTable*
1029 get_countries_with_many (GHashTable *source,
1030                          gboolean want_many)
1031 {
1032   GHashTable *result = g_hash_table_new_full (g_str_hash,
1033                                               g_str_equal,
1034                                               g_free,
1035                                               NULL);
1036   GList *countries = g_hash_table_get_keys (source);
1037   GList *cursor = countries;
1038
1039   while (cursor)
1040     {
1041       gboolean has_many =
1042         GPOINTER_TO_INT (g_hash_table_lookup (source,
1043                                               cursor->data)) >= MANY_BELLTOWERS;
1044
1045       if (has_many == want_many)
1046         {
1047           g_hash_table_insert (result,
1048                                g_strdup (cursor->data),
1049                                g_strdup (cursor->data));
1050         }
1051
1052       cursor = cursor->next;
1053     }
1054
1055   g_list_free (countries);
1056   return result;
1057 }
1058
1059 #define COUNTRIES_WITH_MANY "Countries with many belltowers"
1060 #define COUNTRIES_WITH_FEW "Countries with few belltowers"
1061
1062 /**
1063  * Displays a list of areas of the world with many (or few)
1064  * belltowers.  If you ask for the areas with many, it include
1065  * a link to the areas with few.
1066  *
1067  * \param countries_with_many  True to list countries with many;
1068  *                             false to list countries with few.
1069  */
1070 static void
1071 towers_by_area_with_many (gboolean countries_with_many)
1072 {
1073   GtkWidget *dialog = hildon_picker_dialog_new (GTK_WINDOW (window));
1074   GtkWidget *selector = hildon_touch_selector_new_text ();
1075   GHashTable *countries_to_counts = g_hash_table_new_full (g_str_hash,
1076                                                            g_str_equal,
1077                                                            g_free,
1078                                                            NULL);
1079   GHashTable *country_names;
1080   GSList *list = NULL, *cursor;
1081   gchar *result = NULL;
1082
1083   gtk_window_set_title (GTK_WINDOW (dialog),
1084                         countries_with_many?
1085                         COUNTRIES_WITH_MANY : COUNTRIES_WITH_FEW);
1086
1087   parse_dove (get_countries_cb, countries_to_counts, NULL);
1088
1089   country_names = get_countries_with_many (countries_to_counts,
1090                                            countries_with_many);
1091
1092   g_hash_table_foreach (country_names,
1093                         put_areas_into_list,
1094                         &list);
1095
1096   for (cursor=list; cursor; cursor=cursor->next)
1097     {
1098       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
1099                                          cursor->data);
1100     }
1101
1102   if (countries_with_many)
1103     {
1104       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
1105                                          COUNTRIES_WITH_FEW);
1106     }
1107
1108   hildon_picker_dialog_set_selector (HILDON_PICKER_DIALOG (dialog),
1109                                      HILDON_TOUCH_SELECTOR (selector));
1110
1111   gtk_widget_show_all (GTK_WIDGET (dialog));
1112
1113   if (gtk_dialog_run (GTK_DIALOG (dialog))==GTK_RESPONSE_OK)
1114     {
1115       result = g_strdup (hildon_touch_selector_get_current_text (HILDON_TOUCH_SELECTOR (selector)));
1116     }
1117
1118   g_hash_table_unref (countries_to_counts);
1119   g_hash_table_unref (country_names);
1120   gtk_widget_destroy (GTK_WIDGET (dialog));
1121
1122   if (result)
1123     {
1124       if (countries_with_many)
1125         {
1126           /* these countries have many towers, so
1127            * show the sub-areas
1128            */
1129           if (strcmp (result, COUNTRIES_WITH_FEW)==0)
1130             towers_by_area_with_many (FALSE);
1131           else
1132             towers_by_subarea (result);
1133         }
1134       else
1135         {
1136           country_and_county cac = { result, NULL };
1137           gchar *title = g_strdup_printf ("Belltowers in %s",
1138                                           result);
1139
1140           parse_dove (get_towers_by_county_cb,
1141                       &cac,
1142                       title);
1143
1144           g_free (title);
1145         }
1146
1147       g_free (result);
1148     }
1149 }
1150
1151 /**
1152  * Shows all the towers in areas with many towers.
1153  */
1154 static void
1155 towers_by_area (void)
1156 {
1157   towers_by_area_with_many (TRUE);
1158 }
1159
1160 static void
1161 show_bookmarks (void)
1162 {
1163   parse_dove (get_group_of_towers_cb,
1164               CONFIG_BOOKMARK_GROUP,
1165               "Bookmarks");
1166 }
1167
1168 static void
1169 tower_search (void)
1170 {
1171   GtkWidget *terms = gtk_dialog_new_with_buttons ("What are you looking for?",
1172                                                   GTK_WINDOW (window),
1173                                                   GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
1174                                                   "Search",
1175                                                   GTK_RESPONSE_OK,
1176                                                   NULL);
1177   GtkWidget *entry = gtk_entry_new ();
1178
1179   gtk_box_pack_end (GTK_BOX (GTK_DIALOG (terms)->vbox),
1180                     entry, TRUE, TRUE, 0);
1181
1182   gtk_widget_show_all (GTK_WIDGET (terms));
1183
1184   if (gtk_dialog_run (GTK_DIALOG (terms))==GTK_RESPONSE_OK)
1185     {
1186       parse_dove (get_towers_by_search_cb,
1187                   (char*) gtk_entry_get_text (GTK_ENTRY (entry)),
1188                   "Search results");
1189     }
1190
1191   gtk_widget_destroy (GTK_WIDGET (terms));
1192 }
1193
1194 static void
1195 recent_towers (void)
1196 {
1197   parse_dove (get_group_of_towers_cb,
1198               CONFIG_RECENT_GROUP,
1199               "Towers you have recently viewed");
1200 }
1201
1202 /**
1203  * Displays a web page.
1204  * (Perhaps this should be merged with show_browser().)
1205  *
1206  * \param url  The URL.
1207  */
1208 static void
1209 show_web_page (GtkButton *dummy,
1210                gpointer url)
1211 {
1212   show_browser (url);
1213 }
1214
1215 /**
1216  * Shows the credits.
1217  *
1218  * \param source If non-null, we were called from a button press,
1219  *               so always show the credits.  If null, we were called
1220  *               automatically on startup, so show the credits if
1221  *               they haven't already been seen.
1222  */
1223 static void
1224 show_credits (GtkButton *source,
1225               gpointer dummy)
1226 {
1227   gboolean from_button = (source!=NULL);
1228   GtkWidget *dialog, *label, *button;
1229
1230   if (!from_button &&
1231       g_key_file_get_boolean (config,
1232                               CONFIG_GENERAL_GROUP,
1233                               CONFIG_SEEN_CREDITS_KEY,
1234                               NULL))
1235     {
1236       return;
1237     }
1238                               
1239
1240   dialog = gtk_dialog_new_with_buttons ("Credits",
1241                                         GTK_WINDOW (window),
1242                                         GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
1243                                         NULL
1244                                         );
1245
1246   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
1247                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
1248                                         "Welcome to Belltower.  The program is \xc2\xa9 2009 Thomas Thurman.",
1249                                         "View the program's home page.");
1250   g_signal_connect (button, "clicked", G_CALLBACK (show_web_page),
1251                     "http://belltower.garage.maemo.org");
1252   gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
1253                     button,
1254                     TRUE, TRUE, 0);
1255
1256   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
1257                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
1258                                         "This program is provided under the GNU GPL, with no warranty.",
1259                                         "View the GNU General Public Licence.");
1260   g_signal_connect (button, "clicked", G_CALLBACK (show_web_page),
1261                     "http://www.gnu.org/copyleft/gpl.html");
1262   gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
1263                     button,
1264                     TRUE, TRUE, 0);
1265
1266   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
1267                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
1268                                         "The data comes from Dove's Guide for Church Bell Ringers.",
1269                                         "View Dove's Guide.");
1270   g_signal_connect (button, "clicked", G_CALLBACK (show_web_page),
1271                     "http://dove.cccbr.org.uk");
1272   gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
1273                     button,
1274                     TRUE, TRUE, 0);
1275
1276   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
1277                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
1278                                         "The belfry image is \xc2\xa9 Amanda Slater, cc-by-sa.",
1279                                         "View the original photograph.");
1280   g_signal_connect (button, "clicked", G_CALLBACK (show_web_page),
1281                     "http://www.flickr.com/photos/pikerslanefarm/3398769335/");
1282   gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
1283                     button,
1284                     TRUE, TRUE, 0);
1285   
1286   gtk_widget_show_all (GTK_WIDGET (dialog));
1287
1288   g_key_file_set_boolean (config,
1289                           CONFIG_GENERAL_GROUP,
1290                           CONFIG_SEEN_CREDITS_KEY,
1291                           TRUE);
1292   save_config ();
1293 }
1294
1295 int
1296 main(int argc, char **argv)
1297 {
1298   GtkWidget *bell, *button, *hbox;
1299   GdkPixbuf *bell_picture;
1300
1301   gtk_init (&argc, &argv);
1302   g_set_application_name ("Belltower");
1303
1304   device = g_object_new (LOCATION_TYPE_GPS_DEVICE, NULL);
1305
1306   window = hildon_stackable_window_new ();
1307   gtk_window_set_title (GTK_WINDOW (window), "Belltower");
1308   g_signal_connect (G_OBJECT (window), "delete_event", G_CALLBACK (gtk_main_quit), NULL);
1309
1310   bell_picture = gdk_pixbuf_new_from_file ("/usr/share/belltower/bells1.jpg", NULL);
1311
1312   buttons = gtk_vbox_new (TRUE, 0);
1313   menu = HILDON_APP_MENU (hildon_app_menu_new ());
1314
1315   add_button ("Nearby", nearby_towers);
1316   add_button ("Recent", recent_towers);
1317   add_button ("Bookmarks", show_bookmarks);
1318   add_button ("By area", towers_by_area);
1319   add_button ("Search", tower_search);
1320
1321   /* extra buttons for the app menu */
1322   button = gtk_button_new_with_label ("Credits");
1323   g_signal_connect (button, "clicked", G_CALLBACK (show_credits), NULL);
1324   hildon_app_menu_append (menu, GTK_BUTTON (button));
1325
1326   gtk_widget_show_all (GTK_WIDGET (menu));
1327   hildon_window_set_app_menu (HILDON_WINDOW (window), menu);
1328
1329   hbox = gtk_hbox_new (FALSE, 0);
1330   gtk_box_pack_end (GTK_BOX (hbox), buttons, TRUE, TRUE, 0);
1331   gtk_box_pack_end (GTK_BOX (hbox),
1332                     gtk_image_new_from_pixbuf (bell_picture),
1333                     TRUE, TRUE, 0);
1334
1335   gtk_container_add (GTK_CONTAINER (window), hbox);
1336   gtk_widget_show_all (GTK_WIDGET (window));
1337
1338   load_config ();
1339   show_credits (NULL, NULL);
1340
1341   gtk_main ();
1342
1343   return EXIT_SUCCESS;
1344 }