Add E-mail button
[beifahrer] / src / adac-mitfahrclub.vala
1 /* This file is part of Beifahrer.
2  *
3  * Copyright (C) 2010 Philipp Zabel
4  *
5  * Beifahrer is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * Beifahrer is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with Beifahrer. If not, see <http://www.gnu.org/licenses/>.
17  */
18
19 [Compact]
20 public class City {
21         public int number;
22         public string name;
23         public double latitude;
24         public double longitude;
25         public double north;
26         public double south;
27         public double east;
28         public double west;
29
30         public City (int _number, string _name) {
31                 number = _number;
32                 name = _name;
33                 latitude = 0.0;
34                 longitude = 0.0;
35         }
36
37         internal double bb_area () {
38                 return (north - south) * (east - west);
39         }
40 }
41
42 public enum LiftFlags {
43         SMOKER = 1,
44         NON_SMOKER = 2,
45         ADAC_MEMBER = 4,
46         WOMEN_ONLY = 8
47 }
48
49 public class Lift : Object {
50         public string city_from;
51         public string city_to;
52         public Time time;
53         public int places;
54         public string price;
55         public LiftFlags flags;
56
57         public string href;
58
59         public List<string> city_via;
60         public string name;
61         public string cell;
62         public string phone;
63         public string phone2;
64         public string email;
65         public string email_image_uri;
66         public string description;
67         public string modified;
68
69         public Lift () {
70                 time.hour = -1;
71         }
72 }
73
74 public class CallbackMessage : Soup.Message {
75         public SourceFunc callback;
76
77         public CallbackMessage (string url, SourceFunc? _callback) {
78                 method = "GET";
79                 callback = _callback;
80                 set_uri (new Soup.URI (url));
81         }
82 }
83
84 public class AdacMitfahrclub {
85         const string BASE_URI = "http://mitfahrclub.adac.de";
86
87         Soup.SessionAsync session;
88         List<City> city_list = null;
89
90         public AdacMitfahrclub () {
91                 session = new Soup.SessionAsync ();
92         }
93
94         private void message_finished (Soup.Session session, Soup.Message message) {
95                 Idle.add (((CallbackMessage) message).callback);
96         }
97
98         private async Html.Doc* get_html_document (string url) {
99                 var message = new CallbackMessage (url, get_html_document.callback);
100                 session.queue_message (message, message_finished);
101
102                 yield;
103
104                 return Html.Doc.read_memory ((char[]) message.response_body.data, (int) message.response_body.length,
105                                              url, null, Html.ParserOption.NOERROR | Html.ParserOption.NOWARNING);
106         }
107
108         private void save_city_list () {
109                 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "w");
110                 if (list_file == null)
111                         return;
112
113                 foreach (unowned City city in city_list) {
114                         if (city.north != 0.0 || city.south != 0.0 || city.east != 0.0 || city.west != 0.0)
115                                 list_file.printf ("%d\t%s\t%f\t%f\t%f\t%f\t%f\t%f\n", city.number, city.name, city.latitude, city.longitude, city.north, city.south, city.east, city.west);
116                         else if (city.latitude != 0.0 || city.longitude != 0.0)
117                                 list_file.printf ("%d\t%s\t%f\t%f\n", city.number, city.name, city.latitude, city.longitude);
118                         else
119                                 list_file.printf ("%d\t%s\n", city.number, city.name);
120                 }
121         }
122
123         private bool load_city_list () {
124                 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "r");
125                 if (list_file == null)
126                         list_file = FileStream.open ("/usr/share/beifahrer/city_list", "r");
127                 if (list_file == null)
128                         return false;
129
130                 city_list = new List<City> ();
131                 string line = list_file.read_line ();
132                 while (line != null) {
133                         var split_line = line.split ("\t");
134                         if (split_line.length < 2)
135                                 continue;
136                         int number = split_line[0].to_int ();
137                         weak string name = split_line[1];
138
139                         var city = new City (number, name);
140                         if (split_line.length >= 4) {
141                                 city.latitude = split_line[2].to_double ();
142                                 city.longitude = split_line[3].to_double ();
143                         }
144                         if (split_line.length >= 8) {
145                                 city.north = split_line[4].to_double ();
146                                 city.south = split_line[5].to_double ();
147                                 city.east = split_line[6].to_double ();
148                                 city.west = split_line[7].to_double ();
149                         }
150                         city_list.append ((owned) city);
151
152                         line = list_file.read_line ();
153                 }
154
155                 return true;
156         }
157
158         public unowned List<City>? get_city_list () {
159                 if (city_list != null)
160                         return city_list;
161
162                 if (load_city_list ())
163                         return city_list;
164
165                 return null;
166         }
167
168         public async unowned List<City>? download_city_list () {
169                 var doc = yield get_html_document (BASE_URI);
170                 if (doc == null) {
171                         stderr.printf ("Error: parsing failed\n");
172                         return null;
173                 }
174
175                 var form = search_tag_by_id (doc->children, "form", "search_national_form");
176                 if (form == null) {
177                         stderr.printf ("Error: does not contain search_national_form\n");
178                         return null;
179                 }
180
181                 var select = search_tag_by_name (form->children, "select", "city_from");
182                 if (select == null) {
183                         stderr.printf ("Error: does not contain city_from\n");
184                         return null;
185                 }
186
187                 city_list = new List<City> ();
188                 for (var n = select->children; n != null; n = n->next) {
189                         if (n->name == "option" && n->children != null && n->children->name == "text") {
190                                 int number = n->get_prop ("value").to_int ();
191                                 // Skip 0 "Alle St.dte"
192                                 if (number == 0)
193                                         continue;
194                                 var city = new City(number,
195                                                     n->children->content);
196                                 city_list.append ((owned) city);
197                         }
198                 }
199
200                 // TODO: get coordinates
201
202                 save_city_list ();
203
204                 return city_list;
205         }
206
207         private int get_city_number (string name) {
208                 foreach (unowned City city in city_list) {
209                         if (city.name == name)
210                                 return city.number;
211                 }
212                 return 0;
213         }
214
215         public unowned City find_nearest_city (double latitude, double longitude) {
216                 unowned City result = null;
217                 double min_distance = 0.0;
218                 bool in_result = false;
219
220                 foreach (unowned City city in city_list) {
221                         double lat = latitude - city.latitude;
222                         double lng = longitude - city.longitude;
223                         double distance = lat * lat + lng * lng;
224                         bool in_city = ((city.south <= latitude <= city.north) &&
225                                         (city.west <= longitude <= city.east));
226
227                         if ((result == null) ||
228                             (in_city && !in_result) ||
229                             (in_city && in_result && distance / city.bb_area () < min_distance / result.bb_area ()) ||
230                             (!in_city && !in_result && distance < min_distance)) {
231                                 result = city;
232                                 min_distance = distance;
233                                 in_result = in_city;
234                         }
235                 }
236
237                 return result;
238         }
239
240         public async List<Lift>? get_lift_list (string city_from, int radius_from, string city_to, int radius_to, Date date, int tolerance = 0) {
241                 if (city_list == null)
242                         get_city_list ();
243
244                 int num_from = get_city_number (city_from);
245                 if (num_from == 0) {
246                         stderr.printf ("Unknown city: %s\n", city_to);
247                         return null;
248                 }
249
250                 int num_to = get_city_number (city_to);
251                 if (num_to == 0) {
252                         stderr.printf ("Unknown city: %s\n", city_to);
253                         return null;
254                 }
255
256                 string url = BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
257                         city_from,
258                         city_to
259                 );
260
261                 url += "?type=b&city_from=%d&radius_from=%d&city_to=%d&radius_to=%d".printf (
262                         num_from,
263                         radius_from,
264                         num_to,
265                         radius_to
266                 );
267
268                 url += "&date=date&day=%d&month=%d&year=%d&tolerance=%d&smoking=&avg_speed=&".printf (
269                         date.get_day (),
270                         date.get_month (),
271                         date.get_year (),
272                         tolerance
273                 );
274
275                 var doc = yield get_html_document (url);
276                 if (doc == null) {
277                         stderr.printf ("Error: parsing failed\n");
278                         return null;
279                 }
280
281                 var table = search_tag_by_class (doc->children, "table", "list p_15");
282                 if (table == null) {
283                         stderr.printf ("Error: does not contain list p_15 table\n");
284                         return null;
285                 }
286
287                 var list = new List<Lift> ();
288                 for (var n = table->children; n != null; n = n->next) {
289                         if (n->name == "tr") {
290                                 var lift = parse_lift_row (n->children);
291                                 if (lift.city_from != null) // Skip the title row
292                                         list.append ((owned) lift);
293                         }
294                 }
295
296                 // Error message?
297                 var div = table->next;
298                 if (div != null && div->get_prop ("class") == "error-message") {
299                         if (div->children == null || div->children->content == null ||
300                             !div->children->content.has_prefix ("Es sind leider noch keine Einträge vorhanden.")) {
301                                 stderr.printf ("Got an unknown error message!\n");
302                                 if (div->children != null && div->children->content != null)
303                                         stderr.printf ("\"%s\"\n", div->children->content);
304                         }
305                 }
306
307                 return list;
308         }
309
310         Lift parse_lift_row (Xml.Node* node) {
311                 var lift = new Lift ();
312                 int i = 0;
313                 for (var n = node; n != null; n = n->next) {
314                         if (n->name == "td") {
315                                 var n2 = n->children;
316                                 if (n2 != null) {
317                                         if (n2->name == "a") {
318                                                 var href = n2->get_prop ("href");
319                                                 if (href != null && lift.href == null)
320                                                         lift.href = href;
321                                                 var n3 = n2->children;
322                                                 while (n3 != null) {
323                                                         if (n3->name == "text")
324                                                                 switch (i) {
325                                                                 case 0:
326                                                                         lift.city_from = n3->content;
327                                                                         break;
328                                                                 case 1:
329                                                                         lift.city_to = n3->content;
330                                                                         break;
331                                                                 case 2:
332                                                                         parse_date (n3->content, out lift.time);
333                                                                         break;
334                                                                 case 3:
335                                                                         parse_time (n3->content.strip (), out lift.time);
336                                                                         break;
337                                                                 case 4:
338                                                                         lift.places = n3->content.to_int ();
339                                                                         break;
340                                                                 case 5:
341                                                                         lift.price = n3->content;
342                                                                         break;
343                                                                 default:
344                                                                         print ("TEXT:%s\n", n3->content);
345                                                                         break;
346                                                                 }
347                                                         if (n3->name == "span") {
348                                                                 string class = n3->get_prop ("class");
349                                                                 if (class == "icon_smoker")
350                                                                         lift.flags |= LiftFlags.SMOKER;
351                                                                 else if (class == "icon_non_smoker")
352                                                                         lift.flags |= LiftFlags.NON_SMOKER;
353                                                                 else if (class == "icon_adac")
354                                                                         lift.flags |= LiftFlags.ADAC_MEMBER;
355                                                                 else if (class == "icon_women")
356                                                                         lift.flags |= LiftFlags.WOMEN_ONLY;
357                                                                 else if (class != null)
358                                                                         print ("SPAN %s\n", class);
359                                                         }
360                                                         n3 = n3->next;
361                                                 }
362                                         }
363                                 }
364                                 i++;
365                         }
366                 }
367
368                 return lift;
369         }
370
371         public async bool update_lift_details (Lift lift) {
372                 string url = BASE_URI + lift.href;
373
374                 var doc = yield get_html_document (url);
375                 if (doc == null) {
376                         stderr.printf ("Error: parsing failed\n");
377                         return false;
378                 }
379
380                 var table = search_tag_by_class (doc->children, "table", "lift");
381                 if (table == null) {
382                         stderr.printf ("Error: does not contain lift table\n");
383                         return false;
384                 }
385
386                 Xml.Node* n;
387                 for (n = table->children; n != null; n = n->next) {
388                         if (n->name == "tr") {
389                                 var n2 = n->children;
390                                 if (n2 == null || n2->name != "td" ||
391                                     n2->children == null || n2->children->name != "text")
392                                         continue;
393
394                                 string text = n2->children->content;
395
396                                 if (text != "Strecke & Infos" && text != "&nbsp;" && !text.has_prefix ("\xc2\xa0") &&
397                                     text != "Datum" &&
398                                     text != "Freie Pl\xc3\xa4tze" &&
399                                     text != "Name" &&
400                                     text != "Handy" &&
401                                     text != "Telefon" &&
402                                     text != "Telefon 2" &&
403                                     text != "E-Mail 1" &&
404                                     text != "Details" &&
405                                     text != "Beschreibung")
406                                         continue;
407
408                                 n2 = n2->next;
409                                 if (n2 == null)
410                                         continue;
411
412                                 // Skip text between td nodes
413                                 if (n2->name == "text")
414                                         n2 = n2->next;
415
416                                 if (n2 == null || n2->name != "td" || n2->children == null)
417                                         continue;
418
419                                 if (n2->children->name == "img") {
420                                         // FIXME: email image
421                                          lift.email_image_uri = n2->children->get_prop ("src");
422                                         continue;
423                                 }
424
425                                 if (n2->children->name == "div" && text == "Beschreibung") {
426                                         var n3 = n2->children->children;
427                                         lift.description = "";
428                                         while (n3 != null) {
429                                                 if (n3->name == "text")
430                                                         lift.description += n3->content.strip () + "\n";
431                                                 n3 = n3->next;
432                                         }
433                                         continue;
434                                 } else if (n2->children->name != "text") {
435                                         continue;
436                                 }
437
438                                 var text1 = n2->children->content.strip ();
439
440                                 if (text == "Freie Pl\xc3\xa4tze")
441                                         lift.places = text1.to_int ();
442                                 else if (text == "Name")
443                                         lift.name = text1;
444                                 else if (text == "Handy")
445                                         lift.cell = text1;
446                                 else if (text == "Telefon")
447                                         lift.phone = text1;
448                                 else if (text == "Telefon 2")
449                                         lift.phone2 = text1;
450                                 else if (text == "E-Mail 1")
451                                         lift.email = text1;
452                                 else if (text != "Strecke & Infos" && text != "&nbsp;" &&
453                                     !text.has_prefix ("\xc2\xa0") && text != "Datum" &&
454                                     text != "Details")
455                                         continue;
456
457                                 n2 = n2->next;
458                                 if (n2 == null)
459                                         continue;
460
461                                 // Skip text between td nodes
462                                 if (n2->name == "text")
463                                         n2 = n2->next;
464
465                                 if (n2 == null || n2->name != "td" ||
466                                     n2->children == null)
467                                         continue;
468
469                                 if (n2->children->name == "span" &&
470                                     n2->children->get_prop ("class") == "icon_non_smoker") {
471                                         lift.flags |= LiftFlags.NON_SMOKER;
472                                         continue;
473                                 } else if (n2->children->name == "span" &&
474                                     n2->children->get_prop ("class") == "icon_smoker") {
475                                         lift.flags |= LiftFlags.SMOKER;
476                                         continue;
477                                 } else if (n2->children->name != "text")
478                                         continue;
479
480                                 var text2 = n2->children->content.strip ();
481
482                                 if (text1 == "von")
483                                         lift.city_from = text2;
484                                 else if (text1.has_prefix ("\xc3\xbc"))
485                                         lift.city_via.append (text2);
486                                 else if (text1 == "nach")
487                                         lift.city_to = text2;
488                                 else if (text1 == "Datum")
489                                         parse_date (text2, out lift.time);
490                                 else if (text1 == "Uhrzeit")
491                                         parse_time (text2, out lift.time);
492                                 else if (text1 == "Raucher")
493                                         print ("Raucher: %s\n", text2);
494                                 else if (text1 == "Fahrpreis")
495                                         lift.price = text2;
496                                 else if (text1 == "ADAC-Mitglied" && text2 != "nein")
497                                         lift.flags |= LiftFlags.ADAC_MEMBER;
498                         }
499                 }
500
501                 // The paragraph after the table contains the date of last modification
502                 var p = table->next;
503                 for (n = p->children; n != null; n = n->next) {
504                         if (n->name != "text")
505                                 continue;
506
507                         var s = n->content.strip ();
508                         if (s.has_prefix ("Letztmalig aktualisiert am "))
509                                 lift.modified = s.offset (27).dup (); // "Do 15.04.2010 20:32"
510                 }
511
512                 return true;
513         }
514
515         Xml.Node* search_tag_by_property (Xml.Node* node, string tag, string prop, string val) requires (node != null) {
516                 for (var n = node; n != null; n = n->next) {
517                         if (n->name == tag && n->get_prop (prop) == val)
518                                 return n;
519                         if (n->children != null) {
520                                 var found = search_tag_by_property (n->children, tag, prop, val);
521                                 if (found != null)
522                                         return found;
523                         }
524                 }
525                 return null;
526         }
527
528         Xml.Node* search_tag_by_id (Xml.Node* node, string tag, string id) requires (node != null) {
529                 return search_tag_by_property (node, tag, "id", id);
530         }
531
532         Xml.Node* search_tag_by_name (Xml.Node* node, string tag, string name) requires (node != null) {
533                 return search_tag_by_property (node, tag, "name", name);
534         }
535
536         Xml.Node* search_tag_by_class (Xml.Node* node, string tag, string @class) requires (node != null) {
537                 return search_tag_by_property (node, tag, "class", @class);
538         }
539
540         void parse_date (string date, out Time time) {
541                 int year;
542                 if (date.length == 11)
543                         date = date.offset (3);
544                 if (date.length != 8)
545                         return;
546                 var res = date.scanf ("%02d.%02d.%02d", out time.day, out time.month, out year);
547                 time.year = year + 2000;
548         }
549
550         void parse_time (string time, out Time result) {
551                 var res = time.scanf ("%d.%02d Uhr", out result.hour, out result.minute);
552         }
553 }