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