/* This file is part of Beifahrer. * * Copyright (C) 2010 Philipp Zabel * * Beifahrer is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Beifahrer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Beifahrer. If not, see . */ [Compact] public class City { public int number; public string name; public double latitude; public double longitude; public double north; public double south; public double east; public double west; public City (int _number, string _name) { number = _number; name = _name; latitude = 0.0; longitude = 0.0; } internal double bb_area () { return (north - south) * (east - west); } } public enum LiftFlags { SMOKER = 1, NON_SMOKER = 2, ADAC_MEMBER = 4, WOMEN_ONLY = 8 } public class Lift : Object { public string city_from; public string city_to; public string date; public string time; public int places; public string price; public LiftFlags flags; public string href; public List city_via; public string name; public string cell; public string phone; public string phone2; public string email; public string description; public string modified; } public class AdacMitfahrclub { const string BASE_URI = "http://mitfahrclub.adac.de"; string response; int size; List city_list = null; static size_t write_memory_cb (void* ptr, size_t size, size_t nmemb, void* data) { unowned AdacMitfahrclub self = (AdacMitfahrclub) data; self.response += ((string) ptr).ndup (size * nmemb); self.size += (int) (size * nmemb); return size * nmemb; } private Html.Doc* get_html_document (string url) { var handle = new Curl.EasyHandle (); handle.setopt (Curl.Option.URL, url); handle.setopt (Curl.Option.WRITEFUNCTION, write_memory_cb); handle.setopt (Curl.Option.WRITEDATA, (void*) this); this.response = ""; this.size = 0; handle.perform (); return Html.Doc.read_memory ((char[]) this.response, this.size, url, null, Html.ParserOption.NOERROR | Html.ParserOption.NOWARNING); } private void save_city_list () { FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "w"); if (list_file == null) return; foreach (unowned City city in city_list) { if (city.north != 0.0 || city.south != 0.0 || city.east != 0.0 || city.west != 0.0) 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); else if (city.latitude != 0.0 || city.longitude != 0.0) list_file.printf ("%d\t%s\t%f\t%f\n", city.number, city.name, city.latitude, city.longitude); else list_file.printf ("%d\t%s\n", city.number, city.name); } } private bool load_city_list () { FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "r"); if (list_file == null) list_file = FileStream.open ("/usr/share/beifahrer/city_list", "r"); if (list_file == null) return false; city_list = new List (); string line = list_file.read_line (); while (line != null) { var split_line = line.split ("\t"); if (split_line.length < 2) continue; int number = split_line[0].to_int (); weak string name = split_line[1]; var city = new City (number, name); if (split_line.length >= 4) { city.latitude = split_line[2].to_double (); city.longitude = split_line[3].to_double (); } if (split_line.length >= 8) { city.north = split_line[4].to_double (); city.south = split_line[5].to_double (); city.east = split_line[6].to_double (); city.west = split_line[7].to_double (); } city_list.append ((owned) city); line = list_file.read_line (); } return true; } public unowned List? get_city_list () { if (city_list != null) return city_list; if (load_city_list ()) return city_list; var doc = get_html_document (BASE_URI); if (doc == null) { print ("Error: parsing failed"); print ("%s\n", this.response); return null; } var form = search_tag_by_id (doc->children, "form", "search_national_form"); if (form == null) { print ("Error: does not contain search_national_form"); print ("%s\n", this.response); return null; } var select = search_tag_by_name (form->children, "select", "city_from"); if (select == null) { print ("Error: does not contain city_from"); print ("%s\n", this.response); return null; } city_list = new List (); for (var n = select->children; n != null; n = n->next) { if (n->name == "option" && n->children != null && n->children->name == "text") { int number = n->get_prop ("value").to_int (); // Skip 0 "Alle St.dte" if (number == 0) continue; var city = new City(number, n->children->content); city_list.append ((owned) city); } } // TODO: get coordinates save_city_list (); return city_list; } private int get_city_number (string name) { foreach (unowned City city in city_list) { if (city.name == name) return city.number; } return 0; } public unowned City find_nearest_city (double latitude, double longitude) { unowned City result = null; double min_distance = 0.0; bool in_result = false; foreach (unowned City city in city_list) { double lat = latitude - city.latitude; double lng = longitude - city.longitude; double distance = lat * lat + lng * lng; bool in_city = ((city.south <= latitude <= city.north) && (city.west <= longitude <= city.east)); if ((result == null) || (in_city && !in_result) || (in_city && in_result && distance / city.bb_area () < min_distance / result.bb_area ()) || (!in_city && !in_result && distance < min_distance)) { result = city; min_distance = distance; in_result = in_city; } } return result; } public List? get_lift_list (string city_from, string city_to, Date date, int tolerance = 0) { if (city_list == null) get_city_list (); int num_from = get_city_number (city_from); if (num_from == 0) { stderr.printf ("Unknown city: %s\n", city_to); return null; } int num_to = get_city_number (city_to); if (num_to == 0) { stderr.printf ("Unknown city: %s\n", city_to); return null; } string url = BASE_URI + "/mitfahrclub/%s/%s/b.html".printf ( city_from, city_to ); url += "?type=b&city_from=%d&radius_from=0&city_to=%d&radius_to=0".printf ( num_from, num_to ); url += "&date=date&day=%d&month=%d&year=%d&tolerance=%d&smoking=&avg_speed=&".printf ( date.get_day (), date.get_month (), date.get_year (), tolerance ); var doc = get_html_document (url); if (doc == null) { print ("Error: parsing failed"); print ("%s\n", this.response); return null; } var table = search_tag_by_class (doc->children, "table", "list p_15"); if (table == null) { print ("Error: does not contain list p_15 table"); print ("%s\n", this.response); return null; } var list = new List (); for (var n = table->children; n != null; n = n->next) { if (n->name == "tr") { var lift = parse_lift_row (n->children); if (lift.city_from != null) // Skip the title row list.append ((owned) lift); } } // Error message? var div = table->next; if (div != null && div->get_prop ("class") == "error-message") { if (div->children == null || div->children->content == null || !div->children->content.has_prefix ("Es sind leider noch keine Einträge vorhanden.")) { stderr.printf ("Got an unknown error message!\n"); if (div->children != null && div->children->content != null) stderr.printf ("\"%s\"\n", div->children->content); } } return list; } Lift parse_lift_row (Xml.Node* node) { var lift = new Lift (); int i = 0; for (var n = node; n != null; n = n->next) { if (n->name == "td") { var n2 = n->children; if (n2 != null) { if (n2->name == "a") { var href = n2->get_prop ("href"); if (href != null && lift.href == null) lift.href = href; var n3 = n2->children; while (n3 != null) { if (n3->name == "text") switch (i) { case 0: lift.city_from = n3->content; break; case 1: lift.city_to = n3->content; break; case 2: lift.date = n3->content; break; case 3: lift.time = n3->content; break; case 4: lift.places = n3->content.to_int (); break; case 5: lift.price = n3->content; break; default: print ("TEXT:%s\n", n3->content); break; } if (n3->name == "span") { string class = n3->get_prop ("class"); if (class == "icon_smoker") lift.flags |= LiftFlags.SMOKER; else if (class == "icon_non_smoker") lift.flags |= LiftFlags.NON_SMOKER; else if (class == "icon_adac") lift.flags |= LiftFlags.ADAC_MEMBER; else if (class == "icon_women") lift.flags |= LiftFlags.WOMEN_ONLY; else if (class != null) print ("SPAN %s\n", class); } n3 = n3->next; } } } i++; } } return lift; } public Lift? get_lift_details (string lift_url) { var lift = new Lift (); lift.href = lift_url; if (update_lift_details (lift)) return lift; else return null; } public bool update_lift_details (Lift lift) { string url = BASE_URI + lift.href; var doc = get_html_document (url); if (doc == null) { print ("Error: parsing failed"); print ("%s\n", this.response); return false; } var table = search_tag_by_class (doc->children, "table", "lift"); if (table == null) { print ("Error: does not contain lift table"); print ("%s\n", this.response); return false; } for (var n = table->children; n != null; n = n->next) { if (n->name == "tr") { var n2 = n->children; if (n2 == null || n2->name != "td" || n2->children == null || n2->children->name != "text") continue; string text = n2->children->content; if (text != "Strecke & Infos" && text != " " && !text.has_prefix ("\xc2\xa0") && text != "Datum" && text != "Freie Pl\xc3\xa4tze" && text != "Name" && text != "Handy" && text != "Telefon" && text != "Telefon 2" && text != "E-Mail 1" && text != "Details" && text != "Beschreibung") continue; n2 = n2->next; if (n2 == null) continue; // Skip text between td nodes if (n2->name == "text") n2 = n2->next; if (n2 == null || n2->name != "td" || n2->children == null) continue; if (n2->children->name == "img") { // FIXME: email image // n2->children->get_prop ("src")) continue; } if (n2->children->name == "div" && text == "Beschreibung") { var n3 = n2->children->children; lift.description = ""; while (n3 != null) { if (n3->name == "text") lift.description += n3->content.strip () + "\n"; n3 = n3->next; } continue; } else if (n2->children->name != "text") { continue; } var text1 = n2->children->content.strip (); if (text == "Datum") lift.date = text1; else if (text == "Freie Pl\xc3\xa4tze") lift.places = text1.to_int (); else if (text == "Name") lift.name = text1; else if (text == "Handy") lift.cell = text1; else if (text == "Telefon") lift.phone = text1; else if (text == "Telefon 2") lift.phone2 = text1; else if (text == "E-Mail 1") lift.email = text1; else if (text != "Strecke & Infos" && text != " " && !text.has_prefix ("\xc2\xa0") && text != "Datum" && text != "Details") continue; n2 = n2->next; if (n2 == null) continue; // Skip text between td nodes if (n2->name == "text") n2 = n2->next; if (n2 == null || n2->name != "td" || n2->children == null) continue; if (n2->children->name == "span" && n2->children->get_prop ("class") == "icon_non_smoker") { lift.flags |= LiftFlags.NON_SMOKER; continue; } else if (n2->children->name == "span" && n2->children->get_prop ("class") == "icon_smoker") { lift.flags |= LiftFlags.SMOKER; continue; } else if (n2->children->name != "text") continue; var text2 = n2->children->content.strip (); if (text1 == "von") lift.city_from = text2; else if (text1.has_prefix ("\xc3\xbc")) lift.city_via.append (text2); else if (text1 == "nach") lift.city_to = text2; else if (text1 == "Datum") lift.date = text2; else if (text1 == "Uhrzeit") lift.time = text2; else if (text1 == "Raucher") print ("Raucher: %s\n", text2); else if (text1 == "Fahrpreis") lift.price = text2; else if (text1 == "ADAC-Mitglied" && text2 != "nein") lift.flags |= LiftFlags.ADAC_MEMBER; } } // The paragraph after the table contains the date of last modification var p = table->next; for (var n = p->children; n != null; n = n->next) { if (n->name != "text") continue; var s = n->content.strip (); if (s.has_prefix ("Letztmalig aktualisiert am ")) lift.modified = s.offset (27).dup (); // "Do 15.04.2010 20:32" } return true; } Xml.Node* search_tag_by_property (Xml.Node* node, string tag, string prop, string val) requires (node != null) { for (var n = node; n != null; n = n->next) { if (n->name == tag && n->get_prop (prop) == val) return n; if (n->children != null) { var found = search_tag_by_property (n->children, tag, prop, val); if (found != null) return found; } } return null; } Xml.Node* search_tag_by_id (Xml.Node* node, string tag, string id) requires (node != null) { return search_tag_by_property (node, tag, "id", id); } Xml.Node* search_tag_by_name (Xml.Node* node, string tag, string name) requires (node != null) { return search_tag_by_property (node, tag, "name", name); } Xml.Node* search_tag_by_class (Xml.Node* node, string tag, string @class) requires (node != null) { return search_tag_by_property (node, tag, "class", @class); } }