summaryrefslogtreecommitdiff
path: root/blatube.rb
diff options
context:
space:
mode:
authorotherl <lgdmiller@gmail.com>2013-09-28 19:25:37 +0100
committerotherl <lgdmiller@gmail.com>2013-09-28 19:25:37 +0100
commitfef31338a3d5d93fb0cbe7f7a8a5f80ab648af9e (patch)
tree7e0ad4bc850d9eb8b30a7415589fcc6f0aa71e3e /blatube.rb
parentc40fe4bf6016e9f2aefac576f6eb24787b8f986f (diff)
Renamed the blatube file so that the version is not included.
Diffstat (limited to 'blatube.rb')
-rw-r--r--blatube.rb220
1 files changed, 220 insertions, 0 deletions
diff --git a/blatube.rb b/blatube.rb
new file mode 100644
index 0000000..0ecd5fb
--- /dev/null
+++ b/blatube.rb
@@ -0,0 +1,220 @@
+require 'optparse'
+require 'open-uri'
+require 'nokogiri'
+require 'yaml'
+require 'json'
+
+@output = []
+@line = nil
+@from_station = nil
+@to_station = nil
+@weight = 'peak_time'
+@change_time = 5
+@colour = false
+debug = false
+help = false
+ops = nil
+VERSION = 'v1.0.0'
+INFINITY = 1 << 64
+WEIGHTS = ['distance','peak_time','off_peak_time','unimpeded_time']
+
+STDIN.gets.split(' ').each{|arg| ARGV << arg} if ARGV == []
+args = ARGV.dup
+
+options = OptionParser.new do |opts|
+ opts.banner = "Usage: #{File.basename($0)} -h"
+ opts.on( '-s', '--status LINE', 'Get the current status of a tube line.' ) {|l| @line = l }
+ opts.on( '-f', '--from-station STATION', 'Find route from this station.' ) {|f| @from_station = f }
+ opts.on( '-t', '--to-station STATION', 'Find route to this station.' ) {|t| @to_station = t }
+ opts.on( '-w', '--weight WEIGHT', 'Shortest route using either distance, peak_time, off_peak_time or unimpeded_time.' ) {|w| @weight = w if WEIGHTS.include? w || 'peak_time' }
+ opts.on( '-c', '--change-time TIME', 'Your expected average change time in minutes.' ) {|c| @change_time = c.to_f.round(2) }
+ opts.on( '-C', '--colour', 'Enable line colours in the output.' ) { @colour = true }
+ opts.on( '-d', '--debug', 'Display debug info.' ) { debug = true }
+ opts.on( '-v', '--version', 'blatube version number.' ) { puts VERSION; exit }
+ opts.on( '-h', '--help', 'Display this screen.' ) { help = true; ops = opts }
+end;options.parse!
+
+@output << "args: #{args.inspect}" if debug
+if args == []
+ @output << "Usage: #{File.basename($0)} -h"
+ puts @output.join ' '
+ exit -1
+end
+
+class Array
+ def pairs
+ pairs = []
+ self.each_with_index do |element, index|
+ pairs << [element,self[index+1]]
+ end
+ pairs.pop
+ pairs
+ end
+end
+
+#TODO: add via_station
+def calculate_shortest_route
+ return unless interpret_user_input
+ @shortest_route = []
+ calculate_routes
+ traverse_route @to_station.upcase
+ build_route_output
+end
+
+def interpret_user_input
+ stations = @graph.keys.map {|station| station.split('_')[0]}.uniq.sort
+ found_from_station, @from_station = interpret stations, @from_station
+ found_to_station, @to_station = interpret stations, @to_station
+ return found_from_station && found_to_station
+end
+
+def interpret stations, station
+ found_station = false
+ if stations.include?(station.upcase)
+ found_station = true
+ elsif (most_likely = stations.map {|stat| stat.match("^#{station.upcase}.*") ? stat : nil}.compact) != []
+ if most_likely.size == 1
+ station = most_likely.first
+ found_station = true
+ else
+ @output << "Did you mean: #{most_likely.map {|stat| stat.split(' ').map {|word| word.capitalize}.join(' ')}.join(', ')}?"
+ end
+ elsif (less_likely = stations.map {|stat| stat.match(".*#{station.upcase}.*") ? stat : nil}.compact) != []
+ if less_likely.size == 1
+ station = less_likely.first
+ found_station = true
+ else
+ @output << "Did you mean: #{less_likely.map {|stat| stat.split(' ').map {|word| word.capitalize}.join(' ')}.join(', ')}?"
+ end
+ else
+ @output << "Could not find a matching station for '#{station}'."
+ end
+ return found_station, station
+end
+
+def calculate_routes
+ routes,distances = YAML::load(File.open("cache/#{@from_station}_#{@weight == 'distance' ? @weight : "#{@weight}_#{@change_time}"}.yaml", 'r'))
+ @distances = Hash.new(INFINITY).merge distances
+ @routes = Hash.new(-1).merge routes
+rescue Errno::ENOENT
+ add_change_time unless @weight == 'distance'
+ run_algorithm @graph, @graph.keys, @from_station.upcase
+ File.new("cache/#{@from_station}_#{@weight == 'distance' ? @weight : "#{@weight}_#{@change_time}"}.yaml", 'w') << [@routes,@distances].to_yaml
+end
+
+#TODO: changes on the same line (eg. 'CAMDEN TOWN')
+def add_change_time
+ @graph.each do |from_station,to_stations|
+ next unless current_line = from_station.split('_')[1]
+ to_stations.each do |to_station,data|
+ data[@weight] += @change_time unless data['line'] == current_line
+ end
+ end
+end
+
+def run_algorithm graph, nodes, start_node
+ @distances = Hash.new INFINITY
+ @routes = Hash.new -1
+ @distances[start_node] = 0
+ nodes_size = nodes.size
+ while nodes_size > 0
+ current_node = nodes.first
+ nodes.each {|node| current_node = node if @distances[node] < @distances[current_node]}
+ break if @distances[current_node] == INFINITY
+ nodes.delete current_node
+ nodes_size -= 1
+ graph[current_node].keys.each do |next_node|
+ new_distance = @distances[current_node] + graph[current_node][next_node][@weight]
+ if new_distance < @distances[next_node]
+ @distances[next_node] = new_distance
+ @routes[next_node] = current_node
+ end
+ end
+ end
+ @distances.delete_if {|node, distance| distance == INFINITY}
+end
+
+def traverse_route to_station
+ if @routes[to_station] != -1
+ traverse_route @routes[to_station]
+ end
+ @shortest_route << to_station
+end
+
+#TODO: a nicer way of doing this
+def build_route_output
+ line_colours = @colour ? YAML::load(File.open('line_colours.yaml')) : Hash.new('')
+ verbose_route = ''
+ pairs = @shortest_route.pairs
+ from,to = pairs.first
+ current_line = @graph[from][to]['line']
+ verbose_route << "#{line_colours[current_line]}#{from} - #{current_line} (#{@graph[from][to]['direction']})"
+ pairs[1..-2].each do |from,to|
+ next if (new_line = @graph[from][to]['line']) == current_line || @graph[from][to]['direction'] == nil
+ current_line = new_line
+ from_s = from.split('_')[0]
+ from_1 = from_s[0..(from_s.length/2.0 - 1)]
+ from_2 = from_s[(from_s.length/2.0)..-1]
+ verbose_route << " - #{from_1}#{line_colours['reset_colour']}#{line_colours[new_line]}#{from_2} - #{new_line} (#{@graph[from][to]['direction']})"
+ end
+ from,to = pairs.last
+ new_line = @graph[from][to]['line']
+ from_s = from.split('_')[0]
+ from_1 = from_s[0..(from_s.length/2.0 - 1)]
+ from_2 = from_s[(from_s.length/2.0)..-1]
+ verbose_route << " - #{from_1}#{line_colours['reset_colour']}#{line_colours[new_line]}#{from_2} - #{new_line} (#{@graph[from][to]['direction']})" unless new_line == current_line
+ verbose_route << " - #{to}#{line_colours['reset_colour']}"
+ @output << verbose_route
+ if @weight == 'distance'
+ @output << "Distance: #{@distances[@to_station.upcase].round(2)} km"
+ else
+ minutes = @distances[@to_station.upcase].round(2)
+ seconds = (minutes - minutes.to_i) * 60
+ @output << "Travel Time: #{minutes.to_i}:#{seconds.round}"
+ end
+end
+
+def get_line_status
+ xml = Nokogiri::HTML(open('http://cloud.tfl.gov.uk/TrackerNet/LineStatus')).remove_namespaces!
+ sorted_lines = xml.xpath('//linestatus').sort_by {|xml| xml.xpath('.//line/@name').text}
+ if @line && @line != 'summary'
+ l_s_xml = nil
+ sorted_lines.each {|xml| l_s_xml = xml and break if xml.xpath('.//line/@name').text.upcase.match @line.upcase}
+ @output << ("#{l_s_xml.xpath('.//line/@name').text}: #{l_s_xml.xpath('.//status/@description').text}. #{l_s_xml.xpath('./@statusdetails').text}" rescue "Could not find a line matching '#{@line}'")
+ else
+ line_summary = ''
+ sorted_lines.each {|xml| line_summary << "#{xml.xpath('.//line/@name').text}: #{xml.xpath('./@statusdetails').text} " unless xml.xpath('./@statusdetails').text == ''}
+ @output << (line_summary == '' ? 'Good Serice on all lines.' : "#{line_summary}Good Service on all other lines.")
+ end
+end
+
+#TODO: store graph as yaml in a way that works with add_change_time
+begin
+ if help
+ ops.to_s.split(/\n\s*|\s{2,}/).each{|opt| @output << opt}
+ elsif @line
+ get_line_status
+ elsif @from_station && @to_station
+ #@graph = YAML::load(File.open('graph.yaml'))
+ @graph = JSON.parse(File.open('graph.json').read)
+ calculate_shortest_route
+ elsif @from_station || @to_station
+ @output << 'Please specify the station you are travelling from (-f) and the station you are travelling to (-t).'
+ elsif ARGV.size == 2
+ #@graph = YAML::load(File.open('graph.yaml'))
+ @graph = JSON.parse(File.open('graph.json').read)
+ @from_station = ARGV[0]
+ @to_station = ARGV[1]
+ calculate_shortest_route
+ elsif ARGV.size == 1
+ @line = ARGV[0]
+ get_line_status
+ else
+ get_line_status
+ end
+ puts @output.join ' '
+rescue StandardError => e
+ puts (@output << e.message).join ' '
+ puts e.message
+ puts e.backtrace
+end