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