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/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +21 -0
- data/Rakefile +1 -0
- data/assets/test_assets.yml +34 -0
- data/assets/tube_list.yml +435 -0
- data/bin/undergrounder +30 -0
- data/lib/undergrounder.rb +167 -0
- data/lib/undergrounder/tube_scraper.rb +52 -0
- data/lib/undergrounder/version.rb +3 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/undergrounder_spec.rb +115 -0
- data/undergrounder.gemspec +24 -0
- data/vendor/web_finder/.components +9 -0
- data/vendor/web_finder/.gitignore +8 -0
- data/vendor/web_finder/Gemfile +37 -0
- data/vendor/web_finder/Rakefile +6 -0
- data/vendor/web_finder/app/app.rb +10 -0
- data/vendor/web_finder/app/controllers/tube_planner.rb +17 -0
- data/vendor/web_finder/app/views/tube_planner/index.haml +9 -0
- data/vendor/web_finder/config.ru +9 -0
- data/vendor/web_finder/config/apps.rb +36 -0
- data/vendor/web_finder/config/boot.rb +34 -0
- data/vendor/web_finder/config/database.rb +58 -0
- data/vendor/web_finder/public/favicon.ico +0 -0
- metadata +140 -0
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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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,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
|