rudimentary start at honouring sort key
[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   switch (settings_value[SETTINGS_TOWERSORT])
792     {
793     case TOWERSORT_DISTANCE:
794       /* ... */
795     case TOWERSORT_PRACTICE:
796       /* ... */
797     case TOWERSORT_WEIGHT:
798       /* ... */
799     case TOWERSORT_TOWN:
800     default:
801       result->sortkey = g_strdup (basis->fields[FieldPrimaryKey]);
802     }
803
804   result->primarykey = g_strdup (basis->fields[FieldPrimaryKey]);
805
806   if (device->fix->fields & LOCATION_GPS_DEVICE_LATLONG_SET)
807     {
808       gchar *distance = distance_to_tower_str (basis);
809       result->displayname = g_strdup_printf ("%s, %s (%s, %s) (%s)",
810                                              basis->fields[FieldDedication],
811                                              basis->fields[FieldPlace],
812                                              basis->fields[FieldBells],
813                                              basis->fields[FieldPracticeNight],
814                                              distance);
815       g_free (distance);
816     }
817   else
818     {
819       result->displayname = g_strdup_printf ("%s, %s (%s, %s)",
820                                              basis->fields[FieldDedication],
821                                              basis->fields[FieldPlace],
822                                              basis->fields[FieldBells],
823                                              basis->fields[FieldPracticeNight]);
824     }
825
826   return result;
827 }
828
829 static void
830 found_tower_free (FoundTower *tower)
831 {
832   g_free (tower->sortkey);
833   g_free (tower->primarykey);
834   g_free (tower->displayname);
835   g_free (tower);
836 }
837
838 /**
839  * Calls a given function once for each tower in the world.
840  * (The first call, however, is a header row.)
841  *
842  * \param callback       The function to call.
843  * \param data           Arbitrary data to pass to the callback.
844  * \param dialogue_title If non-NULL, a list will be displayed
845  *                       with the results.  This is the title
846  *                       used for that dialogue.  (The dialogue
847  *                       will automatically free filter_results.)
848  */
849 static void
850 parse_dove (ParseDoveCallback callback,
851             gpointer data,
852             gchar *dialogue_title)
853 {
854   FILE *dove = fopen("/usr/share/belltower/dove.txt", "r");
855   char tower_rec[4096];
856   tower result;
857   char *i;
858   gboolean seen_newline;
859   GSList *filter_results = NULL;
860
861   if (!dove)
862     {
863       show_message ("Cannot open Dove database!");
864       exit (255);
865     }
866
867   result.serial = 0;
868
869   while (fgets (tower_rec, sizeof (tower_rec), dove))
870     {
871       seen_newline = FALSE;
872       result.fields[0] = tower_rec;
873       result.n_fields = 0;
874       for (i=tower_rec; *i; i++) {
875         if (*i=='\n')
876           {
877             seen_newline = TRUE;
878           }
879         if (*i=='\\' || *i=='\n')
880           {
881             *i = 0;
882             result.n_fields++;
883             result.fields[result.n_fields] = i+1;
884           }
885       }
886
887       if (!seen_newline)
888         {
889           /* keep it simple, stupid */
890           show_message ("Line too long, cannot continue.");
891           exit (255);
892         }
893
894       if (strcmp (result.fields[FieldCountry], "")==0)
895         {
896           result.fields[FieldCountry] = "England";
897         }
898
899       switch (callback (&result, data))
900         {
901         case FILTER_IGNORE:
902           /* nothing */
903           break;
904
905         case FILTER_STOP:
906           fclose (dove);
907           return;
908
909         case FILTER_ACCEPT:
910           filter_results = g_slist_append (filter_results,
911                                            found_tower_new (&result));
912         }
913
914       result.serial++;
915     }
916
917   fclose (dove);
918
919   if (dialogue_title)
920     {
921       show_towers_from_list (filter_results,
922                              dialogue_title);
923     }
924   else
925     {
926       free_tower_list (filter_results);
927     }
928 }
929
930 static void
931 show_tower (char *primary_key)
932 {
933   parse_dove (single_tower_cb, primary_key, NULL);
934 }
935
936 static void
937 free_tower_list (GSList *list)
938 {
939   GSList *cursor = list;
940
941   while (cursor)
942     {
943       found_tower_free ((FoundTower*) cursor->data);
944       cursor = cursor->next;
945     }
946
947   g_slist_free (list);
948 }
949
950 /**
951  * Displays a list of towers for the user to choose from.
952  * When one is chosen, we go to the display page for that tower.
953  * If there are none, this will tell the user there were none.
954  * If there is only one, we go straight to its display page.
955  *
956  * \param list       a GSList of FoundTower objects.
957  * \param list_name  the title for the dialogue.
958  */
959 static void
960 show_towers_from_list (GSList *list,
961                        gchar *list_name)
962 {
963   GtkWidget *dialog;
964   GtkWidget *selector;
965   gint result = -1;
966   GSList *cursor;
967
968   if (!list)
969     {
970       hildon_banner_show_information(window,
971                                      NULL,
972                                      "No towers found.");
973       return;
974     }
975
976   if (!list->next)
977     {
978       /* only one; don't bother showing the list */
979       FoundTower* found = (FoundTower*) list->data;
980
981       hildon_banner_show_information(window,
982                                      NULL,
983                                      "One tower found.");
984       show_tower (found->primarykey);
985
986       free_tower_list (list);
987       return;
988     }
989
990   dialog = hildon_picker_dialog_new (GTK_WINDOW (window));
991   selector = hildon_touch_selector_new_text ();
992   gtk_window_set_title (GTK_WINDOW (dialog), list_name);
993
994   for (cursor=list; cursor; cursor=cursor->next)
995     {
996       FoundTower* found = (FoundTower*) cursor->data;
997       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
998                                          found->displayname);
999     }
1000
1001   hildon_picker_dialog_set_selector (HILDON_PICKER_DIALOG (dialog),
1002                                      HILDON_TOUCH_SELECTOR (selector));
1003
1004   gtk_widget_show_all (GTK_WIDGET (dialog));
1005
1006   if (gtk_dialog_run (GTK_DIALOG (dialog))==GTK_RESPONSE_OK)
1007     {
1008       GList *rows = hildon_touch_selector_get_selected_rows (HILDON_TOUCH_SELECTOR (selector),
1009                                                              0);
1010       GtkTreePath *path = (GtkTreePath*) rows->data;
1011       gint *indices = gtk_tree_path_get_indices (path);
1012
1013       result = *indices;
1014     }
1015
1016   gtk_widget_destroy (GTK_WIDGET (dialog));
1017
1018   if (result!=-1)
1019     {
1020       FoundTower *found = (FoundTower *) g_slist_nth_data (list, result);
1021       show_tower (found->primarykey);
1022     }
1023
1024   free_tower_list (list);
1025 }
1026
1027 static gint strcmp_f (gconstpointer a,
1028                       gconstpointer b)
1029 {
1030   return strcmp ((char*)a, (char*)b);
1031 }
1032
1033 static void
1034 put_areas_into_list (gpointer key,
1035                      gpointer value,
1036                      gpointer data)
1037 {
1038   GSList **list = (GSList **)data;
1039   *list = g_slist_insert_sorted (*list,
1040                                  value,
1041                                  strcmp_f);
1042 }
1043
1044 static void
1045 nearby_towers (void)
1046 {
1047   if (!(device->fix->fields & LOCATION_GPS_DEVICE_LATLONG_SET))
1048     {
1049       show_message ("I don't know where you are!");
1050       return;
1051     }
1052
1053   parse_dove (get_nearby_towers_cb,
1054               settings_value[SETTINGS_DISTANCES]==DISTANCES_KILOMETRES?
1055               GINT_TO_POINTER (80) :
1056               GINT_TO_POINTER (50),
1057               settings_value[SETTINGS_DISTANCES]==DISTANCES_KILOMETRES?
1058               "Towers within eighty kilometres of you":
1059               "Towers within fifty miles of you");
1060 }
1061
1062 static void
1063 towers_by_subarea (gchar *area)
1064 {
1065   GtkWidget *dialog = hildon_picker_dialog_new (GTK_WINDOW (window));
1066   GtkWidget *selector = hildon_touch_selector_new_text ();
1067   GHashTable *hash = g_hash_table_new_full (g_str_hash,
1068                                             g_str_equal,
1069                                             g_free,
1070                                             g_free);
1071   GSList *list=NULL, *cursor;
1072   gchar *title = g_strdup_printf ("Areas of %s", area);
1073   country_cb_data d = { hash, area };
1074   country_and_county cac = { area, NULL };
1075
1076   gtk_window_set_title (GTK_WINDOW (dialog), title);
1077   g_free (title);
1078
1079   parse_dove (get_counties_cb, &d, NULL);
1080
1081   g_hash_table_foreach (hash,
1082                         put_areas_into_list,
1083                         &list);
1084
1085   for (cursor=list; cursor; cursor=cursor->next)
1086     {
1087       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
1088                                          cursor->data);
1089     }
1090
1091   hildon_picker_dialog_set_selector (HILDON_PICKER_DIALOG (dialog),
1092                                      HILDON_TOUCH_SELECTOR (selector));
1093
1094   gtk_widget_show_all (GTK_WIDGET (dialog));
1095
1096   if (gtk_dialog_run (GTK_DIALOG (dialog))==GTK_RESPONSE_OK)
1097     {
1098       gchar *title;
1099       cac.county = g_strdup (hildon_touch_selector_get_current_text (HILDON_TOUCH_SELECTOR (selector)));
1100       title = g_strdup_printf ("Towers in %s",
1101                                cac.county);
1102
1103       parse_dove (get_towers_by_county_cb,
1104                   &cac,
1105                   title);
1106       g_free (cac.county);
1107       g_free (title);
1108     }
1109   g_hash_table_unref (hash);
1110   gtk_widget_destroy (GTK_WIDGET (dialog));
1111 }
1112
1113 /**
1114  * Maps a hash table from country names to counts of belltowers to a
1115  * newly-created hash table mapping country names to display
1116  * names, containing only those countries which have many
1117  * (or few) belltowers.
1118  *
1119  * \param source    the source table
1120  * \param want_many true if you want countries with many belltowers;
1121  *                  false if you want countries with few.
1122  */
1123 static GHashTable*
1124 get_countries_with_many (GHashTable *source,
1125                          gboolean want_many)
1126 {
1127   GHashTable *result = g_hash_table_new_full (g_str_hash,
1128                                               g_str_equal,
1129                                               g_free,
1130                                               NULL);
1131   GList *countries = g_hash_table_get_keys (source);
1132   GList *cursor = countries;
1133
1134   while (cursor)
1135     {
1136       gboolean has_many =
1137         GPOINTER_TO_INT (g_hash_table_lookup (source,
1138                                               cursor->data)) >= MANY_BELLTOWERS;
1139
1140       if (has_many == want_many)
1141         {
1142           g_hash_table_insert (result,
1143                                g_strdup (cursor->data),
1144                                g_strdup (cursor->data));
1145         }
1146
1147       cursor = cursor->next;
1148     }
1149
1150   g_list_free (countries);
1151   return result;
1152 }
1153
1154 #define COUNTRIES_WITH_MANY "Countries with many belltowers"
1155 #define COUNTRIES_WITH_FEW "Countries with few belltowers"
1156
1157 /**
1158  * Displays a list of areas of the world with many (or few)
1159  * belltowers.  If you ask for the areas with many, it include
1160  * a link to the areas with few.
1161  *
1162  * \param countries_with_many  True to list countries with many;
1163  *                             false to list countries with few.
1164  */
1165 static void
1166 towers_by_area_with_many (gboolean countries_with_many)
1167 {
1168   GtkWidget *dialog = hildon_picker_dialog_new (GTK_WINDOW (window));
1169   GtkWidget *selector = hildon_touch_selector_new_text ();
1170   GHashTable *countries_to_counts = g_hash_table_new_full (g_str_hash,
1171                                                            g_str_equal,
1172                                                            g_free,
1173                                                            NULL);
1174   GHashTable *country_names;
1175   GSList *list = NULL, *cursor;
1176   gchar *result = NULL;
1177
1178   gtk_window_set_title (GTK_WINDOW (dialog),
1179                         countries_with_many?
1180                         COUNTRIES_WITH_MANY : COUNTRIES_WITH_FEW);
1181
1182   parse_dove (get_countries_cb, countries_to_counts, NULL);
1183
1184   country_names = get_countries_with_many (countries_to_counts,
1185                                            countries_with_many);
1186
1187   g_hash_table_foreach (country_names,
1188                         put_areas_into_list,
1189                         &list);
1190
1191   for (cursor=list; cursor; cursor=cursor->next)
1192     {
1193       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
1194                                          cursor->data);
1195     }
1196
1197   if (countries_with_many)
1198     {
1199       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
1200                                          COUNTRIES_WITH_FEW);
1201     }
1202
1203   hildon_picker_dialog_set_selector (HILDON_PICKER_DIALOG (dialog),
1204                                      HILDON_TOUCH_SELECTOR (selector));
1205
1206   gtk_widget_show_all (GTK_WIDGET (dialog));
1207
1208   if (gtk_dialog_run (GTK_DIALOG (dialog))==GTK_RESPONSE_OK)
1209     {
1210       result = g_strdup (hildon_touch_selector_get_current_text (HILDON_TOUCH_SELECTOR (selector)));
1211     }
1212
1213   g_hash_table_unref (countries_to_counts);
1214   g_hash_table_unref (country_names);
1215   gtk_widget_destroy (GTK_WIDGET (dialog));
1216
1217   if (result)
1218     {
1219       if (countries_with_many)
1220         {
1221           /* these countries have many towers, so
1222            * show the sub-areas
1223            */
1224           if (strcmp (result, COUNTRIES_WITH_FEW)==0)
1225             towers_by_area_with_many (FALSE);
1226           else
1227             towers_by_subarea (result);
1228         }
1229       else
1230         {
1231           country_and_county cac = { result, NULL };
1232           gchar *title = g_strdup_printf ("Belltowers in %s",
1233                                           result);
1234
1235           parse_dove (get_towers_by_county_cb,
1236                       &cac,
1237                       title);
1238
1239           g_free (title);
1240         }
1241
1242       g_free (result);
1243     }
1244 }
1245
1246 /**
1247  * Shows all the towers in areas with many towers.
1248  */
1249 static void
1250 towers_by_area (void)
1251 {
1252   towers_by_area_with_many (TRUE);
1253 }
1254
1255 static void
1256 show_bookmarks (void)
1257 {
1258   parse_dove (get_group_of_towers_cb,
1259               CONFIG_BOOKMARK_GROUP,
1260               "Bookmarks");
1261 }
1262
1263 static void
1264 tower_search (void)
1265 {
1266   GtkWidget *terms = gtk_dialog_new_with_buttons ("What are you looking for?",
1267                                                   GTK_WINDOW (window),
1268                                                   GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
1269                                                   "Search",
1270                                                   GTK_RESPONSE_OK,
1271                                                   NULL);
1272   GtkWidget *entry = gtk_entry_new ();
1273
1274   gtk_box_pack_end (GTK_BOX (GTK_DIALOG (terms)->vbox),
1275                     entry, TRUE, TRUE, 0);
1276
1277   gtk_widget_show_all (GTK_WIDGET (terms));
1278
1279   if (gtk_dialog_run (GTK_DIALOG (terms))==GTK_RESPONSE_OK)
1280     {
1281       parse_dove (get_towers_by_search_cb,
1282                   (char*) gtk_entry_get_text (GTK_ENTRY (entry)),
1283                   "Search results");
1284     }
1285
1286   gtk_widget_destroy (GTK_WIDGET (terms));
1287 }
1288
1289 static void
1290 recent_towers (void)
1291 {
1292   parse_dove (get_group_of_towers_cb,
1293               CONFIG_RECENT_GROUP,
1294               "Towers you have recently viewed");
1295 }
1296
1297 /**
1298  * Displays a web page.
1299  * (Perhaps this should be merged with show_browser().)
1300  *
1301  * \param url  The URL.
1302  */
1303 static void
1304 show_web_page (GtkButton *dummy,
1305                gpointer url)
1306 {
1307   show_browser (url);
1308 }
1309
1310 /**
1311  * Shows the credits.
1312  *
1313  * \param source If non-null, we were called from a button press,
1314  *               so always show the credits.  If null, we were called
1315  *               automatically on startup, so show the credits if
1316  *               they haven't already been seen.
1317  */
1318 static void
1319 show_credits (GtkButton *source,
1320               gpointer dummy)
1321 {
1322   gboolean from_button = (source!=NULL);
1323   GtkWidget *dialog, *label, *button;
1324
1325   if (!from_button &&
1326       g_key_file_get_boolean (config,
1327                               CONFIG_GENERAL_GROUP,
1328                               CONFIG_SEEN_CREDITS_KEY,
1329                               NULL))
1330     {
1331       return;
1332     }
1333                               
1334
1335   dialog = gtk_dialog_new_with_buttons ("Credits",
1336                                         GTK_WINDOW (window),
1337                                         GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
1338                                         NULL
1339                                         );
1340
1341   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
1342                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
1343                                         "Welcome to Belltower.  The program is \xc2\xa9 2009 Thomas Thurman.",
1344                                         "View the program's home page.");
1345   g_signal_connect (button, "clicked", G_CALLBACK (show_web_page),
1346                     "http://belltower.garage.maemo.org");
1347   gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
1348                     button,
1349                     TRUE, TRUE, 0);
1350
1351   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
1352                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
1353                                         "This program is provided under the GNU GPL, with no warranty.",
1354                                         "View the GNU General Public Licence.");
1355   g_signal_connect (button, "clicked", G_CALLBACK (show_web_page),
1356                     "http://www.gnu.org/copyleft/gpl.html");
1357   gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
1358                     button,
1359                     TRUE, TRUE, 0);
1360
1361   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
1362                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
1363                                         "The data comes from Dove's Guide for Church Bell Ringers.",
1364                                         "View Dove's Guide.");
1365   g_signal_connect (button, "clicked", G_CALLBACK (show_web_page),
1366                     "http://dove.cccbr.org.uk");
1367   gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
1368                     button,
1369                     TRUE, TRUE, 0);
1370
1371   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
1372                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
1373                                         "The belfry image is \xc2\xa9 Amanda Slater, cc-by-sa.",
1374                                         "View the original photograph.");
1375   g_signal_connect (button, "clicked", G_CALLBACK (show_web_page),
1376                     "http://www.flickr.com/photos/pikerslanefarm/3398769335/");
1377   gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
1378                     button,
1379                     TRUE, TRUE, 0);
1380   
1381   gtk_widget_show_all (GTK_WIDGET (dialog));
1382
1383   g_key_file_set_boolean (config,
1384                           CONFIG_GENERAL_GROUP,
1385                           CONFIG_SEEN_CREDITS_KEY,
1386                           TRUE);
1387   save_config ();
1388 }
1389
1390 static void
1391 settings_dialogue (GtkButton *source,
1392                    gpointer dummy)
1393 {
1394   GtkWidget *dialog, *button;
1395   GtkWidget *selector[G_N_ELEMENTS (settings)];
1396   gint i;
1397
1398   dialog = gtk_dialog_new_with_buttons ("Settings",
1399                                         GTK_WINDOW (window),
1400                                         GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
1401                                         NULL
1402                                         );
1403
1404   for (i=0; i<G_N_ELEMENTS (settings); i++)
1405     {
1406       char **cursor = settings[i]+2;
1407       selector[i] = hildon_touch_selector_new_text ();
1408
1409       while (*cursor)
1410         {
1411           hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector[i]), *cursor);
1412           cursor++;
1413         }
1414
1415       hildon_touch_selector_set_active (HILDON_TOUCH_SELECTOR (selector[i]), 0, settings_value[i]);
1416
1417       button = hildon_picker_button_new (HILDON_SIZE_AUTO, HILDON_BUTTON_ARRANGEMENT_VERTICAL);
1418       hildon_button_set_title (HILDON_BUTTON (button), settings[i][1]);
1419       hildon_picker_button_set_selector (HILDON_PICKER_BUTTON (button),
1420                                          HILDON_TOUCH_SELECTOR (selector[i]));
1421       gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
1422                           button,
1423                           TRUE, TRUE, 0);
1424     }
1425
1426   gtk_widget_show_all (GTK_WIDGET (dialog));  
1427   gtk_dialog_run (GTK_DIALOG (dialog));
1428
1429   for (i=0; i<G_N_ELEMENTS (settings); i++)
1430     {
1431       GList *rows = hildon_touch_selector_get_selected_rows (HILDON_TOUCH_SELECTOR (selector[i]),
1432                                                              0);
1433       GtkTreePath *path = (GtkTreePath*) rows->data;
1434       gint *indices = gtk_tree_path_get_indices (path);
1435
1436       g_key_file_set_string (config,
1437                              CONFIG_GENERAL_GROUP,
1438                              settings[i][0],
1439                              hildon_touch_selector_get_current_text (HILDON_TOUCH_SELECTOR (selector[i])));
1440
1441       settings_value[i] = *indices;
1442     }
1443   save_config ();
1444
1445   gtk_widget_destroy (GTK_WIDGET (dialog));
1446 }
1447
1448 int
1449 main(int argc, char **argv)
1450 {
1451   GtkWidget *bell, *button, *hbox;
1452   GdkPixbuf *bell_picture;
1453
1454   gtk_init (&argc, &argv);
1455   g_set_application_name ("Belltower");
1456
1457   device = g_object_new (LOCATION_TYPE_GPS_DEVICE, NULL);
1458
1459   window = hildon_stackable_window_new ();
1460   gtk_window_set_title (GTK_WINDOW (window), "Belltower");
1461   g_signal_connect (G_OBJECT (window), "delete_event", G_CALLBACK (gtk_main_quit), NULL);
1462
1463   bell_picture = gdk_pixbuf_new_from_file ("/usr/share/belltower/bells1.jpg", NULL);
1464
1465   buttons = gtk_vbox_new (TRUE, 0);
1466   menu = HILDON_APP_MENU (hildon_app_menu_new ());
1467
1468   add_button ("Nearby", nearby_towers);
1469   add_button ("Recent", recent_towers);
1470   add_button ("Bookmarks", show_bookmarks);
1471   add_button ("By area", towers_by_area);
1472   add_button ("Search", tower_search);
1473   add_button ("(temp) settings", settings_dialogue);
1474
1475   /* extra buttons for the app menu */
1476   button = gtk_button_new_with_label ("Credits");
1477   g_signal_connect (button, "clicked", G_CALLBACK (show_credits), NULL);
1478   hildon_app_menu_append (menu, GTK_BUTTON (button));
1479
1480   gtk_widget_show_all (GTK_WIDGET (menu));
1481   hildon_window_set_app_menu (HILDON_WINDOW (window), menu);
1482
1483   hbox = gtk_hbox_new (FALSE, 0);
1484   gtk_box_pack_end (GTK_BOX (hbox), buttons, TRUE, TRUE, 0);
1485   gtk_box_pack_end (GTK_BOX (hbox),
1486                     gtk_image_new_from_pixbuf (bell_picture),
1487                     TRUE, TRUE, 0);
1488
1489   gtk_container_add (GTK_CONTAINER (window), hbox);
1490   gtk_widget_show_all (GTK_WIDGET (window));
1491
1492   load_config ();
1493   show_credits (NULL, NULL);
1494
1495   gtk_main ();
1496
1497   return EXIT_SUCCESS;
1498 }