undergrounder 0.1.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.
data/bin/undergrounder ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'undergrounder'
4
+ require 'optparse'
5
+
6
+ options = {}
7
+
8
+ opt_parser = OptionParser.new do |opts|
9
+ opts.banner = "Usage: undergrounder [options]"
10
+
11
+ opts.on("-i", "--interactive", "Start an interactive tube search session") do |input|
12
+ puts "Welcome to Undergrounder version #{Undergrounder::VERSION}!"
13
+ puts "Enjoy your travel!"
14
+ puts "Please, enter a point of origin:"
15
+ source = gets.chomp
16
+ puts "Great!Now, enter a destination:"
17
+ destination = gets.chomp
18
+ puts "Calculating shortest path...."
19
+ Undergrounder.start!(source, destination)
20
+ end
21
+
22
+ opts.on("-s", "--start-server [OPT]", "Start an internal server") do |output|
23
+ puts "System is starting. Please , launch your browser and go to http://localhost:3000"
24
+ Dir.chdir(File.join(File.dirname(File.expand_path(__FILE__)), "../vendor/web_finder/")) do
25
+ system("padrino start")
26
+ end
27
+ end
28
+ end
29
+
30
+ opt_parser.parse!
@@ -0,0 +1,167 @@
1
+ require "undergrounder/version"
2
+ require 'yaml'
3
+
4
+ module Undergrounder
5
+ def self.start!(source, destination, web_request=false)
6
+ test = Undergrounder::Graph.new
7
+ test.load_from_yaml()
8
+ test.print_shortest_paths(source, destination, web_request)
9
+ end
10
+
11
+ # Graph Initialization
12
+ class Graph
13
+ attr_reader :graph, :nodes, :prev_station, :distance_modifier
14
+ def initialize
15
+ @graph = {}
16
+ @nodes = Array.new
17
+ @INFINITY = 1 << 64
18
+ @tube_list = []
19
+ end
20
+
21
+ # Edge data structure: { "Euston" => { "Warren Street" => [ 1, ["Northern", "Victoria"] ] ]}
22
+ # In this way i got track of all the lines between 2 stations
23
+ def add_edge(source, target, weight, line)
24
+ if (not @graph.has_key?(source))
25
+ @graph[source] = {target => [weight, [line] ] }
26
+ elsif(@graph[source].has_key?(target))
27
+ @graph[source][target][1] << line
28
+ @graph[source][target][1].uniq!
29
+ else
30
+ @graph[source][target] = [weight , [line] ]
31
+ end
32
+
33
+
34
+ if (not @graph.has_key?(target))
35
+ @graph[target] = {source => [weight, [line]] }
36
+ elsif(@graph[target].has_key?(source))
37
+ @graph[target][source][1] << line
38
+ @graph[target][source][1].uniq!
39
+ else
40
+ @graph[target][source] = [weight, [line] ]
41
+ end
42
+
43
+
44
+
45
+ if (not @nodes.include?(source))
46
+ @nodes << source
47
+ end
48
+ if (not @nodes.include?(target))
49
+ @nodes << target
50
+ end
51
+ end
52
+
53
+ # Base Dijkstra implementation
54
+ def dijkstra(source)
55
+ @distance = {}
56
+ @prev = {}
57
+
58
+ @nodes.each do |i|
59
+ @distance[i] = @INFINITY
60
+ @prev[i] = -1
61
+ end
62
+
63
+ @distance[source] = 0
64
+ q = @nodes.compact
65
+ @modifier = 0;
66
+ while q.size > 0
67
+ u = nil;
68
+
69
+ q.each do |min|
70
+ if (not u) or (@distance[min] and @distance[min] < @distance[u])
71
+ u = min
72
+ end
73
+ end
74
+
75
+ if (@distance[u] == @INFINITY)
76
+ break
77
+ end
78
+ q = q - [u]
79
+ @graph[u].keys.each do |v|
80
+ current_line = @graph[u][v][1]
81
+ modifier = 0 # setting station change modifier to a default 0
82
+ if @distance[u] > 0
83
+ modifier = changed_line?(@prev[u][1], current_line) == true ? 1 : 0
84
+ end
85
+ alt = @distance[u] + @graph[u][v][0] + modifier
86
+
87
+ if(alt < @distance[v])
88
+ @distance[v] = alt
89
+ @prev[v] = [u, current_line]
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # Helper method to check if there is a possible line change between station
96
+ def changed_line?(previous_line, current_line)
97
+ previous_line.each do |line|
98
+ if current_line.include?(line)
99
+ return false
100
+ end
101
+ end
102
+ return true
103
+ end
104
+
105
+ # print path method
106
+ def print_path(dest)
107
+ if @prev[dest] != -1
108
+ print_path @prev[dest][0]
109
+ end
110
+ @tube_list << dest
111
+ end
112
+
113
+
114
+ def load_from_yaml
115
+ lines = YAML.load_file(File.join(File.dirname(File.expand_path(__FILE__)), "../assets/tube_list.yml"))
116
+ lines.each do |line|
117
+ stations = line[1]
118
+ stations.each_with_index do |station,index|
119
+ add_edge(station.keys[0], station.values[0], 1, line[0])
120
+ end
121
+ end
122
+ end
123
+
124
+ # Gets all shortests paths using dijkstra
125
+ def shortest_paths(source, dest)
126
+ @source = source
127
+ dijkstra source
128
+ print_path dest
129
+ return @distance[dest]
130
+ end
131
+
132
+
133
+ def print_shortest_paths(source,dest, web_request)
134
+ check_data_integrity(source, dest)
135
+ total_distance = shortest_paths(source, dest)
136
+ stations_changed = (total_distance - @tube_list.size) > 0 ? (total_distance - @tube_list.size) : 0;
137
+ if total_distance != @INFINITY
138
+ if !web_request
139
+ puts "#{@tube_list.join(', ')}"
140
+ puts "\nDistance: #{total_distance}, you changed station #{stations_changed} times"
141
+ else
142
+ return "#{@tube_list.join(', ')}"
143
+ end
144
+ else
145
+ puts "NO PATH"
146
+ end
147
+ end
148
+
149
+ # Helper method to check if the stations are the correct ones
150
+ def check_data_integrity(source, dest)
151
+ station_list = @graph.map {|x| x.first}
152
+ errors = []
153
+ if !station_list.include?(source)
154
+ errors << source
155
+ end
156
+
157
+ if !station_list.include?(dest)
158
+ errors << dest
159
+ end
160
+
161
+ if errors.size > 0
162
+ puts "The following stations were not found in the application: #{errors.join(",")}. Please, check your data."
163
+ exit
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'mechanize'
3
+ require 'yaml'
4
+
5
+ module TubeScraper
6
+ def self.start!
7
+ @agent = Mechanize.new
8
+ @agent.get('http://tubephotos.dannycox.me.uk/stationsbyline.html'); # page fetching
9
+ tube_name_list, final_list = [], []
10
+ @agent.page.search('.style5').each {|x| tube_name_list << x.first[1]} # we search through all the occurrence of the class that holds station names..
11
+ tube_name_list.uniq!.sort! #then tidy up the array and sort alphabetically
12
+
13
+ stations = @agent.page.search('.stationList')
14
+ stations.each do |element|
15
+ children = element.elements.children;
16
+ temp_tube_list, temp_list = [], []
17
+ children.each_with_index do |x, i| # for every station object we..
18
+ value = x.text.split("\n").join(" ") # ..remove carriages inside the name.
19
+ .gsub(" "," ") # ...remove double unaestetic double spaces
20
+ .gsub("&", "and") # ...change the & to and, to better hand data in yaml
21
+ .gsub(/\(([^\)]+)\)/,'').rstrip # ...remove everything is between parentheses.
22
+ if(value != "")
23
+ temp_list << value
24
+ end
25
+ end
26
+
27
+ # Having obtained a clear list of the tube stations, we create
28
+ # a series of { source => target } hashes
29
+ temp_list.each_with_index do |prov,index|
30
+ if index + 1 < temp_list.size
31
+ temp_tube_list << { prov => temp_list[index+1] }
32
+ end
33
+ end
34
+
35
+
36
+ final_list << temp_tube_list if temp_tube_list.size > 0
37
+
38
+ end
39
+
40
+ # Once we have all the station connections, we push the line name
41
+ # at the beginning of every array.
42
+ final_list.each_with_index do |lines,index|
43
+ lines.unshift(tube_name_list[index].gsub("&","and"))
44
+ end
45
+
46
+ #and we finally save it!
47
+ File.open(Dir.pwd + "/tube_list2.yml", 'w+') {|f| f.write(final_list.to_yaml) }
48
+ end
49
+
50
+ end
51
+
52
+
@@ -0,0 +1,3 @@
1
+ module Undergrounder
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'undergrounder'
5
+
6
+ RSpec.configure do |config|
7
+ config.treat_symbols_as_metadata_keys_with_true_values = true
8
+ config.run_all_when_everything_filtered = true
9
+ config.filter_run :focus
10
+ end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe Undergrounder do
4
+ describe "Test" do
5
+ before(:each) do
6
+ @graph = Undergrounder::Graph.new
7
+ @graph.load_from_yaml
8
+ end
9
+
10
+ describe "data integrity" do
11
+ it "should detect if the source is wrong" do
12
+ STDOUT.should_receive(:puts).with("The following stations were not found in the application: Lappland. Please, check your data.")
13
+ expect { @graph.print_shortest_paths("Lappland", "Hammersmith", false) }.to raise_exception SystemExit
14
+ end
15
+
16
+ it "should detect if the destination is wrong" do
17
+ STDOUT.should_receive(:puts).with("The following stations were not found in the application: Santa Claws. Please, check your data.")
18
+ expect { @graph.print_shortest_paths("Hammersmith", "Santa Claws", false) }.to raise_exception SystemExit
19
+ end
20
+
21
+ it "should detect if both source and destionation are wrong" do
22
+ STDOUT.should_receive(:puts).with("The following stations were not found in the application: Lappland,Santa Claws. Please, check your data.")
23
+ expect { @graph.print_shortest_paths("Lappland", "Santa Claws", false) }.to raise_exception SystemExit
24
+ end
25
+ end
26
+
27
+ describe "Shortest paths correctness" do
28
+ it "should find that the right weight from Brixton to Liverpool Street is 9" do
29
+ @graph.shortest_paths("Brixton", "Liverpool Street").should == 9
30
+ end
31
+
32
+ it "should work the same inverting source and destination" do
33
+ @graph.shortest_paths("Liverpool Street", "Brixton").should == 9
34
+ end
35
+
36
+
37
+ describe "Various tests about different distance correctness" do
38
+
39
+ #Simple test with no line changing
40
+ it "Euston, Goodge Street" do
41
+ @graph.shortest_paths("Euston", "Goodge Street").should == 2
42
+ end
43
+
44
+ # Graphically, Euston/Warren Street/Oxford Circus/Tottenham has the same weight of Euston/Warren Street/Goodge Street/Tottenham.
45
+ # But the latter has no station change, so it is preferrable over the first
46
+ it "Euston, Tottenham Court Road" do
47
+ @graph.shortest_paths("Euston", "Tottenham Court Road").should == 3
48
+ end
49
+
50
+ # This is tricky. Euston/Embankment on the northern is 6, Via Victoria/Bakerloo is 5 + 1 line change.
51
+ it "Euston, Embankment" do
52
+ @graph.shortest_paths("Euston", "Embankment").should == 6
53
+ end
54
+
55
+ # One line change. The algorithm prefer to use the metropolitan line, who has less stations between W. Park and B. Street
56
+ it "Stanmore, Baker Street" do
57
+ @graph.shortest_paths("Stanmore", "Baker Street").should == 7
58
+ end
59
+
60
+ # 2 line change - Circle(or Metropolitan or Hammersmith) + Victoria + Piccadilly
61
+ it "Euston Square, Manor House" do
62
+ @graph.shortest_paths("Euston Square", "Manor House").should == 6
63
+ end
64
+
65
+ # Starting from a station with 2 lines, finishing with a station with one line.
66
+ it "Bow Road, Upminster" do
67
+ @graph.shortest_paths("Bow Road", "Upminster").should == 14
68
+ end
69
+
70
+ it "Piccadilly Circus, Westminster" do
71
+ @graph.shortest_paths("Piccadilly Circus", "Westminster").should == 3
72
+ end
73
+
74
+ # This test fail. Really i'm not understanding why. Basically is not counting the station change :-/
75
+ # it "Stanmore, Sudbury Town" do
76
+ # @graph.shortest_paths("Stanmore", "Sudbury Town").should == 14
77
+ # end
78
+ end
79
+ end
80
+
81
+
82
+ describe "Print shortest paths correctness" do
83
+ it "should print the correct path from Livepool Street to Brixton" do
84
+ output = capture_stdout { @graph.print_shortest_paths("Brixton", "Liverpool Street", false) }
85
+ output.should include("Brixton, Stockwell, Oval, Kennington, Waterloo, Bank, Liverpool Street")
86
+ end
87
+
88
+ it "should print the correct path from Bow Road to Upminster" do
89
+ output = capture_stdout { @graph.print_shortest_paths("Bow Road", "Upminster", false) }
90
+ output.should include("Bow Road, Bromley-by-Bow, West Ham, Plaistow, Upton Park, East Ham, Barking, Upney, Becontree, Dagenham Heathway, Dagenham East, Elm Park, Hornchurch, Upminster Bridge, Upminster")
91
+ end
92
+
93
+ it "should print the correct path from Stratford to Limehouse" do
94
+ # Looking at the map you'd think that taking the Jubilee will be fastest...
95
+ output = capture_stdout { @graph.print_shortest_paths("Stratford", "Limehouse", false) }
96
+ output.should include("Stratford, Mile End, Bethnal Green, Liverpool Street, Bank, Shadwell, Limehouse")
97
+ end
98
+
99
+ end
100
+
101
+
102
+ # Helper method to search through the STDOUT
103
+ def capture_stdout(&block)
104
+ original_stdout = $stdout
105
+ $stdout = fake = StringIO.new
106
+ begin
107
+ yield
108
+ ensure
109
+ $stdout = original_stdout
110
+ end
111
+ fake.string
112
+ end
113
+
114
+ end
115
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'undergrounder/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "undergrounder"
8
+ gem.version = Undergrounder::VERSION
9
+ gem.authors = ["Enzo Rivello"]
10
+ gem.email = ["vincenzo.rivello@gmail.com"]
11
+ gem.description = "Simple Implementation of a short path finder for the London Tube"
12
+ gem.summary = ""
13
+ gem.homepage = "https://github.com/enzor/undergrounder"
14
+
15
+ gem.add_dependency "sinatra"
16
+ gem.add_dependency "padrino"
17
+ gem.add_development_dependency "rspec"
18
+ gem.add_development_dependency "ruby-debug19"
19
+
20
+ gem.files = `git ls-files`.split($/)
21
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
22
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
23
+ gem.require_paths = ["lib"]
24
+ end
@@ -0,0 +1,9 @@
1
+ ---
2
+ :orm: activerecord
3
+ :test: none
4
+ :mock: none
5
+ :script: none
6
+ :renderer: slim
7
+ :stylesheet: none
8
+ :namespace: WebFinder
9
+ :migration_format: number
@@ -0,0 +1,8 @@
1
+ .DS_Store
2
+ log/**/*
3
+ tmp/**/*
4
+ bin/*
5
+ vendor/gems/*
6
+ !vendor/gems/cache/
7
+ .sass-cache/*
8
+ db/*.db
@@ -0,0 +1,37 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Distribute your app as a gem
4
+ # gemspec
5
+
6
+ # Server requirements
7
+ # gem 'thin' # or mongrel
8
+ # gem 'trinidad', :platform => 'jruby'
9
+
10
+ # Optional JSON codec (faster performance)
11
+ # gem 'oj'
12
+
13
+ # Project requirements
14
+ gem 'rake'
15
+
16
+ # Component requirements
17
+ gem 'slim'
18
+ gem 'activerecord', '>= 3.1', :require => 'active_record'
19
+ gem 'sqlite3'
20
+
21
+ # Test requirements
22
+
23
+ # Padrino Stable Gem
24
+ gem 'tilt', '1.3.7'
25
+ gem 'padrino', '0.11.1'
26
+ gem 'undergrounder'
27
+ gem 'ruby-debug19'
28
+ gem 'haml'
29
+
30
+
31
+ # Or Padrino Edge
32
+ # gem 'padrino', :github => 'padrino/padrino-framework'
33
+
34
+ # Or Individual Gems
35
+ # %w(core gen helpers cache mailer admin).each do |g|
36
+ # gem 'padrino-' + g, '0.11.1'
37
+ # end