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.
@@ -1,145 +1,208 @@
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 :price, :parts, :notes, :start_type, :target_type
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
- summary_time = page.search("//div[contains(@class, 'querysummary2')]").text.gsub("\n", " ").strip
22
- html_parts = page.search("//div[contains(@class, 'haupt')]")
23
- price = html_parts.pop.text
24
- html_parts.pop
25
-
26
- route = ""
27
- html_parts.each do |part|
28
- text = part.text.strip
29
- next if text.start_with? "Reiseprofil" # not important
30
- if text.starts_with?("Hinweis", "Aktuelle Informationen")
31
- self.notes = "#{self.notes}\n#{text}" if !text.include?("Start/Ziel mit äquivalentem Bahnhof ersetzt")
32
- next
33
- end
34
-
35
- route << text << "\n"
36
- end
37
- route = route.split("\n")
38
-
39
- idx = 3
40
- @start = RoutePart.new
41
- @target = RoutePart.new
42
- if options[:start_type] == :address
43
- @start.start = Station.new({"value" => route[0], :load => :foot, :do_load => @do_load})
44
- @start.type = "Fußweg" # route[2]
45
- @start.end_time = DateTime.parse(summary_time.split("-").first.gsub(".13", ".2013"))
46
- @start.start_time = @start.end_time - route[1].to_i.minutes
47
- @start.target = route[3]
48
- elsif options[:start_type] == :station
49
- @start.start = Station.new({"value" => route[0], :load => :station, :do_load => @do_load})
50
- @start.start_time = DateTime.parse(@date.to_s + route[1])
51
- @start.type = route[2]
52
- @start.end_time = DateTime.parse(@date.to_s + route[3])
53
- @start.target = Station.new({"value" => route[4], :load => :station, :do_load => @do_load})
54
- idx = 4
55
- end
56
-
57
- @date = @start.start_time.to_date # otherwise all dates will be "today"
58
- create_parts idx, route
59
- if options[:target_type] == :station
60
- @target = @parts.last
61
- elsif options[:target_type] == :address
62
- @target.type = "Fußweg"
63
- @target.target = Station.new({"value" => route.last, :load => :foot, :do_load => @do_load})
64
- if summary_time.split("-").last.strip.length != 5
65
- # Date is included in the string
66
- @target.end_time = DateTime.parse(summary_time.split("-").last.gsub(".13", ".2013"))
67
- else
68
- # no date given, use start date
69
- @target.end_time = DateTime.parse("#{@start.start_time.to_date} #{summary_time.split("-").last}")
70
- end
71
-
72
- @target.end_time += route[route.length-3].to_i.minutes
73
- @parts << @target
74
- end
75
- end
76
-
77
- # Start time of the route
78
- def start_time
79
- @parts.first.start_time
80
- end
81
-
82
- # End time of the route
83
- def end_time
84
- @parts.last.end_time
85
- end
86
-
87
- # Starting point of the route
88
- def start
89
- @parts.first.start
90
- end
91
-
92
- # Target point of the route
93
- def target
94
- @parts.last.target
95
- end
96
-
97
- private
98
-
99
- # Create all general parts.
100
- # Set @parts, @target and @date first!
101
- def create_parts start_index, route
102
- @parts = [@start]
103
- i = start_index
104
- done_anything = false
105
- while i < route.length do
106
- if route[i..i+4].count != 5 || route[i..i+4].include?(nil)
107
- break
108
- end
109
-
110
- done_anything = true
111
- part = RoutePart.new
112
- part.start = Station.new({"value" => route[i], :load => :station, :do_load => @do_load})
113
- @parts.last.target = part.start
114
- part.type = route[i+2].squeeze
115
-
116
- begin
117
- part.start_time = DateTime.parse(@date.to_s + route[i+1])
118
- part.end_time = DateTime.parse(@date.to_s + route[i+3])
119
- part.target = Station.new({"value" => route[i+4], :load => :station, :do_load => @do_load})
120
- i += 4
121
- rescue ArgumentError
122
- # occures if there is a "Fußweg" in between
123
- part.start_time = @parts.last.end_time
124
- part.end_time = part.start_time + route[i+1].to_i.minutes
125
- part.target = Station.new({"value" => route[i+3], :load => :foot, :do_load => @do_load})
126
- i += 3
127
- end
128
-
129
- part.end_time += 1.day if part.end_time.hour < @start.start_time.hour
130
- part.start_time += 1.day if part.start_time.hour < @start.start_time.hour
131
-
132
- @target.start_time = part.end_time
133
- @target.start = part.target
134
-
135
- # we don't want to show Fußwege from and to the same station
136
- @parts << part unless part.start == part.target
137
- end
138
-
139
- unless done_anything
140
- @target.start_time = @parts.last.end_time
141
- @target.start = @parts.last.start
142
- end
143
- end
144
- end
145
- end
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
@@ -1,34 +1,55 @@
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, :price, :start_time, :end_time
8
-
9
- # Return a nicely formatted route
10
- # Raises errors if not everything is set properly
11
- def to_s
12
- "Am #{start_time.to_date} von #{start_time.to_formatted_s :time} bis #{end_time.to_date.to_s + ' ' if end_time.to_date != start_time.to_date}#{end_time.to_formatted_s :time} : #{start} nach #{target} via #{type}"
13
- end
14
-
15
- # Set the type, e.g. Fußweg
16
- def type= val
17
- @type = val.squeeze(" ")
18
- end
19
-
20
- def transport_type
21
- short_type = self.type.split.first.downcase
22
- if ["str", "u", "s", "re", "erb", "ic", "ice"].include? short_type
23
- return :train
24
- elsif ["bus", "ne"].include? short_type
25
- return :bus
26
- elsif "Fußweg" == short_type
27
- return :foot
28
- end
29
-
30
- # nothing else works
31
- self.type
32
- end
33
- end
34
- end
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
@@ -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
- "#{self.name} (#{self.lat},#{self.lon})"
24
- end
25
-
26
- def to_coordinates
27
- [lat, lon]
28
- end
29
- alias_method :coordinates, :to_coordinates
30
-
31
- def == other
32
- return false if other.nil?
33
- other.name == self.name
34
- end
35
- end
36
- end
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