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