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