1 /* This file is part of Beifahrer.
3 * Copyright (C) 2010 Philipp Zabel
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.
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.
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/>.
23 public double latitude;
24 public double longitude;
30 public City (int _number, string _name) {
37 internal double bb_area () {
38 return (north - south) * (east - west);
42 public enum LiftFlags {
49 public class Lift : Object {
50 public string city_from;
51 public string city_to;
56 public LiftFlags flags;
60 public List<string> city_via;
66 public string description;
67 public string modified;
70 public class AdacMitfahrclub {
71 const string BASE_URI = "http://mitfahrclub.adac.de";
75 List<City> city_list = null;
77 static size_t write_memory_cb (void* ptr, size_t size, size_t nmemb, void* data) {
78 unowned AdacMitfahrclub self = (AdacMitfahrclub) data;
80 self.response += ((string) ptr).ndup (size * nmemb);
81 self.size += (int) (size * nmemb);
86 private Html.Doc* get_html_document (string url) {
87 var handle = new Curl.EasyHandle ();
89 handle.setopt (Curl.Option.URL, url);
90 handle.setopt (Curl.Option.WRITEFUNCTION, write_memory_cb);
91 handle.setopt (Curl.Option.WRITEDATA, (void*) this);
98 return Html.Doc.read_memory ((char[]) this.response, this.size,
99 url, null, Html.ParserOption.NOERROR | Html.ParserOption.NOWARNING);
102 private void save_city_list () {
103 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "w");
104 if (list_file == null)
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);
113 list_file.printf ("%d\t%s\n", city.number, city.name);
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)
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)
130 int number = split_line[0].to_int ();
131 weak string name = split_line[1];
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 ();
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 ();
144 city_list.append ((owned) city);
146 line = list_file.read_line ();
152 public unowned List<City>? get_city_list () {
153 if (city_list != null)
156 if (load_city_list ())
159 var doc = get_html_document (BASE_URI);
161 print ("Error: parsing failed");
162 print ("%s\n", this.response);
166 var form = search_tag_by_id (doc->children, "form", "search_national_form");
168 print ("Error: does not contain search_national_form");
169 print ("%s\n", this.response);
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);
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"
187 var city = new City(number,
188 n->children->content);
189 city_list.append ((owned) city);
193 // TODO: get coordinates
200 private int get_city_number (string name) {
201 foreach (unowned City city in city_list) {
202 if (city.name == name)
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;
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));
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)) {
225 min_distance = distance;
233 public List<Lift>? get_lift_list (string city_from, string city_to, Date date) {
234 if (city_list == null)
237 int num_from = get_city_number (city_from);
239 stderr.printf ("Unknown city: %s\n", city_to);
243 int num_to = get_city_number (city_to);
245 stderr.printf ("Unknown city: %s\n", city_to);
249 string url = BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
254 url += "?type=b&city_from=%d&radius_from=0&city_to=%d&radius_to=0".printf (
261 url += "&date=date&day=%d&month=%d&year=%d&tolerance=%d&smoking=&avg_speed=&".printf (
268 var doc = get_html_document (url);
270 print ("Error: parsing failed");
271 print ("%s\n", this.response);
275 var table = search_tag_by_class (doc->children, "table", "list p_15");
277 print ("Error: does not contain list p_15 table");
278 print ("%s\n", this.response);
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);
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);
305 Lift parse_lift_row (Xml.Node* node) {
306 var lift = new Lift ();
308 for (var n = node; n != null; n = n->next) {
309 if (n->name == "td") {
310 var n2 = n->children;
312 if (n2->name == "a") {
313 var href = n2->get_prop ("href");
314 if (href != null && lift.href == null)
316 var n3 = n2->children;
318 if (n3->name == "text")
321 lift.city_from = n3->content;
324 lift.city_to = n3->content;
327 lift.date = n3->content;
330 lift.time = n3->content;
333 lift.places = n3->content.to_int ();
336 lift.price = n3->content;
339 print ("TEXT:%s\n", n3->content);
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);
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))
375 public bool update_lift_details (Lift lift) {
376 string url = BASE_URI + lift.href;
378 var doc = get_html_document (url);
380 print ("Error: parsing failed");
381 print ("%s\n", this.response);
385 var table = search_tag_by_class (doc->children, "table", "lift");
387 print ("Error: does not contain lift table");
388 print ("%s\n", this.response);
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")
399 string text = n2->children->content;
401 if (text != "Strecke & Infos" && text != " " && !text.has_prefix ("\xc2\xa0") &&
403 text != "Freie Pl\xc3\xa4tze" &&
407 text != "Telefon 2" &&
408 text != "E-Mail 1" &&
410 text != "Beschreibung")
417 // Skip text between td nodes
418 if (n2->name == "text")
421 if (n2 == null || n2->name != "td" || n2->children == null)
424 if (n2->children->name == "img") {
425 // FIXME: email image
426 // n2->children->get_prop ("src"))
430 if (n2->children->name == "div" && text == "Beschreibung") {
431 var n3 = n2->children->children;
432 lift.description = "";
434 if (n3->name == "text")
435 lift.description += n3->content.strip () + "\n";
439 } else if (n2->children->name != "text") {
443 var text1 = n2->children->content.strip ();
447 else if (text == "Freie Pl\xc3\xa4tze")
448 lift.places = text1.to_int ();
449 else if (text == "Name")
451 else if (text == "Handy")
453 else if (text == "Telefon")
455 else if (text == "Telefon 2")
457 else if (text == "E-Mail 1")
459 else if (text != "Strecke & Infos" && text != " " &&
460 !text.has_prefix ("\xc2\xa0") && text != "Datum" &&
468 // Skip text between td nodes
469 if (n2->name == "text")
472 if (n2 == null || n2->name != "td" ||
473 n2->children == null)
476 if (n2->children->name == "span" &&
477 n2->children->get_prop ("class") == "icon_non_smoker") {
478 lift.flags |= LiftFlags.NON_SMOKER;
480 } else if (n2->children->name == "span" &&
481 n2->children->get_prop ("class") == "icon_smoker") {
482 lift.flags |= LiftFlags.SMOKER;
484 } else if (n2->children->name != "text")
487 var text2 = n2->children->content.strip ();
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")
497 else if (text1 == "Uhrzeit")
499 else if (text1 == "Raucher")
500 print ("Raucher: %s\n", text2);
501 else if (text1 == "Fahrpreis")
503 else if (text1 == "ADAC-Mitglied" && text2 != "nein")
504 lift.flags |= LiftFlags.ADAC_MEMBER;
508 // The paragraph after the table contains the date of last modification
510 for (var n = p->children; n != null; n = n->next) {
511 if (n->name != "text")
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"
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)
526 if (n->children != null) {
527 var found = search_tag_by_property (n->children, tag, prop, val);
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);
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);
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);