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