fix distance calculation
[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
21 GtkWidget *window;
22
23 typedef enum {
24   /** stop scanning the database */
25   FILTER_STOP,
26   /** ignore this one */
27   FILTER_IGNORE,
28   /** add this one to the list */
29   FILTER_ACCEPT
30 } FilterResult;
31
32 /*
33   FIXME:
34   We should really do this by looking at the header row of the table.
35   They might decide to put in new columns some day.
36 */
37 typedef enum {
38   FieldPrimaryKey,
39   FieldNationalGrid,
40   FieldAccRef,
41   FieldSNLat,
42   FieldSNLong,
43   FieldPostcode,
44   FieldTowerBase,
45   FieldCounty,
46   FieldCountry,
47   FieldDiocese,
48   FieldPlace,
49   FieldPlace2,
50   FieldPlaceCL,
51   FieldDedication,
52   FieldBells,
53   FieldWt,
54   FieldApp,
55   FieldNote,
56   FieldHz,
57   FieldDetails,
58   FieldGF,
59   FieldToilet,
60   FieldUR,
61   FieldPDNo,
62   FieldPracticeNight,
63   FieldPSt,
64   FieldPrXF,
65   FieldOvhaulYr,
66   FieldContractor,
67   FieldExtraInfo,
68   FieldWebPage,
69   FieldUpdated,
70   FieldAffiliations,
71   FieldAltName,
72   FieldLat,
73   FieldLong,
74   FieldSimulator
75 } field;
76
77 typedef struct {
78   int serial;
79
80   /* the raw data */
81
82   char* fields[MAX_FIELDS];
83   int n_fields;
84 } tower;
85
86 /*
87  * we're going to pretend you're in Helsinki
88  * until I get the GPS working
89  */
90 double current_lat = 60.161790;
91 double current_long = 23.924902;
92
93 static void
94 show_message (char *message)
95 {
96   HildonNote* note = HILDON_NOTE
97     (hildon_note_new_information (GTK_WINDOW (window),
98                                   message?message:
99                                   "Some message was supposed to be here."));
100   gtk_dialog_run (GTK_DIALOG (note));
101   gtk_widget_destroy (GTK_WIDGET (note));
102 }
103
104 static gchar*
105 distance_to_tower (tower *details)
106 {
107   char *endptr;
108   double tower_lat;
109   double tower_long;
110   double km_distance;
111   const double km_to_miles = 1.609344;
112
113   tower_lat = strtod(details->fields[FieldLat], &endptr);
114   if (*endptr) return g_strdup ("unknown");
115   tower_long = strtod(details->fields[FieldLong], &endptr);
116   if (*endptr) return g_strdup ("unknown");
117
118   km_distance = location_distance_between (current_lat,
119                                            current_long,
120                                            tower_lat,
121                                            tower_long);
122
123   return g_strdup_printf("%dmi", (int) (km_distance / km_to_miles));
124 }
125
126 static void
127 call_dbus (DBusBusType type,
128            char *name,
129            char *path,
130            char *interface,
131            char *method,
132            char *parameter)
133 {
134   DBusGConnection *connection;
135   GError *error = NULL;
136
137   DBusGProxy *proxy;
138
139   connection = dbus_g_bus_get (type,
140                                &error);
141   if (connection == NULL)
142     {
143       show_message (error->message);
144       g_error_free (error);
145       return;
146     }
147
148   proxy = dbus_g_proxy_new_for_name (connection, name, path, interface);
149
150   error = NULL;
151   if (!dbus_g_proxy_call (proxy, method, &error,
152                           G_TYPE_STRING, parameter,
153                           G_TYPE_INVALID,
154                           G_TYPE_INVALID))
155     {
156       show_message (error->message);
157       g_error_free (error);
158     }
159 }
160
161 static void
162 show_browser (gchar *url)
163 {
164   call_dbus (DBUS_BUS_SESSION,
165              "com.nokia.osso_browser",
166              "/com/nokia/osso_browser/request",
167              "com.nokia.osso_browser",
168              "load_url",
169              url);
170 }
171
172 typedef FilterResult (*ParseDoveCallback)(tower *details, gpointer data);
173 typedef void (*ButtonCallback)(void);
174
175 GtkWidget *tower_window, *buttons, *tower_table;
176 HildonAppMenu *menu;
177
178 static void
179 add_table_field (char *name,
180                  char *value)
181 {
182   int row;
183   GtkLabel *label;
184   gchar *str;
185
186   g_object_get(tower_table, "n-rows", &row);
187
188   row++;
189
190   gtk_table_resize (GTK_TABLE (tower_table), row, 2);
191
192   label = GTK_LABEL (gtk_label_new (NULL));
193   str = g_strdup_printf("<b>%s</b>", name);
194   gtk_label_set_markup (label, str);
195   g_free (str);
196   gtk_label_set_justify (label, GTK_JUSTIFY_RIGHT);
197   gtk_table_attach_defaults (GTK_TABLE (tower_table),
198                              GTK_WIDGET (label),
199                              0, 1, row, row+1);
200
201   label = GTK_LABEL (gtk_label_new (value));
202   gtk_label_set_justify (label, GTK_JUSTIFY_LEFT);
203   gtk_table_attach_defaults (GTK_TABLE (tower_table),
204                              GTK_WIDGET (label),
205                              1, 2, row, row+1);
206 }
207
208 static void
209 add_button (char *label,
210             ButtonCallback callback)
211 {
212   GtkWidget *button;
213
214   button = gtk_button_new_with_label (label);
215   g_signal_connect (button, "clicked", G_CALLBACK (callback), NULL);
216   hildon_app_menu_append (menu, GTK_BUTTON (button));
217   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
218                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
219                                         label, NULL);
220   g_signal_connect (button, "clicked", G_CALLBACK (callback), NULL);
221   gtk_box_pack_end (GTK_BOX (buttons), button, FALSE, FALSE, 0);
222 }
223
224
225 static void
226 bookmark_toggled (GtkButton *button,
227                   gpointer dummy)
228 {
229   show_message ("Bookmarks are not yet implemented.");
230 }
231
232 char *tower_website = NULL;
233 char *tower_map = NULL;
234 char *tower_directions = NULL;
235 char *peals_list = NULL;
236
237 static void
238 show_tower_website (void)
239 {
240   show_browser (tower_website);
241 }
242
243 static void
244 show_tower_map (void)
245 {
246   show_browser (tower_map);
247 }
248
249 static void
250 show_peals_list (void)
251 {
252   show_browser (peals_list);
253 }
254
255 static FilterResult
256 get_countries_cb (tower *details,
257                   gpointer data)
258 {
259   GHashTable *hash = (GHashTable *)data;
260
261   if (details->serial==0)
262     return TRUE; /* header row */
263
264   if (!g_hash_table_lookup_extended (hash,
265                                     details->fields[FieldCountry],
266                                      NULL, NULL))
267     {
268       g_hash_table_insert (hash,
269                            g_strdup(details->fields[FieldCountry]),
270                            g_strdup (details->fields[FieldCountry]));
271     }
272
273   return FILTER_IGNORE;
274 }
275
276 typedef struct {
277   GHashTable *hash;
278   gchar *country_name;
279 } country_cb_data;
280
281 typedef struct {
282   char *country;
283   char *county;
284 } country_and_county;
285
286 static FilterResult
287 get_counties_cb (tower *details,
288                  gpointer data)
289 {
290   country_cb_data *d = (country_cb_data *)data;
291
292   if (details->serial==0)
293     return FILTER_IGNORE; /* header row */
294
295   if (strcmp(details->fields[FieldCountry], d->country_name)!=0)
296     return FILTER_IGNORE; /* wrong country */
297
298   if (!g_hash_table_lookup_extended (d->hash,
299                                     details->fields[FieldCounty],
300                                      NULL, NULL))
301     {
302       g_hash_table_insert (d->hash,
303                            g_strdup(details->fields[FieldCounty]),
304                            g_strdup (details->fields[FieldCounty]));
305     }
306
307   return FILTER_IGNORE;
308 }
309
310 static FilterResult
311 get_towers_by_county_cb (tower *details,
312                          gpointer data)
313 {
314   country_and_county *cac = (country_and_county *) data;
315
316   if ((!cac->county || strcmp (cac->county, details->fields[FieldCounty])==0) &&
317       (!cac->country || strcmp (cac->country, details->fields[FieldCountry])==0))
318     {
319       return FILTER_ACCEPT;
320     }
321   else
322     {
323       return FILTER_IGNORE;
324     }
325 }
326
327 static FilterResult
328 single_tower_cb (tower *details,
329                  gpointer data)
330 {
331
332   GtkWidget *hbox, *button;
333   gchar *str;
334   gint tenor_weight;
335   gchar *primary_key = (gchar*) data;
336   gchar *miles;
337
338   if (strcmp(details->fields[FieldPrimaryKey], primary_key)!=0)
339     {
340       /* not this one; keep going */
341       return FILTER_IGNORE;
342     }
343
344   tower_window = hildon_stackable_window_new ();
345
346   if (g_str_has_prefix (details->fields[FieldDedication],
347                         "S "))
348     {
349       /* FIXME: This needs to be cleverer, because we can have
350        * e.g. "S Peter and S Paul".
351        * May have to use regexps.
352        * Reallocation in general even when unchanged is okay,
353        * because it's the common case (most towers are S Something)
354        */
355       
356       /* FIXME: Since we're passing this in as markup,
357        * we need to escape the strings.
358        */
359
360       str = g_strdup_printf("S<sup>t</sup> %s, %s",
361                               details->fields[FieldDedication]+2,
362                               details->fields[FieldPlace]);
363
364     }
365   else
366     {
367       str = g_strdup_printf("%s, %s",
368                               details->fields[FieldDedication],
369                               details->fields[FieldPlace]);
370     }
371
372   hildon_window_set_markup (HILDON_WINDOW (tower_window),
373                             str);
374   g_free (str);
375
376   hbox = gtk_hbox_new (FALSE, 0);
377   tower_table = gtk_table_new (0, 2, FALSE);
378   buttons = gtk_vbox_new (TRUE, 0);
379   menu = HILDON_APP_MENU (hildon_app_menu_new ());
380
381   miles = distance_to_tower(details);
382
383   add_table_field ("Distance", miles);
384   add_table_field ("Postcode", details->fields[FieldPostcode]);
385   add_table_field ("County", details->fields[FieldCounty]);
386   add_table_field ("Country", details->fields[FieldCountry]);
387   add_table_field ("Diocese", details->fields[FieldDiocese]);
388   add_table_field ("Practice night", details->fields[FieldPracticeNight]);
389   add_table_field ("Bells", details->fields[FieldBells]);
390
391   g_free (miles);
392
393   tenor_weight = atoi (details->fields[FieldWt]);
394   str = g_strdup_printf("%dcwt %dqr %dlb in %s",
395                         tenor_weight/112,
396                         (tenor_weight % 112)/28,
397                         tenor_weight % 28,
398                         details->fields[FieldNote]
399                         );
400   add_table_field ("Tenor", str);
401   g_free (str);
402
403   add_button ("Tower website", show_tower_website);
404   add_button ("Peals", show_peals_list);
405   add_button ("Map", show_tower_map);
406   add_button ("Directions", NULL);
407
408   /* don't use a toggle button: it looks stupid */
409   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
410                                         HILDON_BUTTON_ARRANGEMENT_VERTICAL,
411                                         "Bookmark", NULL);
412   g_signal_connect (button, "clicked", G_CALLBACK (bookmark_toggled), NULL);
413   gtk_box_pack_start (GTK_BOX (buttons), button, FALSE, FALSE, 0);
414
415   gtk_widget_show_all (GTK_WIDGET (menu));
416   hildon_window_set_app_menu (HILDON_WINDOW (tower_window), menu);
417
418   gtk_box_pack_end (GTK_BOX (hbox), buttons, TRUE, TRUE, 0);
419   gtk_box_pack_end (GTK_BOX (hbox), tower_table, TRUE, TRUE, 0);
420
421   gtk_container_add (GTK_CONTAINER (tower_window), hbox);
422
423   g_free (tower_website);
424   tower_website = g_strdup_printf ("http://%s", details->fields[FieldWebPage]);
425   g_free (peals_list);
426   peals_list = g_strdup_printf ("http://www.pealbase.ismysite.co.uk/felstead/tbid.php?tid=%s",
427        details->fields[FieldTowerBase]);
428   g_free (tower_map);
429   tower_map = g_strdup_printf ("http://maps.google.com/maps?q=%s,%s",
430         details->fields[FieldLat],
431         details->fields[FieldLong]);
432   gtk_widget_show_all (GTK_WIDGET (tower_window));
433
434   return FILTER_STOP;
435 }
436
437 /**
438  * A tower that was accepted by a filter.
439  */
440 typedef struct {
441   char *sortkey;
442   char *primarykey;
443   char *displayname;
444 } FoundTower;
445
446 static FoundTower *
447 found_tower_new (tower *basis)
448 {
449   FoundTower* result = g_new (FoundTower, 1);
450
451   result->sortkey = g_strdup (basis->fields[FieldPrimaryKey]);
452   result->primarykey = g_strdup (basis->fields[FieldPrimaryKey]);
453   result->displayname = g_strdup_printf ("%s, %s (%s, %s)",
454                                          basis->fields[FieldDedication],
455                                          basis->fields[FieldPlace],
456                                          basis->fields[FieldBells],
457                                          basis->fields[FieldPracticeNight]);
458
459   return result;
460 }
461
462 static void
463 found_tower_free (FoundTower *tower)
464 {
465   g_free (tower->sortkey);
466   g_free (tower->primarykey);
467   g_free (tower->displayname);
468   g_free (tower);
469 }
470
471 static void
472 parse_dove (ParseDoveCallback callback,
473             GSList **filter_results,
474             gpointer data)
475 {
476   FILE *dove = fopen("/usr/share/belltower/dove.txt", "r");
477   char tower_rec[4096];
478   tower result;
479   char *i;
480   gboolean seen_newline;
481
482   if (!dove)
483     {
484       show_message ("Cannot open Dove database!");
485       exit (255);
486     }
487
488   result.serial = 0;
489
490   while (fgets (tower_rec, sizeof (tower_rec), dove))
491     {
492       seen_newline = FALSE;
493       result.fields[0] = tower_rec;
494       result.n_fields = 0;
495       for (i=tower_rec; *i; i++) {
496         if (*i=='\n')
497           {
498             seen_newline = TRUE;
499           }
500         if (*i=='\\' || *i=='\n')
501           {
502             *i = 0;
503             result.n_fields++;
504             result.fields[result.n_fields] = i+1;
505           }
506       }
507
508       if (!seen_newline)
509         {
510           /* keep it simple, stupid */
511           show_message ("Line too long, cannot continue.");
512           exit (255);
513         }
514
515       if (strcmp (result.fields[FieldCountry], "")==0)
516         {
517           result.fields[FieldCountry] = "England";
518         }
519
520       switch (callback (&result, data))
521         {
522         case FILTER_IGNORE:
523           /* nothing */
524           break;
525
526         case FILTER_STOP:
527           fclose (dove);
528           return;
529
530         case FILTER_ACCEPT:
531           if (filter_results)
532             {
533               *filter_results = g_slist_append (*filter_results,
534                                                 found_tower_new (&result));
535             }
536         }
537
538       result.serial++;
539     }
540
541   fclose (dove);
542 }
543
544 static void
545 nearby_towers (void)
546 {
547   char buffer[4096];
548   LocationGPSDevice *device;
549   device = g_object_new (LOCATION_TYPE_GPS_DEVICE, NULL);
550
551   sprintf(buffer, "%f %f %x",
552       device->fix->latitude,
553       device->fix->longitude,
554       device->fix->fields);
555   show_message (buffer);
556
557   if (device->fix->fields & LOCATION_GPS_DEVICE_LATLONG_SET)
558     {
559       show_message ("I know where you are!");
560     }
561   else
562     {
563       show_message ("I don't know where you are!");
564     }
565
566   g_object_unref (device);
567 }
568
569 static void
570 show_tower (char *primary_key)
571 {
572   parse_dove (single_tower_cb, NULL, primary_key);
573 }
574
575 static void
576 show_towers_from_list (GSList *list)
577 {
578   GtkWidget *dialog;
579   GtkWidget *selector;
580   gint result = -1;
581   GSList *cursor;
582   gchar foo[2048];
583
584   if (!list)
585     {
586       hildon_banner_show_information(window,
587                                      NULL,
588                                      "No towers found.");
589       return;
590     }
591
592   if (!list->next)
593     {
594       /* only one; don't bother showing the list */
595       hildon_banner_show_information(window,
596                                      NULL,
597                                      "One tower found.");
598       show_tower (list->data);
599
600       /* FIXME: and free the list */
601       return;
602     }
603
604   dialog = hildon_picker_dialog_new (GTK_WINDOW (window));
605   selector = hildon_touch_selector_new_text ();
606
607   for (cursor=list; cursor; cursor=cursor->next)
608     {
609       FoundTower* found = (FoundTower*) cursor->data;
610       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
611                                          found->displayname);
612     }
613
614   hildon_picker_dialog_set_selector (HILDON_PICKER_DIALOG (dialog),
615                                      HILDON_TOUCH_SELECTOR (selector));
616
617   gtk_widget_show_all (GTK_WIDGET (dialog));
618
619   if (gtk_dialog_run (GTK_DIALOG (dialog))==GTK_RESPONSE_OK)
620     {
621       GList *rows = hildon_touch_selector_get_selected_rows (HILDON_TOUCH_SELECTOR (selector),
622                                                              0);
623       GtkTreePath *path = (GtkTreePath*) rows->data;
624       gint *indices = gtk_tree_path_get_indices (path);
625
626       result = *indices;
627     }
628
629   gtk_widget_destroy (GTK_WIDGET (dialog));
630
631   if (result!=-1)
632     {
633       FoundTower *found = (FoundTower *) g_slist_nth_data (list, result);
634       show_tower (found->primarykey);
635     }
636
637   /* FIXME: and free the list */
638 }
639
640 static gint strcmp_f (gconstpointer a,
641                       gconstpointer b)
642 {
643   return strcmp ((char*)a, (char*)b);
644 }
645
646 static void
647 put_areas_into_list (gpointer key,
648                      gpointer value,
649                      gpointer data)
650 {
651   GSList **list = (GSList **)data;
652   *list = g_slist_insert_sorted (*list,
653                                  value,
654                                  strcmp_f);
655 }
656
657 static void
658 towers_by_subarea (gchar *area)
659 {
660   GtkWidget *dialog = hildon_picker_dialog_new (GTK_WINDOW (window));
661   GtkWidget *selector = hildon_touch_selector_new_text ();
662   GHashTable *hash = g_hash_table_new_full (g_str_hash,
663                                             g_str_equal,
664                                             g_free,
665                                             g_free);
666   GSList *list=NULL, *cursor;
667   gchar *title = g_strdup_printf ("Areas of %s", area);
668   country_cb_data d = { hash, area };
669   country_and_county cac = { area, NULL };
670
671   gtk_window_set_title (GTK_WINDOW (dialog), title);
672   g_free (title);
673
674   parse_dove (get_counties_cb, NULL, &d);
675
676   g_hash_table_foreach (hash,
677                         put_areas_into_list,
678                         &list);
679
680   for (cursor=list; cursor; cursor=cursor->next)
681     {
682       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
683                                          cursor->data);
684     }
685
686   hildon_picker_dialog_set_selector (HILDON_PICKER_DIALOG (dialog),
687                                      HILDON_TOUCH_SELECTOR (selector));
688
689   gtk_widget_show_all (GTK_WIDGET (dialog));
690
691   if (gtk_dialog_run (GTK_DIALOG (dialog))==GTK_RESPONSE_OK)
692     {
693       GSList *matches = NULL;
694       cac.county = strdup (hildon_touch_selector_get_current_text (HILDON_TOUCH_SELECTOR (selector)));
695
696       parse_dove (get_towers_by_county_cb,
697                   &matches,
698                   &cac);
699       g_free (cac.county);
700
701       show_towers_from_list (matches);
702     }
703   g_hash_table_unref (hash);
704   gtk_widget_destroy (GTK_WIDGET (dialog));
705 }
706
707 static void
708 towers_by_area (void)
709 {
710   GtkWidget *dialog = hildon_picker_dialog_new (GTK_WINDOW (window));
711   GtkWidget *selector = hildon_touch_selector_new_text ();
712   GHashTable *hash = g_hash_table_new_full (g_str_hash,
713                                             g_str_equal,
714                                             g_free,
715                                             g_free);
716   GSList *list = NULL, *cursor;
717   gchar *result = NULL;
718
719   gtk_window_set_title (GTK_WINDOW (dialog), "Areas of the world");
720
721   parse_dove (get_countries_cb, NULL, hash);
722
723   g_hash_table_foreach (hash,
724                         put_areas_into_list,
725                         &list);
726
727   for (cursor=list; cursor; cursor=cursor->next)
728     {
729       hildon_touch_selector_append_text (HILDON_TOUCH_SELECTOR (selector),
730                                          cursor->data);
731     }
732
733   hildon_picker_dialog_set_selector (HILDON_PICKER_DIALOG (dialog),
734                                      HILDON_TOUCH_SELECTOR (selector));
735
736   gtk_widget_show_all (GTK_WIDGET (dialog));
737
738   if (gtk_dialog_run (GTK_DIALOG (dialog))==GTK_RESPONSE_OK)
739     {
740       result = g_strdup (hildon_touch_selector_get_current_text (HILDON_TOUCH_SELECTOR (selector)));
741     }
742   g_hash_table_unref (hash);
743   gtk_widget_destroy (GTK_WIDGET (dialog));
744
745   if (result)
746     {
747       towers_by_subarea (result);
748       g_free (result);
749     }
750 }
751
752 static void
753 show_bookmarks (void)
754 {
755 }
756
757 static void
758 tower_search (void)
759 {
760 }
761
762 static void
763 recent_towers (void)
764 {
765 }
766
767 int
768 main(int argc, char **argv)
769 {
770   GtkWidget *bell, *button, *hbox;
771   GdkPixbuf *bell_picture;
772
773   gtk_init (&argc, &argv);
774   g_set_application_name ("Belltower");
775
776   window = hildon_stackable_window_new ();
777   gtk_window_set_title (GTK_WINDOW (window), "Belltower");
778   g_signal_connect (G_OBJECT (window), "delete_event", G_CALLBACK (gtk_main_quit), NULL);
779
780   bell_picture = gdk_pixbuf_new_from_file ("/usr/share/belltower/bells1.jpg", NULL);
781
782   buttons = gtk_vbox_new (TRUE, 0);
783   menu = HILDON_APP_MENU (hildon_app_menu_new ());
784
785   add_button ("Nearby", nearby_towers);
786   add_button ("Recent", recent_towers);
787   add_button ("Bookmarks", show_bookmarks);
788   add_button ("By area", towers_by_area);
789   add_button ("Search", tower_search);
790
791   /* extra buttons for the app menu */
792   button = gtk_button_new_with_label ("Credits");
793   hildon_app_menu_append (menu, GTK_BUTTON (button));
794   hildon_app_menu_append (menu, GTK_BUTTON (button));
795
796   gtk_widget_show_all (GTK_WIDGET (menu));
797   hildon_window_set_app_menu (HILDON_WINDOW (window), menu);
798
799   hbox = gtk_hbox_new (FALSE, 0);
800   gtk_box_pack_end (GTK_BOX (hbox), buttons, TRUE, TRUE, 0);
801   gtk_box_pack_end (GTK_BOX (hbox),
802                     gtk_image_new_from_pixbuf (bell_picture),
803                     TRUE, TRUE, 0);
804
805   gtk_container_add (GTK_CONTAINER (window), hbox);
806   gtk_widget_show_all (GTK_WIDGET (window));
807
808   gtk_main ();
809
810   return EXIT_SUCCESS;
811 }