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;
55 public LiftFlags flags;
59 public List<string> city_via;
65 public string email_image_uri;
66 public string description;
67 public string modified;
74 public class CallbackMessage : Soup.Message {
75 public SourceFunc callback;
77 public CallbackMessage (string url, SourceFunc? _callback) {
80 set_uri (new Soup.URI (url));
84 public class AdacMitfahrclub {
85 const string BASE_URI = "http://mitfahrclub.adac.de";
87 Soup.SessionAsync session;
88 List<City> city_list = null;
90 public AdacMitfahrclub () {
91 session = new Soup.SessionAsync ();
94 private void message_finished (Soup.Session session, Soup.Message message) {
95 Idle.add (((CallbackMessage) message).callback);
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);
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);
108 private void save_city_list () {
109 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "w");
110 if (list_file == null)
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);
119 list_file.printf ("%d\t%s\n", city.number, city.name);
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)
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)
136 int number = split_line[0].to_int ();
137 weak string name = split_line[1];
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 ();
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 ();
150 city_list.append ((owned) city);
152 line = list_file.read_line ();
158 public unowned List<City>? get_city_list () {
159 if (city_list != null)
162 if (load_city_list ())
168 public async unowned List<City>? download_city_list () {
169 var doc = yield get_html_document (BASE_URI);
171 stderr.printf ("Error: parsing failed\n");
175 var form = search_tag_by_id (doc->children, "form", "search_national_form");
177 stderr.printf ("Error: does not contain search_national_form\n");
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");
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"
194 var city = new City(number,
195 n->children->content);
196 city_list.append ((owned) city);
200 // TODO: get coordinates
207 private int get_city_number (string name) {
208 foreach (unowned City city in city_list) {
209 if (city.name == name)
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;
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));
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)) {
232 min_distance = distance;
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)
244 int num_from = get_city_number (city_from);
246 stderr.printf ("Unknown city: %s\n", city_to);
250 int num_to = get_city_number (city_to);
252 stderr.printf ("Unknown city: %s\n", city_to);
256 string url = BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
261 url += "?type=b&city_from=%d&radius_from=%d&city_to=%d&radius_to=%d".printf (
268 url += "&date=date&day=%d&month=%d&year=%d&tolerance=%d&smoking=&avg_speed=&".printf (
275 var doc = yield get_html_document (url);
277 stderr.printf ("Error: parsing failed\n");
281 var table = search_tag_by_class (doc->children, "table", "list p_15");
283 stderr.printf ("Error: does not contain list p_15 table\n");
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);
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);
310 Lift parse_lift_row (Xml.Node* node) {
311 var lift = new Lift ();
313 for (var n = node; n != null; n = n->next) {
314 if (n->name == "td") {
315 var n2 = n->children;
317 if (n2->name == "a") {
318 var href = n2->get_prop ("href");
319 if (href != null && lift.href == null)
321 var n3 = n2->children;
323 if (n3->name == "text")
326 lift.city_from = n3->content;
329 lift.city_to = n3->content;
332 parse_date (n3->content, out lift.time);
335 parse_time (n3->content.strip (), out lift.time);
338 lift.places = n3->content.to_int ();
341 lift.price = n3->content;
344 print ("TEXT:%s\n", n3->content);
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);
371 public async bool update_lift_details (Lift lift) {
372 string url = BASE_URI + lift.href;
374 var doc = yield get_html_document (url);
376 stderr.printf ("Error: parsing failed\n");
380 var table = search_tag_by_class (doc->children, "table", "lift");
382 stderr.printf ("Error: does not contain lift table\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")
394 string text = n2->children->content;
396 if (text != "Strecke & Infos" && text != " " && !text.has_prefix ("\xc2\xa0") &&
398 text != "Freie Pl\xc3\xa4tze" &&
402 text != "Telefon 2" &&
403 text != "E-Mail 1" &&
405 text != "Beschreibung")
412 // Skip text between td nodes
413 if (n2->name == "text")
416 if (n2 == null || n2->name != "td" || n2->children == null)
419 if (n2->children->name == "img") {
420 // FIXME: email image
421 lift.email_image_uri = n2->children->get_prop ("src");
425 if (n2->children->name == "div" && text == "Beschreibung") {
426 var n3 = n2->children->children;
427 lift.description = "";
429 if (n3->name == "text")
430 lift.description += n3->content.strip () + "\n";
434 } else if (n2->children->name != "text") {
438 var text1 = n2->children->content.strip ();
440 if (text == "Freie Pl\xc3\xa4tze")
441 lift.places = text1.to_int ();
442 else if (text == "Name")
444 else if (text == "Handy")
446 else if (text == "Telefon")
448 else if (text == "Telefon 2")
450 else if (text == "E-Mail 1")
452 else if (text != "Strecke & Infos" && text != " " &&
453 !text.has_prefix ("\xc2\xa0") && text != "Datum" &&
461 // Skip text between td nodes
462 if (n2->name == "text")
465 if (n2 == null || n2->name != "td" ||
466 n2->children == null)
469 if (n2->children->name == "span" &&
470 n2->children->get_prop ("class") == "icon_non_smoker") {
471 lift.flags |= LiftFlags.NON_SMOKER;
473 } else if (n2->children->name == "span" &&
474 n2->children->get_prop ("class") == "icon_smoker") {
475 lift.flags |= LiftFlags.SMOKER;
477 } else if (n2->children->name != "text")
480 var text2 = n2->children->content.strip ();
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")
496 else if (text1 == "ADAC-Mitglied" && text2 != "nein")
497 lift.flags |= LiftFlags.ADAC_MEMBER;
501 // The paragraph after the table contains the date of last modification
503 for (n = p->children; n != null; n = n->next) {
504 if (n->name != "text")
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"
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)
519 if (n->children != null) {
520 var found = search_tag_by_property (n->children, tag, prop, val);
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);
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);
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);
540 void parse_date (string date, out Time time) {
542 if (date.length == 11)
543 date = date.offset (3);
544 if (date.length != 8)
546 var res = date.scanf ("%02d.%02d.%02d", out time.day, out time.month, out year);
547 time.year = year + 2000;
550 void parse_time (string time, out Time result) {
551 var res = time.scanf ("%d.%02d Uhr", out result.hour, out result.minute);