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