bahn.rb 2.1.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +7 -7
- data/README.md +34 -34
- data/changelog.md +10 -0
- data/lib/bahn.rb +11 -11
- data/lib/bahn/bahn_agent.rb +271 -177
- data/lib/bahn/bahn_route.rb +208 -145
- data/lib/bahn/bahn_routepart.rb +55 -34
- data/lib/bahn/bahn_station.rb +50 -36
- data/lib/bahn/version.rb +3 -3
- metadata +49 -27
data/lib/bahn/bahn_route.rb
CHANGED
|
@@ -1,145 +1,208 @@
|
|
|
1
|
-
# encoding: utf-8
|
|
2
|
-
|
|
3
|
-
module Bahn
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module Bahn
|
|
4
|
+
# The whole Route from A to B.
|
|
5
|
+
# This is created from the m.bahn.de detail view page and parses the given data.
|
|
6
|
+
# At the end you'll have a nice step by step navigation
|
|
7
|
+
# Feel free to refactor ;)
|
|
8
|
+
class Route
|
|
9
|
+
attr_accessor :parts, :notes, :start_type, :target_type, :price
|
|
10
|
+
|
|
11
|
+
# Initialize with a Mechanize::Page and a specific type
|
|
12
|
+
# The page should be the detail view of m.bahn.de
|
|
13
|
+
# Parameters:
|
|
14
|
+
# * page => Mechanize::Page
|
|
15
|
+
# * type => :door2door or :station2station
|
|
16
|
+
def initialize page, options = {}
|
|
17
|
+
options = {:start_type => :address, :target_type => :address, :include_coords => true}.merge(options)
|
|
18
|
+
@do_load = options[:include_coords]
|
|
19
|
+
self.start_type = options[:start_type]
|
|
20
|
+
self.target_type = options[:target_type]
|
|
21
|
+
self.notes = Array.new
|
|
22
|
+
summary_time = page.search("//div[contains(@class, 'querysummary2')]").text.strip
|
|
23
|
+
|
|
24
|
+
# we'll add it for now...
|
|
25
|
+
#includes = ["Einfache Fahrt", "Preisinformationen", "Weitere Informationen", "Start/Ziel mit äquivalentem Bahnhof ersetzt"]
|
|
26
|
+
#start_withs = ["Reiseprofil", "Hinweis", "Aktuelle Informationen"]
|
|
27
|
+
notes = Array.new
|
|
28
|
+
#notes << page.search("//div[contains(@class, 'haupt rline')]").map(&:text).map(&:strip)
|
|
29
|
+
notes << page.search("//div[contains(@class, 'red bold haupt')]").map(&:text).map(&:strip)
|
|
30
|
+
notes.each do |note|
|
|
31
|
+
self.notes << note if note.size > 0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
self.price = parse_price(page.search("//div[contains(@class, 'formular')]").map(&:text).map(&:strip))
|
|
35
|
+
|
|
36
|
+
change = page.search("//div[contains(@class, 'routeStart')]")
|
|
37
|
+
name = station_to_name change
|
|
38
|
+
|
|
39
|
+
type = page.search("//div[contains(@class, 'routeStart')]/following::*[1]").text.strip
|
|
40
|
+
last_lines = get_lines(change)
|
|
41
|
+
|
|
42
|
+
part = RoutePart.new
|
|
43
|
+
part.type = type
|
|
44
|
+
part.start_time = parse_date(summary_time.split("\n")[0...2].join(" "))
|
|
45
|
+
part.start_time -= last_lines.last.to_i.minutes if options[:start_type] == :address
|
|
46
|
+
part.start_delay = parse_delay(summary_time.split("\n")[0...2].join(" "))
|
|
47
|
+
part.start = Station.new({"value" => name, :load => options[:start_type] == :address ? :foot : :station, :do_load => @do_load})
|
|
48
|
+
part.platform_start = parse_platform(last_lines.last)
|
|
49
|
+
|
|
50
|
+
@parts = [part]
|
|
51
|
+
|
|
52
|
+
page.search("//div[contains(@class, 'routeChange')]").each_with_index do |change, idx|
|
|
53
|
+
part = RoutePart.new
|
|
54
|
+
name = station_to_name change
|
|
55
|
+
type = page.search("//div[contains(@class, 'routeChange')][#{idx+1}]/following::*[1]").text.strip
|
|
56
|
+
lines = change.text.split("\n")
|
|
57
|
+
|
|
58
|
+
part.type = type
|
|
59
|
+
part.start = Station.new({"value" => name, :load => :station, :do_load => @do_load})
|
|
60
|
+
|
|
61
|
+
lines = get_lines(change)
|
|
62
|
+
if lines.last.start_with?("ab")
|
|
63
|
+
part.start_time = parse_date(lines.last)
|
|
64
|
+
part.start_delay = parse_delay(lines.last)
|
|
65
|
+
part.platform_start = parse_platform(lines.last)
|
|
66
|
+
unless lines.first.starts_with?("an")
|
|
67
|
+
@parts.last.end_time = @parts.last.start_time + last_lines.last.to_i.minutes
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if lines.first.starts_with?("an")
|
|
72
|
+
@parts.last.end_time = parse_date(lines.first)
|
|
73
|
+
@parts.last.target_delay = parse_delay(lines.first)
|
|
74
|
+
@parts.last.platform_target = parse_platform(lines.first)
|
|
75
|
+
unless lines.last.start_with?("ab")
|
|
76
|
+
# Fußweg for part
|
|
77
|
+
part.start_time = @parts.last.end_time
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
last_lines = lines
|
|
82
|
+
|
|
83
|
+
@parts.last.target = part.start
|
|
84
|
+
@parts << part #unless @parts.last.start == part.start
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
change = page.search("//div[contains(@class, 'routeEnd')]")
|
|
89
|
+
name = station_to_name change
|
|
90
|
+
@parts.last.target = Station.new({"value" => name, :load => options[:target_type] == :address ? :foot : :station, :do_load => @do_load})
|
|
91
|
+
lines = get_lines(change)
|
|
92
|
+
|
|
93
|
+
if lines.first.starts_with?("an")
|
|
94
|
+
@parts.last.end_time = parse_date(lines.first)
|
|
95
|
+
@parts.last.target_delay = parse_delay(lines.first)
|
|
96
|
+
@parts.last.platform_target = parse_platform(lines.first)
|
|
97
|
+
else
|
|
98
|
+
@parts.last.end_time = @parts.last.start_time + last_lines.last.to_i.minutes
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Start time of the route
|
|
105
|
+
def start_time
|
|
106
|
+
@parts.first.start_time
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# End time of the route
|
|
110
|
+
def end_time
|
|
111
|
+
@parts.last.end_time
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Duration of a route
|
|
115
|
+
def duration
|
|
116
|
+
end_time_plus_delay = end_time + @parts.last.target_delay
|
|
117
|
+
duration_in_s = end_time_plus_delay.to_i - start_time.to_i
|
|
118
|
+
return duration_in_s
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Starting point of the route
|
|
122
|
+
def start
|
|
123
|
+
@parts.first.start
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Target point of the route
|
|
127
|
+
def target
|
|
128
|
+
@parts.last.target
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def has_delay?
|
|
132
|
+
@parts.any? { |part| (part.start_delay > 0 || part.target_delay > 0) }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def station_to_name change
|
|
138
|
+
change.search("span").select{|s| s.attributes["class"].value != "red"}.inject(" "){|r, s| r << s.text}.strip.gsub(/\+\d+/, "")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def get_lines change
|
|
142
|
+
change.text.split("\n").reject{|s| s.to_s.length == 0}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def parse_price(to_parse)
|
|
146
|
+
return Array.new unless to_parse.first.match("EUR")
|
|
147
|
+
tags = to_parse.uniq.map { |p| p.split(/\d+\,\d{1,2}.EUR/) }.flatten
|
|
148
|
+
tags.map! { |t| t.gsub(/\p{Space}/, " ").strip } # remove ugly whitespaces: ruby >= 1.9.3
|
|
149
|
+
prices = to_parse.uniq.map { |p| p.scan(/\d+\,\d{1,2}.EUR/) }.flatten.map { |p| p.gsub(",",".").to_f }
|
|
150
|
+
price_information = Array.new
|
|
151
|
+
(0...prices.size).each do |idx|
|
|
152
|
+
price_information << {
|
|
153
|
+
:price => prices[idx],
|
|
154
|
+
:class => tags[idx+1].scan(/\d\.\sKlasse/).first.scan(/\d/).first,
|
|
155
|
+
:details => tags[idx+1].split(/\d\.\sKlasse/).last
|
|
156
|
+
}
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
return price_information
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def parse_platform(to_parse)
|
|
163
|
+
return to_parse.split("Gl.").last.strip if to_parse.match("Gl.")
|
|
164
|
+
return nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def parse_delay(to_parse)
|
|
168
|
+
to_parse = to_parse.split("+") # + sign indicates delay information
|
|
169
|
+
|
|
170
|
+
if to_parse.size > 1 # extract delay information
|
|
171
|
+
delay_information = to_parse.last.split.first.to_i
|
|
172
|
+
else
|
|
173
|
+
delay_information = 0
|
|
174
|
+
end
|
|
175
|
+
return delay_information
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def parse_date to_parse
|
|
179
|
+
to_parse = to_parse.split("+") # clears time errors e.g.: "an 18:01 +4 Gl. 17"
|
|
180
|
+
|
|
181
|
+
# fix number of year digits from 2 to 4
|
|
182
|
+
to_parse = to_parse.first.gsub(".#{DateTime.now.year.to_s[2..4]} ", ".#{DateTime.now.year.to_s} ")
|
|
183
|
+
|
|
184
|
+
# fix missing year information in route parts (interesting for
|
|
185
|
+
# past or future connections)
|
|
186
|
+
unless to_parse.match(/\d{1,2}\.\d{1,2}\.\d{4}/)
|
|
187
|
+
tmp_date = DateTime.parse(to_parse[/\d{1,2}:\d{1,2}/])
|
|
188
|
+
|
|
189
|
+
tmp_date = DateTime.new(self.start_time.year,
|
|
190
|
+
self.start_time.month,
|
|
191
|
+
self.start_time.day,
|
|
192
|
+
tmp_date.hour,
|
|
193
|
+
tmp_date.min,
|
|
194
|
+
tmp_date.sec)
|
|
195
|
+
|
|
196
|
+
tmp_date = tmp_date +1 if tmp_date < self.start_time
|
|
197
|
+
to_parse = tmp_date.to_s
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
to_parse = DateTime.parse(to_parse).to_s
|
|
201
|
+
|
|
202
|
+
# fix timezone
|
|
203
|
+
time_zone = DateTime.now.in_time_zone("Berlin").strftime("%z")
|
|
204
|
+
to_parse = to_parse.gsub("+00:00", time_zone).gsub("+0000", time_zone)
|
|
205
|
+
return DateTime.parse(to_parse)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
data/lib/bahn/bahn_routepart.rb
CHANGED
|
@@ -1,34 +1,55 @@
|
|
|
1
|
-
# encoding: utf-8
|
|
2
|
-
|
|
3
|
-
module Bahn
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module Bahn
|
|
4
|
+
# Route Parts show a small step of the route from A to B with one specific type of transportation
|
|
5
|
+
# Example: "Am 2013-02-01 von 17:33 bis 17:47 : Heerdter Sandberg U, Düsseldorf nach Düsseldorf Hauptbahnhof via U 7"
|
|
6
|
+
class RoutePart
|
|
7
|
+
attr_accessor :start, :target, :type, :platform_start, :platform_target, :price, :start_time, :end_time, :start_delay, :target_delay
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@start_delay = 0
|
|
11
|
+
@target_delay = 0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Return a nicely formatted route
|
|
15
|
+
# Raises errors if not everything is set properly
|
|
16
|
+
def to_s
|
|
17
|
+
"Am %s von %s%s bis %s%s: %s (Gl. %s) nach %s (Gl. %s) via %s" %
|
|
18
|
+
[ start_time.to_date,
|
|
19
|
+
start_time.to_formatted_s(:time),
|
|
20
|
+
(start_delay > 0 ? " (+%i)" % start_delay : ""),
|
|
21
|
+
(end_time.to_date != start_time.to_date ? end_time.to_date.to_s + ' ' : "") + end_time.to_formatted_s(:time),
|
|
22
|
+
target_delay > 0 ? " (+%i)" % target_delay : "",
|
|
23
|
+
start.name, platform_start, target.name, platform_target, type]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Set the type, e.g. Fußweg
|
|
27
|
+
def type= val
|
|
28
|
+
@type = val.squeeze(" ")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def transport_type
|
|
32
|
+
short_type = self.type.split.first.downcase
|
|
33
|
+
if ["str", "u", "s", "re", "erb", "ic", "ice"].include? short_type
|
|
34
|
+
return :train
|
|
35
|
+
elsif ["bus", "ne"].include? short_type
|
|
36
|
+
return :bus
|
|
37
|
+
elsif "Fußweg" == short_type
|
|
38
|
+
return :foot
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# nothing else works
|
|
42
|
+
self.type
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ==(rp)
|
|
46
|
+
self.start == rp.start &&
|
|
47
|
+
self.target == rp.target &&
|
|
48
|
+
self.type == rp.type &&
|
|
49
|
+
self.platform_start == rp.platform_start &&
|
|
50
|
+
self.platform_target == rp.platform_target &&
|
|
51
|
+
self.start_time == rp.start_time &&
|
|
52
|
+
self.end_time == rp.end_time
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/bahn/bahn_station.rb
CHANGED
|
@@ -1,36 +1,50 @@
|
|
|
1
|
-
module Bahn
|
|
2
|
-
class Station
|
|
3
|
-
attr_accessor :lat, :lon, :distance, :name
|
|
4
|
-
|
|
5
|
-
def initialize json={}
|
|
6
|
-
self.name = json["value"] unless json["value"].nil?
|
|
7
|
-
|
|
8
|
-
if json[:do_load]
|
|
9
|
-
station = Agent.new.find_station(name) if json[:load] == :station
|
|
10
|
-
station = Agent.new.find_address(name) if json[:load] == :foot
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
if station.nil?
|
|
14
|
-
self.lat = json["ycoord"].insert(-7, ".") unless json["ycoord"].nil?
|
|
15
|
-
self.lon = json["xcoord"].insert(-7, ".") unless json["xcoord"].nil?
|
|
16
|
-
else
|
|
17
|
-
self.lat = station.lat
|
|
18
|
-
self.lon = station.lon
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def to_s
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
module Bahn
|
|
2
|
+
class Station
|
|
3
|
+
attr_accessor :lat, :lon, :distance, :name, :station_type
|
|
4
|
+
|
|
5
|
+
def initialize json={}
|
|
6
|
+
self.name = json["value"].to_s.gsub("(S-Bahn)", "(S)") unless json["value"].nil?
|
|
7
|
+
|
|
8
|
+
if json[:do_load]
|
|
9
|
+
station = Agent.new.find_station(name) if json[:load] == :station
|
|
10
|
+
station = Agent.new.find_address(name) if json[:load] == :foot
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
if station.nil?
|
|
14
|
+
self.lat = json["ycoord"].insert(-7, ".") unless json["ycoord"].nil?
|
|
15
|
+
self.lon = json["xcoord"].insert(-7, ".") unless json["xcoord"].nil?
|
|
16
|
+
else
|
|
17
|
+
self.lat = station.lat
|
|
18
|
+
self.lon = station.lon
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
return "#{self.name}" if (self.lat.nil? || self.lon.nil?)
|
|
24
|
+
return "#{self.name} (#{self.lat},#{self.lon})"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_coordinates
|
|
28
|
+
[lat, lon]
|
|
29
|
+
end
|
|
30
|
+
alias_method :coordinates, :to_coordinates
|
|
31
|
+
|
|
32
|
+
def == other
|
|
33
|
+
return false if other.nil?
|
|
34
|
+
remove_parenthesis(other.name) == remove_parenthesis(name)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
#############################################
|
|
39
|
+
private
|
|
40
|
+
#############################################
|
|
41
|
+
|
|
42
|
+
# Often we have stations like "Berlin Gesundbrunnen (S)" and "Berlin Gesundbrunnen"
|
|
43
|
+
# we want these stations to be different
|
|
44
|
+
def remove_parenthesis string
|
|
45
|
+
x = string.dup
|
|
46
|
+
while x.gsub!(/\([^()]*\)/,""); end
|
|
47
|
+
x.strip
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|