bahn.rb 2.1.1 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|