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.1.2' 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 || 'summary' } 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 = WEIGHTS.include?(w) ? 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