forums
[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_peals_list (void)
365 {
366   show_browser (peals_list);
367 }
368
369 static FilterResult
370 get_countries_cb (tower *details,
371                   gpointer data)
372 {
373   GHashTable *hash = (GHashTable *)data;
374   gpointer value;
375
376   if (details->serial==0)
377     return TRUE; /* header row */
378
379   if (!g_hash_table_lookup_extended (hash,
380                                      details->fields[FieldCountry],
381                                      NULL, &value))
382     {
383       g_hash_table_insert (hash,
384                            g_strdup(details->fields[FieldCountry]),
385                            GINT_TO_POINTER (0));
386     }
387   else
388     {
389       g_hash_table_replace (hash,
390                             g_strdup(details->fields[FieldCountry]),
391                             GINT_TO_POINTER (GPOINTER_TO_INT (value)+1));
392     }
393
394   return FILTER_IGNORE;
395 }
396
397 typedef struct {
398   GHashTable *hash;
399   gchar *country_name;
400 } country_cb_data;
401
402 typedef struct {
403   char *country;
404   char *county;
405 } country_and_county;
406
407 static FilterResult
408 get_counties_cb (tower *details,
409                  gpointer data)
410 {
411   country_cb_data *d = (country_cb_data *)data;
412
413   if (details->serial==0)
414     return FILTER_IGNORE; /* header row */
415
416   if (strcmp(details->fields[FieldCountry], d->country_name)!=0)
417     return FILTER_IGNORE; /* wrong country */
418
419   if (!g_hash_table_lookup_extended (d->hash,
420                                     details->fields[FieldCounty],
421                                      NULL, NULL))
422     {
423       g_hash_table_insert (d->hash,
424                            g_strdup(details->fields[FieldCounty]),
425                            g_strdup (details->fields[FieldCounty]));
426     }
427
428   return FILTER_IGNORE;
429 }
430
431 static FilterResult
432 get_nearby_towers_cb (tower *details,
433                       gpointer data)
434 {
435   if (details->serial==0)
436     return FILTER_IGNORE; /* header row */
437
438   if (distance_to_tower (details) < 50)
439     {
440       return FILTER_ACCEPT;
441     }
442   else
443     {
444       return FILTER_IGNORE;
445     }
446 }
447
448 static FilterResult
449 get_towers_by_county_cb (tower *details,
450                          gpointer data)
451 {
452   country_and_county *cac = (country_and_county *) data;
453
454   if ((!cac->county || strcmp (cac->county, details->fields[FieldCounty])==0) &&
455       (!cac->country || strcmp (cac->country, details->fields[FieldCountry])==0))
456     {
457       return FILTER_ACCEPT;
458     }
459   else
460     {
461       return FILTER_IGNORE;
462     }
463 }
464
465 static FilterResult
466 get_towers_by_search_cb (tower *details,
467                          gpointer data)
468 {
469   char *s = (char *) data;
470
471   if (strcasestr(details->fields[FieldCountry], s) ||
472       strcasestr(details->fields[FieldCounty], s) ||
473       strcasestr(details->fields[FieldDedication], s) ||
474       strcasestr(details->fields[FieldPlace], s))
475     {
476       return FILTER_ACCEPT;
477     }
478   else
479     {
480       return FILTER_IGNORE;
481     }
482 }
483
484 /**
485  * A filter which accepts towers based on whether they
486  * appear in a particular group in the config file.
487  *
488  * \param details  the candidate tower
489  * \param data     pointer to a char* which names the group
490  */
491 static FilterResult
492 get_group_of_towers_cb (tower *details,
493                           gpointer data)
494 {
495   if (g_key_file_has_key (config,
496                           (char*) data,
497                           details->fields[FieldPrimaryKey],
498                           NULL))
499     {
500       return FILTER_ACCEPT;
501     }
502   else
503     {
504       return FILTER_IGNORE;
505     }
506 }
507
508 /**
509  * Removes the oldest entry from the [Recent] group in the config
510  * file until there are only five entries left.  Does not save
511  * the file; you have to do that.
512  */
513 static void
514 remove_old_recent_entries (void)
515 {
516   gint count;
517
518   do
519     {
520       gchar **towers;
521       gint oldest_date = 0;
522       gchar *oldest_tower = NULL;
523       gint i;
524
525       /* It is a bit inefficient to do this every
526        * time we go around the loop.  However, it
527        * makes the code far simpler, and we almost
528        * never go around more than once.
529        */
530       towers = g_key_file_get_keys (config,
531                                     CONFIG_RECENT_GROUP,
532                                     &count,
533                                     NULL);
534
535       if (count <= MAX_RECENT)
536         /* everything's fine */
537         return;
538
539       for (i=0; i<count; i++)
540         {
541           gint date = g_key_file_get_integer (config,
542                                               CONFIG_RECENT_GROUP,
543                                               towers[i],
544                                               NULL);
545
546           if (date==0)
547             continue;
548
549           if (oldest_date==0 ||
550               date < oldest_date)
551             {
552               oldest_tower = towers[i];
553               oldest_date = date;
554             }
555         }
556
557       if (oldest_tower)
558         {
559           g_key_file_remove_key (config,
560                                  CONFIG_RECENT_GROUP,
561                                  oldest_tower,
562                                  NULL);
563           count --;
564         }
565       g_strfreev (towers);
566     }
567   while (count > MAX_RECENT);
568 }
569
570 static FilterResult
571 single_tower_cb (tower *details,
572                  gpointer data)
573 {
574
575   GtkWidget *hbox, *button;
576   gchar *str;
577   gint tenor_weight;
578   gchar *primary_key = (gchar*) data;
579   gchar *miles;
580
581   if (strcmp(details->fields[FieldPrimaryKey], primary_key)!=0)
582     {
583       /* not this one; keep going */
584       return FILTER_IGNORE;
585     }
586
587   tower_window = hildon_stackable_window_new ();
588
589   str = g_strdup_printf("%s, %s",
590                         details->fields[FieldDedication],
591                         details->fields[FieldPlace]);
592
593   hildon_window_set_markup (HILDON_WINDOW (tower_window),
594                             str);
595   g_free (str);
596
597   hbox = gtk_hbox_new (FALSE, 0);
598   tower_table = gtk_table_new (0, 2, FALSE);
599   buttons = gtk_vbox_new (TRUE, 0);
600   menu = HILDON_APP_MENU (hildon_app_menu_new ());
601
602   miles = distance_to_tower_str(details);
603
604   add_table_field ("Distance", miles);
605   add_table_field ("Postcode", details->fields[FieldPostcode]);
606   add_table_field ("County", details->fields[FieldCounty]);
607   add_table_field ("Country", details->fields[FieldCountry]);
608   add_table_field ("Diocese", details->fields[FieldDiocese]);
609   add_table_field ("Practice night", details->fields[FieldPracticeNight]);
610   add_table_field ("Bells", details->fields[FieldBells]);
611
612   g_free (miles);
613
614   tenor_weight = atoi (details->fields[FieldWt]);
615   str = g_strdup_printf("%dcwt %dqr %dlb in %s",
616                         tenor_weight/112,
617                         (tenor_weight % 112)/28,
618                         tenor_weight % 28,
619                         details->fields[FieldNote]
620                         );
621   add_table_field ("Tenor", str);
622   g_free (str);
623
624   if (strcmp(details->fields[FieldWebPage], "")!=0)
625     {
626       add_button ("Tower website", show_tower_website);
627     }
628   add_button ("Peals", show_peals_list);
629   add_button ("Map", show_tower_map);
630   add_button ("Directions", NULL);
631
632   /* don't use a toggle button: it looks stupid */
633   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
634                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
635                                         g_key_file_get_boolean (config,
636                                                                 CONFIG_BOOKMARK_GROUP,
637                                                                 details->fields[FieldPrimaryKey],
638                                                                 NULL)?
639                                         BUTTON_BOOKMARKED_YES: BUTTON_BOOKMARKED_NO,
640                                         NULL);
641   g_signal_connect (button, "clicked", G_CALLBACK (bookmark_toggled), NULL);
642   gtk_box_pack_start (GTK_BOX (buttons), button, FALSE, FALSE, 0);
643
644   gtk_widget_show_all (GTK_WIDGET (menu));
645   hildon_window_set_app_menu (HILDON_WINDOW (tower_window), menu);
646
647   gtk_box_pack_end (GTK_BOX (hbox), buttons, TRUE, TRUE, 0);
648   gtk_box_pack_end (GTK_BOX (hbox), tower_table, TRUE, TRUE, 0);
649
650   gtk_container_add (GTK_CONTAINER (tower_window), hbox);
651
652   g_free (tower_website);
653   tower_website = g_strdup_printf ("http://%s", details->fields[FieldWebPage]);
654   g_free (peals_list);
655   peals_list = g_strdup_printf ("http://www.pealbase.ismysite.co.uk/felstead/tbid.php?tid=%s",
656        details->fields[FieldTowerBase]);
657   g_free (tower_map);
658   tower_map = g_strdup_printf ("http://maps.google.com/maps?q=%s,%s",
659         details->fields[FieldLat],
660         details->fields[FieldLong]);
661   g_free (tower_displayed);
662   tower_displayed = g_strdup (details->fields[FieldPrimaryKey]);
663
664   g_key_file_set_integer (config,
665                           CONFIG_RECENT_GROUP,
666                           tower_displayed,
667                           time (NULL));
668   remove_old_recent_entries ();
669   save_config ();
670
671   gtk_widget_show_all (GTK_WIDGET (tower_window));
672
673   return FILTER_STOP;
674 }
675
676 /**
677  * A tower that was accepted by a filter.
678  */
679 typedef struct {
680   char *sortkey;
681   char *primarykey;
682   char *displayname;
683 } FoundTower;
684
685 static FoundTower *
686 found_tower_new (tower *basis)
687 {
688   FoundTower* result = g_new (FoundTower, 1);
689
690   result->sortkey = g_strdup (basis->fields[FieldPrimaryKey]);
691   result->primarykey = g_strdup (basis->fields[FieldPrimaryKey]);
692
693   if (device->fix->fields & LOCATION_GPS_DEVICE_LATLONG_SET)
694     {
695       gchar *distance = distance_to_tower_str (basis);
696       result->displayname = g_strdup_printf ("%s, %s (%s, %s) (%s)",
697                                              basis->fields[FieldDedication],
698                                              basis->fields[FieldPlace],
699                                              basis->fields[FieldBells],
700                                              basis->fields[FieldPracticeNight],
701                                              distance);
702       g_free (distance);
703     }
704   else
705     {
706       result->displayname = g_strdup_printf ("%s, %s (%s, %s)",
707                                              basis->fields[FieldDedication],
708                                              basis->fields[FieldPlace],
709                                              basis->fields[FieldBells],
710                                              basis->fields[FieldPracticeNight]);
711     }
712
713   return result;
714 }
715
716 static void
717 found_tower_free (FoundTower *tower)
718 {
719   g_free (tower->sortkey);
720   g_free (tower->primarykey);
721   g_free (tower->displayname);
722   g_free (tower);
723 }
724
725 /**
726  * Calls a given function once for each tower in the world.
727  * (The first call, however, is a header row.)
728  *
729  * \param callback       The function to call.
730  * \param data           Arbitrary data to pass to the callback.
731  * \param dialogue_title If non-NULL, a list will be displayed
732  *                       with the results.  This is the title
733  *                       used for that dialogue.  (The dialogue
734  *                       will automatically free filter_results.)
735  */
736 static void
737 parse_dove (ParseDoveCallback callback,
738             gpointer data,
739             gchar *dialogue_title)
740 {
741   FILE *dove = fopen("/usr/share/belltower/dove.txt", "r");
742   char tower_rec[4096];
743   tower result;
744   char *i;
745   gboolean seen_newline;
746   GSList *filter_results = NULL;
747
748   if (!dove)
749     {
750       show_message ("Cannot open Dove database!");
751       exit (255);
752     }
753
754   result.serial = 0;
755
756   while (fgets (tower_rec, sizeof (tower_rec), dove))
757     {
758       seen_newline = FALSE;
759       result.fields[0] = tower_rec;
760       result.n_fields = 0;
761       for (i=tower_rec; *i; i++) {
762         if (*i=='\n')
763           {
764             seen_newline = TRUE;
765           }
766         if (*i=='\\' || *i=='\n')
767           {
768             *i = 0;
769             result.n_fields++;
770             result.fields[result.n_fields] = i+1;
771           }
772       }
773
774       if (!seen_newline)
775         {
776           /* keep it simple, stupid */
777           show_message ("Line too long, cannot continue.");
778           exit (255);
779         }
780
781       if (strcmp (result.fields[FieldCountry], "")==0)
782         {
783           result.fields[FieldCountry] = "England";
784         }
785
786       switch (callback (&result, data))
787         {
788         case FILTER_IGNORE:
789           /* nothing */
790           break;
791
792         case FILTER_STOP:
793           fclose (dove);
794           return;
795
796         case FILTER_ACCEPT:
797           filter_results = g_slist_append (filter_results,
798                                            found_tower_new (&result));
799         }
800
801       result.serial++;
802     }
803
804   fclose (dove);
805
806   if (dialogue_title)
807     {
808       show_towers_from_list (filter_results,
809                              dialogue_title);
810     }
811   else
812     {
813       free_tower_list (filter_results);
814     }
815 }
816
817 static void
818 show_tower (char *primary_key)
819 {
820   parse_dove (single_tower_cb, primary_key, NULL);
821 }
822
823 static void
824 free_tower_list (GSList *list)
825 {
826   GSList *cursor = list;
827
828   while (cursor)
829     {
830       found_tower_free ((FoundTower*) cursor->data);
831       cursor = cursor->next;
832     }
833
834   g_slist_free (list);
835 }
836
837 /**
838  * Displays a list of towers for the user to choose from.
839  * When one is chosen, we go to the display page for that tower.
840  * If there are none, this will tell the user there were none.
841  * If there is only one, we go straight to its display page.
842  *
843  * \param list       a GSList of FoundTower objects.
844  * \param list_name  the title for the dialogue.
845  */
846 static void
847 show_towers_from_list (GSList *list,
848                        gchar *list_name)
849 {
850   GtkWidget *dialog;
851   GtkWidget *selector;
852   gint result = -1;
853   GSList *cursor;
854
855   if (!list)
856     {
857       hildon_banner_show_information(window,
858                                      NULL,
859                                      "No towers found.");
860       return;
861     }
862
863   if (!list->next)
864     {
865       /* only one; don't bother showing the list */
866       FoundTower* found = (FoundTower*) list->data;
867
868       hildon_banner_show_information(window,
869                                      NULL,
870                                      "One tower found.");
871       show_tower (found->primarykey);
872
873       free_tower_list (list);
874       return;
875     }
876
877   dialog = hildon_picker_dialog_new (GTK_WINDOW (window));
878   selector = hildon_touch_selector_new_text ();
879   gtk_window_set_title (GTK_WINDOW (dialog), list_name);
880
881   for (cursor=list; cursor; cursor=cursor->next)
882     {
883       FoundTower* found = (FoundTower*) cursor->data;
884       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
885                                          found->displayname);
886     }
887
888   hildon_picker_dialog_set_selector (HILDON_PICKER_DIALOG (dialog),
889                                      HILDON_TOUCH_SELECTOR (selector));
890
891   gtk_widget_show_all (GTK_WIDGET (dialog));
892
893   if (gtk_dialog_run (GTK_DIALOG (dialog))==GTK_RESPONSE_OK)
894     {
895       GList *rows = hildon_touch_selector_get_selected_rows (HILDON_TOUCH_SELECTOR (selector),
896                                                              0);
897       GtkTreePath *path = (GtkTreePath*) rows->data;
898       gint *indices = gtk_tree_path_get_indices (path);
899
900       result = *indices;
901     }
902
903   gtk_widget_destroy (GTK_WIDGET (dialog));
904
905   if (result!=-1)
906     {
907       FoundTower *found = (FoundTower *) g_slist_nth_data (list, result);
908       show_tower (found->primarykey);
909     }
910
911   free_tower_list (list);
912 }
913
914 static gint strcmp_f (gconstpointer a,
915                       gconstpointer b)
916 {
917   return strcmp ((char*)a, (char*)b);
918 }
919
920 static void
921 put_areas_into_list (gpointer key,
922                      gpointer value,
923                      gpointer data)
924 {
925   GSList **list = (GSList **)data;
926   *list = g_slist_insert_sorted (*list,
927                                  value,
928                                  strcmp_f);
929 }
930
931 static void
932 nearby_towers (void)
933 {
934   if (!(device->fix->fields & LOCATION_GPS_DEVICE_LATLONG_SET))
935     {
936       show_message ("I don't know where you are!");
937       return;
938     }
939
940   parse_dove (get_nearby_towers_cb,
941               NULL,
942               "Towers within fifty miles of you");
943 }
944
945 static void
946 towers_by_subarea (gchar *area)
947 {
948   GtkWidget *dialog = hildon_picker_dialog_new (GTK_WINDOW (window));
949   GtkWidget *selector = hildon_touch_selector_new_text ();
950   GHashTable *hash = g_hash_table_new_full (g_str_hash,
951                                             g_str_equal,
952                                             g_free,
953                                             g_free);
954   GSList *list=NULL, *cursor;
955   gchar *title = g_strdup_printf ("Areas of %s", area);
956   country_cb_data d = { hash, area };
957   country_and_county cac = { area, NULL };
958
959   gtk_window_set_title (GTK_WINDOW (dialog), title);
960   g_free (title);
961
962   parse_dove (get_counties_cb, &d, NULL);
963
964   g_hash_table_foreach (hash,
965                         put_areas_into_list,
966                         &list);
967
968   for (cursor=list; cursor; cursor=cursor->next)
969     {
970       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
971                                          cursor->data);
972     }
973
974   hildon_picker_dialog_set_selector (HILDON_PICKER_DIALOG (dialog),
975                                      HILDON_TOUCH_SELECTOR (selector));
976
977   gtk_widget_show_all (GTK_WIDGET (dialog));
978
979   if (gtk_dialog_run (GTK_DIALOG (dialog))==GTK_RESPONSE_OK)
980     {
981       gchar *title;
982       cac.county = g_strdup (hildon_touch_selector_get_current_text (HILDON_TOUCH_SELECTOR (selector)));
983       title = g_strdup_printf ("Towers in %s",
984                                cac.county);
985
986       parse_dove (get_towers_by_county_cb,
987                   &cac,
988                   title);
989       g_free (cac.county);
990       g_free (title);
991     }
992   g_hash_table_unref (hash);
993   gtk_widget_destroy (GTK_WIDGET (dialog));
994 }
995
996 /**
997  * Maps a hash table from country names to counts of belltowers to a
998  * newly-created hash table mapping country names to display
999  * names, containing only those countries which have many
1000  * (or few) belltowers.
1001  *
1002  * \param source    the source table
1003  * \param want_many true if you want countries with many belltowers;
1004  *                  false if you want countries with few.
1005  */
1006 static GHashTable*
1007 get_countries_with_many (GHashTable *source,
1008                          gboolean want_many)
1009 {
1010   GHashTable *result = g_hash_table_new_full (g_str_hash,
1011                                               g_str_equal,
1012                                               g_free,
1013                                               NULL);
1014   GList *countries = g_hash_table_get_keys (source);
1015   GList *cursor = countries;
1016
1017   while (cursor)
1018     {
1019       gboolean has_many =
1020         GPOINTER_TO_INT (g_hash_table_lookup (source,
1021                                               cursor->data)) >= MANY_BELLTOWERS;
1022
1023       if (has_many == want_many)
1024         {
1025           g_hash_table_insert (result,
1026                                g_strdup (cursor->data),
1027                                g_strdup (cursor->data));
1028         }
1029
1030       cursor = cursor->next;
1031     }
1032
1033   g_list_free (countries);
1034   return result;
1035 }
1036
1037 #define COUNTRIES_WITH_MANY "Countries with many belltowers"
1038 #define COUNTRIES_WITH_FEW "Countries with few belltowers"
1039
1040 /**
1041  * Displays a list of areas of the world with many (or few)
1042  * belltowers.  If you ask for the areas with many, it include
1043  * a link to the areas with few.
1044  *
1045  * \param countries_with_many  True to list countries with many;
1046  *                             false to list countries with few.
1047  */
1048 static void
1049 towers_by_area_with_many (gboolean countries_with_many)
1050 {
1051   GtkWidget *dialog = hildon_picker_dialog_new (GTK_WINDOW (window));
1052   GtkWidget *selector = hildon_touch_selector_new_text ();
1053   GHashTable *countries_to_counts = g_hash_table_new_full (g_str_hash,
1054                                                            g_str_equal,
1055                                                            g_free,
1056                                                            NULL);
1057   GHashTable *country_names;
1058   GSList *list = NULL, *cursor;
1059   gchar *result = NULL;
1060
1061   gtk_window_set_title (GTK_WINDOW (dialog),
1062                         countries_with_many?
1063                         COUNTRIES_WITH_MANY : COUNTRIES_WITH_FEW);
1064
1065   parse_dove (get_countries_cb, countries_to_counts, NULL);
1066
1067   country_names = get_countries_with_many (countries_to_counts,
1068                                            countries_with_many);
1069
1070   g_hash_table_foreach (country_names,
1071                         put_areas_into_list,
1072                         &list);
1073
1074   for (cursor=list; cursor; cursor=cursor->next)
1075     {
1076       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
1077                                          cursor->data);
1078     }
1079
1080   if (countries_with_many)
1081     {
1082       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
1083                                          COUNTRIES_WITH_FEW);
1084     }
1085
1086   hildon_picker_dialog_set_selector (HILDON_PICKER_DIALOG (dialog),
1087                                      HILDON_TOUCH_SELECTOR (selector));
1088
1089   gtk_widget_show_all (GTK_WIDGET (dialog));
1090
1091   if (gtk_dialog_run (GTK_DIALOG (dialog))==GTK_RESPONSE_OK)
1092     {
1093       result = g_strdup (hildon_touch_selector_get_current_text (HILDON_TOUCH_SELECTOR (selector)));
1094     }
1095
1096   g_hash_table_unref (countries_to_counts);
1097   g_hash_table_unref (country_names);
1098   gtk_widget_destroy (GTK_WIDGET (dialog));
1099
1100   if (result)
1101     {
1102       if (countries_with_many)
1103         {
1104           /* these countries have many towers, so
1105            * show the sub-areas
1106            */
1107           if (strcmp (result, COUNTRIES_WITH_FEW)==0)
1108             towers_by_area_with_many (FALSE);
1109           else
1110             towers_by_subarea (result);
1111         }
1112       else
1113         {
1114           country_and_county cac = { result, NULL };
1115           gchar *title = g_strdup_printf ("Belltowers in %s",
1116                                           result);
1117
1118           parse_dove (get_towers_by_county_cb,
1119                       &cac,
1120                       title);
1121
1122           g_free (title);
1123         }
1124
1125       g_free (result);
1126     }
1127 }
1128
1129 /**
1130  * Shows all the towers in areas with many towers.
1131  */
1132 static void
1133 towers_by_area (void)
1134 {
1135   towers_by_area_with_many (TRUE);
1136 }
1137
1138 static void
1139 show_bookmarks (void)
1140 {
1141   parse_dove (get_group_of_towers_cb,
1142               CONFIG_BOOKMARK_GROUP,
1143               "Bookmarks");
1144 }
1145
1146 static void
1147 tower_search (void)
1148 {
1149   GtkWidget *terms = gtk_dialog_new_with_buttons ("What are you looking for?",
1150                                                   GTK_WINDOW (window),
1151                                                   GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
1152                                                   "Search",
1153                                                   GTK_RESPONSE_OK,
1154                                                   NULL);
1155   GtkWidget *entry = gtk_entry_new ();
1156
1157   gtk_box_pack_end (GTK_BOX (GTK_DIALOG (terms)->vbox),
1158                     entry, TRUE, TRUE, 0);
1159
1160   gtk_widget_show_all (GTK_WIDGET (terms));
1161
1162   if (gtk_dialog_run (GTK_DIALOG (terms))==GTK_RESPONSE_OK)
1163     {
1164       parse_dove (get_towers_by_search_cb,
1165                   (char*) gtk_entry_get_text (GTK_ENTRY (entry)),
1166                   "Search results");
1167     }
1168
1169   gtk_widget_destroy (GTK_WIDGET (terms));
1170 }
1171
1172 static void
1173 recent_towers (void)
1174 {
1175   parse_dove (get_group_of_towers_cb,
1176               CONFIG_RECENT_GROUP,
1177               "Towers you have recently viewed");
1178 }
1179
1180 /**
1181  * Displays a web page.
1182  * (Perhaps this should be merged with show_browser().)
1183  *
1184  * \param url  The URL.
1185  */
1186 static void
1187 show_web_page (GtkButton *dummy,
1188                gpointer url)
1189 {
1190   show_browser (url);
1191 }
1192
1193 /**
1194  * Shows the credits.
1195  *
1196  * \param source If non-null, we were called from a button press,
1197  *               so always show the credits.  If null, we were called
1198  *               automatically on startup, so show the credits if
1199  *               they haven't already been seen.
1200  */
1201 static void
1202 show_credits (GtkButton *source,
1203               gpointer dummy)
1204 {
1205   gboolean from_button = (source!=NULL);
1206   GtkWidget *dialog, *label, *button;
1207
1208   if (!from_button &&
1209       g_key_file_get_boolean (config,
1210                               CONFIG_GENERAL_GROUP,
1211                               CONFIG_SEEN_CREDITS_KEY,
1212                               NULL))
1213     {
1214       return;
1215     }
1216                               
1217
1218   dialog = gtk_dialog_new_with_buttons ("Credits",
1219                                         GTK_WINDOW (window),
1220                                         GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
1221                                         NULL
1222                                         );
1223
1224   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
1225                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
1226                                         "Welcome to Belltower.  The program is \xc2\xa9 2009 Thomas Thurman.",
1227                                         "View the program's home page.");
1228   g_signal_connect (button, "clicked", G_CALLBACK (show_web_page),
1229                     "http://belltower.garage.maemo.org");
1230   gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
1231                     button,
1232                     TRUE, TRUE, 0);
1233
1234   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
1235                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
1236                                         "This program is provided under the GNU GPL, with no warranty.",
1237                                         "View the GNU General Public Licence.");
1238   g_signal_connect (button, "clicked", G_CALLBACK (show_web_page),
1239                     "http://www.gnu.org/copyleft/gpl.html");
1240   gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
1241                     button,
1242                     TRUE, TRUE, 0);
1243
1244   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
1245                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
1246                                         "The data comes from Dove's Guide for Church Bell Ringers.",
1247                                         "View Dove's Guide.");
1248   g_signal_connect (button, "clicked", G_CALLBACK (show_web_page),
1249                     "http://dove.cccbr.org.uk");
1250   gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
1251                     button,
1252                     TRUE, TRUE, 0);
1253
1254   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
1255                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
1256                                         "The belfry image is \xc2\xa9 Amanda Slater, cc-by-sa.",
1257                                         "View the original photograph.");
1258   g_signal_connect (button, "clicked", G_CALLBACK (show_web_page),
1259                     "http://www.flickr.com/photos/pikerslanefarm/3398769335/");
1260   gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
1261                     button,
1262                     TRUE, TRUE, 0);
1263   
1264   gtk_widget_show_all (GTK_WIDGET (dialog));
1265
1266   g_key_file_set_boolean (config,
1267                           CONFIG_GENERAL_GROUP,
1268                           CONFIG_SEEN_CREDITS_KEY,
1269                           TRUE);
1270   save_config ();
1271 }
1272
1273 int
1274 main(int argc, char **argv)
1275 {
1276   GtkWidget *bell, *button, *hbox;
1277   GdkPixbuf *bell_picture;
1278
1279   gtk_init (&argc, &argv);
1280   g_set_application_name ("Belltower");
1281
1282   device = g_object_new (LOCATION_TYPE_GPS_DEVICE, NULL);
1283
1284   window = hildon_stackable_window_new ();
1285   gtk_window_set_title (GTK_WINDOW (window), "Belltower");
1286   g_signal_connect (G_OBJECT (window), "delete_event", G_CALLBACK (gtk_main_quit), NULL);
1287
1288   bell_picture = gdk_pixbuf_new_from_file ("/usr/share/belltower/bells1.jpg", NULL);
1289
1290   buttons = gtk_vbox_new (TRUE, 0);
1291   menu = HILDON_APP_MENU (hildon_app_menu_new ());
1292
1293   add_button ("Nearby", nearby_towers);
1294   add_button ("Recent", recent_towers);
1295   add_button ("Bookmarks", show_bookmarks);
1296   add_button ("By area", towers_by_area);
1297   add_button ("Search", tower_search);
1298
1299   /* extra buttons for the app menu */
1300   button = gtk_button_new_with_label ("Credits");
1301   g_signal_connect (button, "clicked", G_CALLBACK (show_credits), NULL);
1302   hildon_app_menu_append (menu, GTK_BUTTON (button));
1303
1304   gtk_widget_show_all (GTK_WIDGET (menu));
1305   hildon_window_set_app_menu (HILDON_WINDOW (window), menu);
1306
1307   hbox = gtk_hbox_new (FALSE, 0);
1308   gtk_box_pack_end (GTK_BOX (hbox), buttons, TRUE, TRUE, 0);
1309   gtk_box_pack_end (GTK_BOX (hbox),
1310                     gtk_image_new_from_pixbuf (bell_picture),
1311                     TRUE, TRUE, 0);
1312
1313   gtk_container_add (GTK_CONTAINER (window), hbox);
1314   gtk_widget_show_all (GTK_WIDGET (window));
1315
1316   load_config ();
1317   show_credits (NULL, NULL);
1318
1319   gtk_main ();
1320
1321   return EXIT_SUCCESS;
1322 }