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