diff --git a/.gitignore b/.gitignore index 52e45c485a9..eee2f95c1c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,14 @@ +# Test folders # +################### +test/speedprofile.ini +test/cache +test/data +test/server.ini + +# Sandbox folder # +################### +sandbox + # Compiled source # ################### *.com @@ -6,6 +17,9 @@ *.exe *.o *.so +osrm-extract +osrm-prepare +osrm-routed # Packages # ############ @@ -48,7 +62,7 @@ SconsBuilder* # stxxl related files # ####################### -.stxxl +./.stxxl stxxl.log stxxl.errlog @@ -70,4 +84,4 @@ win/*.suo win/Debug/ win/Release/ win/bin/ -win/bin-debug/ \ No newline at end of file +win/bin-debug/ diff --git a/DataStructures/ExtractorCallBacks.h b/DataStructures/ExtractorCallBacks.h index e06f6037454..7d302016b3b 100644 --- a/DataStructures/ExtractorCallBacks.h +++ b/DataStructures/ExtractorCallBacks.h @@ -173,12 +173,18 @@ class ExtractorCallbacks{ } if( settings.obeyOneways ) { - if( oneway == "no" || oneway == "0" || oneway == "false" || onewayClass == "no" || onewayClass == "0" || onewayClass == "false" || ((settings.accessTag == "bicycle") && (cycleway == "opposite" || cycleway == "opposite_track" || cycleway == "opposite_lane")) ) { + if( oneway == "no" || oneway == "0" || oneway == "false" || + onewayClass == "no" || onewayClass == "0" || onewayClass == "false" || + ((settings.accessTag == "bicycle") && (cycleway == "opposite" || cycleway == "opposite_track" || cycleway == "opposite_lane")) ) { w.direction = _Way::bidirectional; } else if( oneway == "-1") { w.direction = _Way::opposite; } - else if( oneway == "yes" || oneway == "1" || oneway == "true" || onewayClass == "yes" || onewayClass == "1" || onewayClass == "true" || junction == "roundabout" || highway == "motorway_link" || highway == "motorway" ) { + else if( oneway == "yes" || oneway == "1" || oneway == "true" || + onewayClass == "yes" || onewayClass == "1" || onewayClass == "true" || + junction == "roundabout" || + highway == "motorway" || + highway == "motorway_link" ) { w.direction = _Way::oneway; } else { w.direction = _Way::bidirectional; @@ -214,10 +220,6 @@ class ExtractorCallbacks{ return true; } - if ( w.direction == _Way::opposite ){ - std::reverse( w.path.begin(), w.path.end() ); - } - for(vector< NodeID >::size_type n = 0; n < w.path.size()-1; ++n) { externalMemory->allEdges.push_back(_Edge(w.path[n], w.path[n+1], w.type, w.direction, w.speed, w.nameID, w.roundabout, highway == settings.excludeFromGrid)); externalMemory->usedNodeIDs.push_back(w.path[n]); diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000000..b0db7beada8 --- /dev/null +++ b/Gemfile @@ -0,0 +1,6 @@ +source "http://rubygems.org" + +gem "cucumber" +gem "rake" +gem "osmlib-base" +gem "sys-proctable" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000000..d4bff7b4130 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,27 @@ +GEM + remote: http://rubygems.org/ + specs: + builder (3.0.0) + cucumber (1.1.4) + builder (>= 2.1.2) + diff-lcs (>= 1.1.2) + gherkin (~> 2.7.1) + json (>= 1.4.6) + term-ansicolor (>= 1.0.6) + diff-lcs (1.1.3) + gherkin (2.7.6) + json (>= 1.4.6) + json (1.6.5) + osmlib-base (0.1.4) + rake (0.9.2.2) + sys-proctable (0.9.1) + term-ansicolor (1.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + cucumber + osmlib-base + rake + sys-proctable diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000000..0c5fb768ad0 --- /dev/null +++ b/Rakefile @@ -0,0 +1,181 @@ +require 'OSM/StreamParser' +require 'socket' +require 'digest/sha1' +require 'cucumber/rake/task' +require 'sys/proctable' + +SANDBOX = 'sandbox' +DATA_FOLDER = 'osm_data' + +Cucumber::Rake::Task.new do |t| + t.cucumber_opts = %w{--format pretty} +end + +areas = { + :kbh => { :country => 'denmark', :bbox => 'top=55.6972 left=12.5222 right=12.624 bottom=55.6376' }, + :frd => { :country => 'denmark', :bbox => 'top=55.7007 left=12.4765 bottom=55.6576 right=12.5698' }, + :regh => { :country => 'denmark', :bbox => 'top=56.164 left=11.792 bottom=55.403 right=12.731' }, + :dk => { :country => 'denmark', :bbox => nil }, + :skaane => { :counry => 'sweden', :bbox => 'top=56.55 left=12.4 bottom=55.3 right=14.6' } +} + + + +osm_data_area_name = ARGV[1] ? ARGV[1].to_s.to_sym : :kbh +raise "Unknown data area." unless areas[osm_data_area_name] +osm_data_country = areas[osm_data_area_name][:country] +osm_data_area_bbox = areas[osm_data_area_name][:bbox] + + +task osm_data_area_name.to_sym {} #define empty task to prevent rake from whining. will break if area has same name as a task + + +def each_process name, &block + Sys::ProcTable.ps do |process| + if process.comm.strip == name.strip + yield process.pid.to_i, process.state.strip + end + end +end + +def up? + find_pid('osrm-routed') != nil +end + +def find_pid name + each_process(name) { |pid,state| return pid.to_i } + return nil +end + +def wait_for_shutdown name + timeout = 10 + (timeout*10).times do + return if find_pid(name) == nil + sleep 0.1 + end + raise "*** Could not terminate #{name}." +end + +def write_server_ini osm_file + s=<<-EOF + Threads = 1 + IP = 0.0.0.0 + Port = 5000 + + hsgrData=#{DATA_FOLDER}/#{osm_file}.osrm.hsgr + nodesData=#{DATA_FOLDER}/#{osm_file}.osrm.nodes + ramIndex=#{DATA_FOLDER}/#{osm_file}.osrm.ramIndex + fileIndex=#{DATA_FOLDER}/#{osm_file}.osrm.fileIndex + namesData=#{DATA_FOLDER}/#{osm_file}.osrm.names + EOF + File.open( 'server.ini', 'w') {|f| f.write( s ) } +end + + +desc "Rebuild and run tests." +task :default => [:build, :cucumber] + +desc "Build using SConsstruct." +task :build do + system "scons" +end + +desc "Setup config files." +task :setup do + Dir.mkdir "#{SANDBOX}/#{DATA_FOLDER}" unless File.exist? "#{SANDBOX}/#{DATA_FOLDER}" + ['server.ini','speedprofile.ini','extractor.ini','contractor.ini'].each do |file| + unless File.exist? "#{SANDBOX}/#{file}" + puts "Copying #{file} template to sandbox/#{file}" + FileUtils.cp file, "#{SANDBOX}/#{file}" + end + end +end + +desc "Download OSM data." +task :download => :setup do + puts "Downloading..." + raise "Error while downloading data." unless system "curl http://download.geofabrik.de/osm/europe/#{osm_data_country}.osm.pbf -o #{SANDBOX}/#{DATA_FOLDER}/#{osm_data_country}.osm.pbf" + if osm_data_area_bbox + puts "Cropping and converting to protobuffer..." + raise "Error while cropping data." unless system "osmosis --read-pbf file=#{SANDBOX}/#{DATA_FOLDER}/#{osm_data_country}.osm.pbf --bounding-box #{osm_data_area_bbox} --write-pbf file=#{SANDBOX}/#{DATA_FOLDER}/#{osm_data_area_name}.osm.pbf omitmetadata=true" + end +end + +desc "Crop OSM data" +task :crop do + if osm_data_area_bbox + raise "Error while cropping data." unless system "osmosis --read-pbf file=#{SANDBOX}/#{DATA_FOLDER}/#{osm_data_country}.osm.pbf --bounding-box #{osm_data_area_bbox} --write-pbf file=#{SANDBOX}/#{DATA_FOLDER}/#{osm_data_area_name}.osm.pbf omitmetadata=true" + end +end + +desc "Reprocess OSM data." +task :process => :setup do + Dir.chdir SANDBOX do + raise "Error while extracting data." unless system "../osrm-extract #{DATA_FOLDER}/#{osm_data_area_name}.osm.pbf" + puts + raise "Error while preparing data." unless system "../osrm-prepare #{DATA_FOLDER}/#{osm_data_area_name}.osrm #{DATA_FOLDER}/#{osm_data_area_name}.osrm.restrictions" + puts + end +end + +desc "Delete preprocessing files." +task :clean do + File.delete *Dir.glob("#{SANDBOX}/#{DATA_FOLDER}/*.osrm") + File.delete *Dir.glob("#{SANDBOX}/#{DATA_FOLDER}/*.osrm.*") +end + +desc "Run all cucumber test" +task :test do + system "cucumber" + puts +end + +desc "Run the routing server in the terminal. Press Ctrl-C to stop." +task :run => :setup do + Dir.chdir SANDBOX do + write_server_ini osm_data_area_name + system "../osrm-routed" + end +end + +desc "Launch the routing server in the background. Use rake:down to stop it." +task :up => :setup do + Dir.chdir SANDBOX do + abort("Already up.") if up? + write_server_ini osm_data_area_name + pipe = IO.popen('../osrm-routed 1>>osrm-routed.log 2>>osrm-routed.log') + timeout = 5 + (timeout*10).times do + begin + socket = TCPSocket.new('localhost', 5000) + socket.puts 'ping' + rescue Errno::ECONNREFUSED + sleep 0.1 + end + end + end +end + +desc "Stop the routing server." +task :down do + pid = find_pid 'osrm-routed' + abort("Already down.") unless pid + Process.kill 'TERM', pid +end + +desc "Kill all osrm-extract, osrm-prepare and osrm-routed processes." +task :kill do + each_process('osrm-routed') { |pid,state| Process.kill 'KILL', pid } + each_process('osrm-prepare') { |pid,state| Process.kill 'KILL', pid } + each_process('osrm-extract') { |pid,state| Process.kill 'KILL', pid } + wait_for_shutdown 'osrm-routed' + wait_for_shutdown 'osrm-prepare' + wait_for_shutdown 'osrm-extract' +end + +desc "Get PIDs of all osrm-extract, osrm-prepare and osrm-routed processes." +task :pid do + each_process 'osrm-routed' do |pid,state| + puts "#{pid}\t#{state}" + end +end diff --git a/SConstruct b/SConstruct index eb1566e0482..4b00abe3dda 100644 --- a/SConstruct +++ b/SConstruct @@ -49,58 +49,79 @@ def CheckProtobuf(context, version): context.Result(ret) return ret + AddOption('--cxx', dest='cxx', type='string', nargs=1, action='store', metavar='STRING', help='C++ Compiler') AddOption('--stxxlroot', dest='stxxlroot', type='string', nargs=1, action='store', metavar='STRING', help='root directory of STXXL') AddOption('--verbosity', dest='verbosity', type='string', nargs=1, action='store', metavar='STRING', help='make Scons talking') AddOption('--buildconfiguration', dest='buildconfiguration', type='string', nargs=1, action='store', metavar='STRING', help='debug or release') -env = Environment(ENV = {'PATH' : os.environ['PATH']} ,COMPILER = GetOption('cxx')) -if sys.platform.startswith("freebsd"): - env.ParseConfig('pkg-config --cflags --libs protobuf') + +env = Environment( ENV = {'PATH' : os.environ['PATH']} ,COMPILER = GetOption('cxx')) +conf = Configure(env, custom_tests = { 'CheckBoost' : CheckBoost, 'CheckProtobuf' : CheckProtobuf }) + + if GetOption('cxx') is None: #default Compiler print 'Using default C++ Compiler: ', env['CXX'] else: env.Replace(CXX = GetOption('cxx')) print 'Using user supplied C++ Compiler: ', env['CXX'] -if GetOption('stxxlroot') is not None: - env.Append(CPPPATH = GetOption('stxxlroot')+'/include') - env.Append(LIBPATH = GetOption('stxxlroot')+'/lib') - print 'STXXLROOT = ', GetOption('stxxlroot') -if sys.platform == 'win32': - #SCons really wants to use Microsoft compiler - print "Compiling is not yet supported on Windows" - Exit(-1) -else: #Mac OS X - if sys.platform == 'darwin': - print "Compiling is experimental on Mac" - env.Append(CPPPATH = ['/opt/local/include/', '/opt/local/include/libxml2']) - env.Append(LIBPATH = ['/opt/local/lib']) - elif sys.platform.startswith('freebsd'): - env.Append(CPPPATH = ['/usr/local/include', '/usr/local/include/libxml2']) - env.Append(LIBPATH = ['/usr/local/lib']) - else: - env.Append(CPPPATH = ['/usr/include', '/usr/include/include', '/usr/include/libxml2/']) + if GetOption('buildconfiguration') == 'debug': env.Append(CCFLAGS = ['-Wall', '-g3', '-rdynamic']) else: - env.Append(CCFLAGS = ['-O3', '-DNDEBUG', '-march=native']) -#print "Compiling with: ", env['CXX'] -conf = Configure(env, custom_tests = { 'CheckBoost' : CheckBoost, 'CheckProtobuf' : CheckProtobuf }) -if not conf.CheckHeader('omp.h'): - print "Compiler does not support OpenMP. Exiting" - if sys.platform == 'darwin': - print "Continuing because we are on Mac. This might be fatal." - else: + env.Append(CCFLAGS = ['-O3', '-DNDEBUG']) + + +if sys.platform == 'darwin': #Mac OS X + env.Append(CPPPATH = ['/usr/include/libxml2'] ) #comes with os x + #assume dependencies are installed with homebrew, and call out to get folder locations + import subprocess + stxxl_prefix = subprocess.check_output(["brew", "--prefix", "libstxxl"]).strip() + env.Append(CPPPATH = [stxxl_prefix+"/include"] ) + env.Append(LIBPATH = [stxxl_prefix+"/lib"] ) + + boost_prefix = subprocess.check_output(["brew", "--prefix", "boost"]).strip() + env.Append(CPPPATH = [boost_prefix+"/include"] ) + env.Append(LIBPATH = [boost_prefix+"/lib"] ) + + #libxml2 should be installed by default, but can also be installed by via brew + #libxml2_prefix = subprocess.check_output(["brew", "--prefix", "libxml2"]).strip() + #env.Append(CPPPATH = [libxml2_prefix+"/include"] ) + #env.Append(LIBPATH = [libxml2_prefix+"/lib"] ) + +elif sys.platform.startswith("freebsd"): + env.ParseConfig('pkg-config --cflags --libs protobuf') + env.Append(CPPPATH = ['/usr/local/include', '/usr/local/include/libxml2']) + env.Append(LIBPATH = ['/usr/local/lib']) + if GetOption('stxxlroot') is not None: + env.Append(CPPPATH = GetOption('stxxlroot')+'/include') + env.Append(LIBPATH = GetOption('stxxlroot')+'/lib') + print 'STXXLROOT = ', GetOption('stxxlroot') + if GetOption('buildconfiguration') != 'debug': + env.Append(CCFLAGS = ['-march=native']) + #print "Compiling with: ", env['CXX'] + env.Append(CCFLAGS = ['-fopenmp']) + env.Append(LINKFLAGS = ['-fopenmp']) +elif sys.platform == 'win32': + #SCons really wants to use Microsoft compiler + print "Compiling is not yet supported on Windows" + Exit(-1) +else: + print "Unknown platform.." + env.Append(CPPPATH = ['/usr/include', '/usr/include/include', '/usr/include/libxml2/']) + + +if sys.platform != 'darwin': + if not conf.CheckHeader('omp.h'): + print "Compiler does not support OpenMP. Exiting" Exit(-1) + if not conf.CheckLibWithHeader('bz2', 'bzlib.h', 'CXX'): print "bz2 library not found. Exiting" Exit(-1) if not conf.CheckLibWithHeader('libzip', 'zip.h', 'CXX'): print "Zip library not found. Exiting" Exit(-1) -if not conf.CheckLibWithHeader('pthread', 'pthread.h', 'CXX'): - print "pthread not found. Exiting" - Exit(-1) if not conf.CheckLibWithHeader('protobuf', 'google/protobuf/descriptor.h', 'CXX'): print "Google Protobuffer library not found. Exiting" Exit(-1) @@ -132,9 +153,6 @@ if not conf.CheckLibWithHeader('Magick++', 'ImageMagick/Magick++.h', 'CXX'): if not (conf.CheckBoost('1.41')): print 'Boost version >= 1.41 needed' Exit(-1); -if not conf.CheckLib('boost_system', language="C++"): - print "boost_system library not found. Exiting" - Exit(-1) if not conf.CheckLibWithHeader('boost_thread', 'boost/thread.hpp', 'CXX'): if not conf.CheckLibWithHeader('boost_thread-mt', 'boost/thread.hpp', 'CXX'): print "boost thread library not found. Exiting" @@ -144,14 +162,21 @@ if not conf.CheckLibWithHeader('boost_thread', 'boost/thread.hpp', 'CXX'): env.Append(CCFLAGS = ' -lboost_thread-mt') env.Append(LINKFLAGS = ' -lboost_thread-mt') if not conf.CheckLibWithHeader('boost_regex', 'boost/regex.hpp', 'CXX'): - print "boost/regex.hpp not found. Exiting" - Exit(-1) -if not conf.CheckCXXHeader('boost/array.hpp'): - print "boost/thread.hpp not found. Exiting" - Exit(-1) -if not conf.CheckCXXHeader('boost/asio.hpp'): - print "boost/thread.hpp not found. Exiting" - Exit(-1) + if not conf.CheckLibWithHeader('boost_regex-mt', 'boost/regex.hpp', 'CXX'): + print "boost/regex.hpp not found. Exiting" + Exit(-1) + else: + print "using boost_regex -mt" + env.Append(CCFLAGS = ' -lboost_regex-mt') + env.Append(LINKFLAGS = ' -lboost_regex-mt') +if not conf.CheckLib('boost_system', language="C++"): + if not conf.CheckLib('boost_system-mt', language="C++"): + print "boost_system library not found. Exiting" + Exit(-1) + else: + print "using boost -mt" + env.Append(CCFLAGS = ' -lboost_system-mt') + env.Append(LINKFLAGS = ' -lboost_system-mt') if not conf.CheckCXXHeader('boost/bind.hpp'): print "boost/bind.hpp not found. Exiting" Exit(-1) @@ -194,18 +219,26 @@ if not conf.CheckCXXHeader('boost/tuple/tuple.hpp'): if not conf.CheckCXXHeader('boost/unordered_map.hpp'): print "boost thread header not found. Exiting" Exit(-1) +#if os.sysconf('SC_NPROCESSORS_ONLN') > 1: +# env.Append(CCFLAGS = ' -D_GLIBCXX_PARALLEL'); +if not (conf.CheckBoost('1.41')): + print 'Boost version >= 1.41 needed' + Exit(-1); +#check for protobuf 2.3.0, else rebuild proto files +if not (conf.CheckProtobuf('2.3.0')): + print 'libprotobuf version >= 2.3.0 needed' + Exit(-1); +if not (env.Detect('protoc')): + print 'protobuffer compiler not found' + protobld = Builder(action = 'protoc -I=DataStructures/pbf-proto --cpp_out=DataStructures/pbf-proto $SOURCE') env.Append(BUILDERS = {'Protobuf' : protobld}) env.Protobuf('DataStructures/pbf-proto/fileformat.proto') env.Protobuf('DataStructures/pbf-proto/osmformat.proto') -env.Append(CCFLAGS = ['-fopenmp']) -env.Append(LINKFLAGS = ['-fopenmp']) - -env.Program(target = 'osrm-extract', source = ["extractor.cpp", Glob('DataStructures/pbf-proto/*.pb.cc'), Glob('Util/*.cpp')]) -env.Program(target = 'osrm-prepare', source = ["createHierarchy.cpp", 'Contractor/EdgeBasedGraphFactory.cpp', Glob('Util/SRTMLookup/*.cpp')]) -env.Append(CCFLAGS = ['-lboost_regex', '-lboost_iostreams', '-lbz2', '-lz', '-lprotobuf']) -env.Append(LINKFLAGS = ['-lboost_system']) -env.Program(target = 'osrm-routed', source = ["routed.cpp", 'Descriptors/DescriptionFactory.cpp'], CCFLAGS = ['-DROUTED']) + +env.Program(target = 'osrm-extract', source = ["extractor.cpp", 'DataStructures/pbf-proto/fileformat.pb.cc', 'DataStructures/pbf-proto/osmformat.pb.cc']) +env.Program(target = 'osrm-prepare', source = ["createHierarchy.cpp", 'Contractor/EdgeBasedGraphFactory.cpp']) +env.Program(target = 'osrm-routed', source = ["routed.cpp", 'Descriptors/DescriptionFactory.cpp']) env = conf.Finish() diff --git a/contractor.ini b/contractor.ini index d82afbbffa4..fa39ddf7383 100644 --- a/contractor.ini +++ b/contractor.ini @@ -1,2 +1,2 @@ Threads = 4 -SRTM = /opt/storage/srtm/Eurasia \ No newline at end of file +SRTM = data/srtm/Eurasia \ No newline at end of file diff --git a/createHierarchy.cpp b/createHierarchy.cpp index 78cc90c028d..52af0863935 100644 --- a/createHierarchy.cpp +++ b/createHierarchy.cpp @@ -79,7 +79,7 @@ int main (int argc, char *argv[]) { INFO("Loading SRTM from/to " << SRTM_ROOT); omp_set_num_threads(numberOfThreads); - INFO("preprocessing data from input file " << argv[2] << " using STL " + INFO("preprocessing data from input file " << argv[1] << " using STL " #ifdef _GLIBCXX_PARALLEL "parallel (GCC)" #else diff --git a/extractor.ini b/extractor.ini index 15f135ff82c..aa6ba2e304d 100644 --- a/extractor.ini +++ b/extractor.ini @@ -1 +1 @@ -Memory = 2 +Memory = 1 \ No newline at end of file diff --git a/features/access.feature b/features/access.feature new file mode 100644 index 00000000000..6d22b63401e --- /dev/null +++ b/features/access.feature @@ -0,0 +1,54 @@ +@routing @access +Feature: Oneway streets + Basic accessability of various way types. + + Scenario: Basic access for cars + Given the speedprofile "car" + Then routability should be + | highway | forw | + | motorway | x | + | motorway_link | x | + | trunk | x | + | trunk_link | x | + | primary | x | + | secondary | x | + | tertiary | x | + | residential | x | + | service | x | + | unclassified | x | + | living_street | x | + | road | x | + | track | | + | path | | + | footway | | + | pedestrian | | + | steps | | + | pier | | + | cycleway | | + | bridleway | | + + Scenario: Basic access for bicycles + Bikes are allowed on footways etc because you can pull your bike at a lower speed. + Given the speedprofile "bicycle" + Then routability should be + | highway | forw | + | motorway | | + | motorway_link | | + | trunk | | + | trunk_link | | + | primary | x | + | secondary | x | + | tertiary | x | + | residential | x | + | service | x | + | unclassified | x | + | living_street | x | + | road | x | + | track | x | + | path | x | + | footway | x | + | pedestrian | x | + | steps | x | + | pier | x | + | cycleway | x | + | bridleway | | diff --git a/features/bad.feature b/features/bad.feature new file mode 100644 index 00000000000..ded973e265f --- /dev/null +++ b/features/bad.feature @@ -0,0 +1,54 @@ +@routing @bad +Feature: Handle bad data in a graceful manner + + Scenario: Empty dataset + Given the nodes + | a | b | + + Given the ways + | nodes | + + When I route I should get + | from | to | route | + | a | b | | + + Scenario: Start/end point at the same location + Given the nodes + | a | b | + | 1 | 2 | + + Given the ways + | nodes | + | ab | + + When I route I should get + | from | to | route | + | a | a | | + | b | b | | + | 1 | 1 | | + | 2 | 2 | | + + Scenario: Start/end point far outside data area + Given the nodes + | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 1 | + | a | b | | | | | | | | | | | | | | | | | | | | | | | | | | | 2 | + | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 3 | + + Given the ways + | nodes | + | ab | + + When I route I should get + | from | to | route | + | 1 | a | ab | + | 2 | a | ab | + | 3 | a | ab | + | 1 | b | | + | 2 | b | | + | 3 | b | | + | 1 | 2 | | + | 1 | 3 | | + | 2 | 1 | | + | 2 | 3 | | + | 3 | 1 | | + | 3 | 2 | | diff --git a/features/basic.feature b/features/basic.feature new file mode 100644 index 00000000000..b94757ac7ad --- /dev/null +++ b/features/basic.feature @@ -0,0 +1,135 @@ +@routing @basic +Feature: Basic Routing + + @smallest + Scenario: A single way with two nodes + Given the nodes + | a | b | + + And the ways + | nodes | + | ab | + + When I route I should get + | from | to | route | + | a | b | ab | + | b | a | ab | + + Scenario: Routing in between two nodes of way + Given the nodes + | a | b | 1 | 2 | c | d | + + And the ways + | nodes | + | abcd | + + When I route I should get + | from | to | route | + | 1 | 2 | abcd | + | 2 | 1 | abcd | + + Scenario: Routing between the middle nodes of way + Given the nodes + | a | b | c | d | e | f | + + And the ways + | nodes | + | abcdef | + + When I route I should get + | from | to | route | + | b | c | abcdef | + | b | d | abcdef | + | b | e | abcdef | + | c | b | abcdef | + | c | d | abcdef | + | c | e | abcdef | + | d | b | abcdef | + | d | c | abcdef | + | d | e | abcdef | + | e | b | abcdef | + | e | c | abcdef | + | e | d | abcdef | + + Scenario: Two ways connected in a straight line + Given the nodes + | a | b | c | + + And the ways + | nodes | + | ab | + | bc | + + When I route I should get + | from | to | route | + | a | c | ab,bc | + | c | a | bc,ab | + | a | b | ab | + | b | a | ab | + | b | c | bc | + | c | b | bc | + + Scenario: 2 unconnected parallel ways + Given the nodes + | a | b | + | c | d | + + And the ways + | nodes | + | ab | + | cd | + + When I route I should get + | from | to | route | + | a | b | ab | + | b | a | ab | + | c | d | cd | + | d | c | cd | + | a | c | | + | c | a | | + | b | d | | + | d | b | | + | a | d | | + | d | a | | + + Scenario: 3 ways connected in a triangle + Given the nodes + | a | | b | + | | | | + | | c | | + + And the ways + | nodes | + | ab | + | bc | + | ca | + + When I route I should get + | from | to | route | + | a | b | ab | + | a | c | ca | + | b | c | bc | + | b | a | ab | + | c | a | ca | + | c | b | bc | + + Scenario: To ways connected at a 45 degree angle + Given the nodes + | a | | | + | b | | | + | c | d | e | + + And the ways + | nodes | + | abc | + | cde | + + When I route I should get + | from | to | route | + | b | d | abc,cde | + | a | e | abc,cde | + | a | c | abc | + | c | a | abc | + | c | e | cde | + | e | c | cde | + diff --git a/features/ferry.feature b/features/ferry.feature new file mode 100644 index 00000000000..9f947c5915e --- /dev/null +++ b/features/ferry.feature @@ -0,0 +1,26 @@ +@routing @ferry +Feature: Handle ferry routes + + Scenario: Use a ferry route + Given the nodes + | a | b | c | | | + | | | d | | | + | | | e | f | g | + + And the ways + | nodes | highway | route | bicycle | + | abc | primary | | | + | cde | | ferry | yes | + | efg | primary | | | + + When I route I should get + | from | to | route | + | a | g | abc,cde,efg | + | b | f | abc,cde,efg | + | e | c | cde | + | e | b | cde,abc | + | e | a | cde,abc | + | c | e | cde | + | c | f | cde,efg | + | c | g | cde,efg | + diff --git a/features/oneway.feature b/features/oneway.feature new file mode 100644 index 00000000000..7338f4a56c9 --- /dev/null +++ b/features/oneway.feature @@ -0,0 +1,165 @@ +@routing @oneway +Feature: Oneway streets + Handle oneways streets, as defined at http://wiki.openstreetmap.org/wiki/OSM_tags_for_routing + + Scenario: Simple oneway + Given the speedprofile "car" + Then routability should be + | highway | oneway | forw | backw | + | primary | yes | x | | + + Scenario: Simple reverse oneway + Given the speedprofile "car" + Then routability should be + | highway | oneway | forw | backw | + | primary | -1 | | x | + + Scenario: Around the Block + Given the nodes + | a | b | + | d | c | + + And the ways + | nodes | oneway | + | ab | yes | + | bc | | + | cd | | + | da | | + + When I route I should get + | from | to | route | + | a | b | ab | + | b | a | bc,cd,da | + + Scenario: Avoid oneway traps + Given the nodes + | | x | | + | a | b | c | + | | y | | + + And the ways + | nodes | oneway | + | abc | | + | bx | yes | + | yb | -1 | + + When I route I should get + | from | to | route | + | b | x | | + | b | y | | + + Scenario: Handle various oneway tag values + Given the speedprofile "bicycle" + Then routability should be + | highway | oneway | forw | backw | + | primary | | x | x | + | primary | nonsense | x | x | + | primary | no | x | x | + | primary | false | x | x | + | primary | 0 | x | x | + | primary | yes | x | | + | primary | true | x | | + | primary | 1 | x | | + | primary | -1 | | x | + + Scenario: Implied oneways + Given the speedprofile "car" + Then routability should be + | highway | junction | forw | backw | + | motorway | | x | | + | motorway_link | | x | | + | trunk | | x | x | + | trunk_link | | x | x | + | primary | | x | x | + | primary_link | | x | x | + | secondary | | x | x | + | secondary_link | | x | x | + | tertiary | | x | x | + | tertiary_link | | x | x | + | residential | | x | x | + | primary | roundabout | x | | + | secondary | roundabout | x | | + | tertiary | roundabout | x | | + | residential | roundabout | x | | + + Scenario: Overriding implied oneways + Given the speedprofile "car" + Then routability should be + | highway | junction | oneway | forw | backw | + | motorway_link | | no | x | x | + | trunk_link | | no | x | x | + | primary | roundabout | no | x | x | + | motorway_link | | yes | x | | + | trunk_link | | yes | x | | + | primary | roundabout | yes | x | | + | motorway_link | | -1 | | x | + | trunk_link | | -1 | | x | + | primary | roundabout | -1 | | x | + + Scenario: Disabling oneways in speedprofile + Given the speedprofile "car" + And the speedprofile settings + | obeyOneways | no | + Then routability should be + | highway | junction | oneway | forw | backw | + | primary | | yes | x | x | + | primary | | true | x | x | + | primary | | 1 | x | x | + | primary | | -1 | x | x | + | motorway_link | | | x | x | + | trunk_link | | | x | x | + | primary | roundabout | | x | x | + + @bicycle + Scenario: Oneway:bicycle should override normal oneways tags + Given the speedprofile "bicycle" + Then routability should be + | highway | junction | oneway | oneway:bicycle | forw | backw | + | primary | | | yes | x | | + | primary | | yes | yes | x | | + | primary | | no | yes | x | | + | primary | | -1 | yes | x | | + | primary | roundabout | | yes | x | | + | primary | | | no | x | x | + | primary | | yes | no | x | x | + | primary | | no | no | x | x | + | primary | | -1 | no | x | x | + | primary | roundabout | | no | x | x | + | primary | | | -1 | | x | + | primary | | yes | -1 | | x | + | primary | | no | -1 | | x | + | primary | | -1 | -1 | | x | + | primary | roundabout | | -1 | | x | + + @bicycle + Scenario: Bicycles and contra flow + Given the speedprofile "bicycle" + Then routability should be + | highway | oneway | cycleway | forw | backw | + | primary | yes | opposite | x | x | + | primary | yes | opposite_track | x | x | + | primary | yes | opposite_lane | x | x | + | primary | -1 | opposite | x | x | + | primary | -1 | opposite_track | x | x | + | primary | -1 | opposite_lane | x | x | + | primary | no | opposite | x | x | + | primary | no | opposite_track | x | x | + | primary | no | opposite_lane | x | x | + + @bicycle + Scenario: Cars should not be affected by bicycle tags + Given the speedprofile "car" + Then routability should be + | highway | junction | oneway | oneway:bicycle | forw | backw | + | primary | | yes | yes | x | | + | primary | | yes | no | x | | + | primary | | yes | -1 | x | | + | primary | | no | yes | x | x | + | primary | | no | no | x | x | + | primary | | no | -1 | x | x | + | primary | | -1 | yes | | x | + | primary | | -1 | no | | x | + | primary | | -1 | -1 | | x | + | primary | roundabout | | yes | x | | + | primary | roundabout | | no | x | | + | primary | roundabout | | -1 | x | | diff --git a/features/restrictions.feature b/features/restrictions.feature new file mode 100644 index 00000000000..55fc00642dc --- /dev/null +++ b/features/restrictions.feature @@ -0,0 +1,94 @@ +@routing @restrictions +Feature: Turn restrictions + Handle turn restrictions as defined by http://wiki.openstreetmap.org/wiki/Relation:restriction + How this plays with u-turns can be tricky. + + Scenario: No left turn + Given the nodes + | | t | | + | a | j | b | + | | s | | + + And the ways + | nodes | + | bj | + | aj | + | sj | + | tj | + + And the relations + | from | to | via | restriction | + | sj | aj | j | no_left_turn | + + When I route I should get + | from | to | route | + | s | a | | + | s | b | sj,jb | + | s | t | sj,tj | + | a | b | aj,bj | + | a | a | aj,sj | + | a | t | aj,tj | + | b | b | jb,aj | + | b | s | bj,sj | + | b | t | bj,tj | + + Scenario: No left turn, go counter-clockwise around the block instead + Given the nodes + | x | t | | + | a | j | b | + | | s | | + + And the ways + | nodes | + | bj | + | aj | + | sj | + | tj | + | axt | + + And the relations + | from | to | via | restriction | + | sj | aj | j | no_left_turn | + + When I route I should get + | from | to | route | + | s | a | sj,tj,axt | + | s | b | sj,jb | + | s | t | sj,tj | + | a | b | aj,bj | + | a | a | aj,sj | + | a | t | aj,tj | + | b | b | jb,aj | + | b | s | bj,sj | + | b | t | bj,tj | + + Scenario: No left turn, go clockwise around the block instead + Given the nodes + | | | t | | + | z | a | j | b | + | x | | s | | + + And the ways + | nodes | + | bj | + | aj | + | sj | + | tj | + | sxza | + + And the relations + | from | to | via | restriction | + | sj | aj | j | no_left_turn | + + When I route I should get + | from | to | route | + | s | a | sxza | + | s | b | sj,jb | + | s | t | sj,tj | + | a | b | aj,bj | + | a | a | aj,sj | + | a | t | aj,tj | + | b | b | jb,aj | + | b | s | bj,sj | + | b | t | bj,tj | + diff --git a/features/snap.feature b/features/snap.feature new file mode 100644 index 00000000000..8991f306bf2 --- /dev/null +++ b/features/snap.feature @@ -0,0 +1,86 @@ +@routing @snap +Feature: Snap start/end point to the nearest way + + Scenario: Snap to nearest protruding oneway + Given the nodes + | | 1 | | 2 | | + | 8 | | n | | 3 | + | | w | c | e | | + | 7 | | s | | 4 | + | | 6 | | 5 | | + + And the ways + | nodes | + | nc | + | ec | + | sc | + | wc | + + When I route I should get + | from | to | route | + | 1 | c | nc | + | 2 | c | nc | + | 3 | c | ec | + | 4 | c | ec | + | 5 | c | sc | + | 6 | c | sc | + | 7 | c | wc | + | 8 | c | wc | + + Scenario: Snap to nearest edge of a square + Given the nodes + | 4 | 5 | 6 | 7 | + | 3 | a | | u | + | 2 | | | | + | 1 | d | | b | + + And the ways + | nodes | + | aub | + | adb | + + When I route I should get + | from | to | route | + | 1 | b | adb | + | 2 | b | adb | + | 6 | b | aub | + | 7 | b | aub | + + Scenario: Snap to edge right under start/end point + Given the nodes + | d | e | f | g | + | c | | | h | + | b | | | i | + | a | l | k | j | + + And the ways + | nodes | + | abcd | + | defg | + | ghij | + | jkla | + + When I route I should get + | from | to | route | + | a | b | abcd | + | a | c | abcd | + | a | d | abcd | + | a | e | abcd | + | a | f | abcd | + | a | g | abcd | + | a | h | jkla | + | a | i | jkla | + | a | j | jkla | + | a | k | jkla | + | a | l | jkla | + | b | a | abcd | + | b | c | abcd | + | b | d | abcd | + | b | e | abcd | + | b | f | abcd | + | b | g | abcd | + | b | h | jkla | + | b | i | jkla | + | b | j | jkla | + | b | k | jkla | + | b | l | jkla | \ No newline at end of file diff --git a/features/step_definitions/data.rb b/features/step_definitions/data.rb new file mode 100644 index 00000000000..45a2c4c021c --- /dev/null +++ b/features/step_definitions/data.rb @@ -0,0 +1,73 @@ + +Given /^the speedprofile "([^"]*)"$/ do |profile| + read_speedprofile profile +end + +Given /^the speedprofile settings$/ do |table| + table.raw.each do |row| + speedprofile[ row[0] ] = row[1] + end +end + +Given /^the nodes$/ do |table| + table.raw.each_with_index do |row,ri| + row.each_with_index do |name,ci| + unless name.empty? + raise "*** node invalid name '#{name}', must be single characters" unless name.size == 1 + raise "*** invalid node name '#{name}', must me alphanumeric" unless name.match /[a-z0-9]/ + raise "*** duplicate node '#{name}'" if name_node_hash[name] + node = OSM::Node.new make_osm_id, OSM_USER, OSM_TIMESTAMP, ORIGIN[0]+ci*ZOOM, ORIGIN[1]-ri*ZOOM + node << { :name => name } + node.uid = OSM_UID + osm_db << node + name_node_hash[name] = node + end + end + end +end + +Given /^the ways$/ do |table| + table.hashes.each do |row| + name = row.delete 'nodes' + raise "*** duplicate way '#{name}'" if name_way_hash[name] + way = OSM::Way.new make_osm_id, OSM_USER, OSM_TIMESTAMP + defaults = { 'highway' => 'primary' } + way << defaults.merge( 'name' => name ).merge(row) + way.uid = OSM_UID + name.each_char do |c| + raise "*** node invalid name '#{c}', must be single characters" unless c.size == 1 + raise "*** ways cannot use numbered nodes, '#{name}'" unless c.match /[a-z]/ + node = find_node_by_name(c) + raise "*** unknown node '#{c}'" unless node + way << node + end + osm_db << way + name_way_hash[name] = way + end +end + +Given /^the relations$/ do |table| + table.hashes.each do |row| + relation = OSM::Relation.new make_osm_id, OSM_USER, OSM_TIMESTAMP + relation << { :type => :restriction, :restriction => 'no_left_turn' } + from_way = find_way_by_name(row['from']) + raise "*** unknown way '#{row['from']}'" unless from_way + to_way = find_way_by_name(row['to']) + raise "*** unknown way '#{row['to']}'" unless to_way + relation << OSM::Member.new( 'way', from_way.id, 'from' ) + relation << OSM::Member.new( 'way', to_way.id, 'to' ) + c = row['via'] + unless c.empty? + raise "*** node invalid name '#{c}', must be single characters" unless c.size == 1 + raise "*** via node cannot use numbered nodes, '#{c}'" unless c.match /[a-z]/ + via_node = find_node_by_name(c) + raise "*** unknown node '#{row['via']}'" unless via_node + relation << OSM::Member.new( 'node', via_node.id, 'via' ) + end + relation.uid = OSM_UID + osm_db << relation + end +end + +Given /^the defaults$/ do +end diff --git a/features/step_definitions/processing.rb b/features/step_definitions/processing.rb new file mode 100644 index 00000000000..44582dbe275 --- /dev/null +++ b/features/step_definitions/processing.rb @@ -0,0 +1,71 @@ +require 'OSM/StreamParser' + +class OSMTestParserCallbacks < OSM::Callbacks + @@locations = nil + + def self.locations + if @@locations + @@locations + else + #parse the test file, so we can later reference nodes and ways by name in tests + @@locations = {} + file = 'test/data/test.osm' + callbacks = OSMTestParserCallbacks.new + parser = OSM::StreamParser.new(:filename => file, :callbacks => callbacks) + parser.parse + puts @@locations + end + end + + def node(node) + @@locations[node.name] = [node.lat,node.lon] + end +end + + +Given /^the OSM file contains$/ do |string| + file = 'data/test.osm' + File.open( file, 'w') {|f| f.write(string) } + + #convert from .osm to .osm.pbf, which is the format osrm reads + system "osmosis --read-xml data/test.osm --write-pbf data/test.osm.pbf omitmetadata=true" +end + +Given /^the speedprofile contains$/ do |string| + File.open( 'speedprofile.ini', 'w') {|f| f.write(string) } +end + + + +Given /^the data file "([^"]*)" is present$/ do |file| + File.exists?(file).should == true +end + +When /^I run the extractor with "([^"]*)"$/ do |cmd| + @response = `#{cmd}` + #Dir.chdir @test_folder do + # @response = IO.popen([cmd, :err=>[:child, :out]]) { |ls_io| ls_result_with_error = ls_io.read } + #end +end + +When /^I run the preprocessor with "([^"]*)"$/ do |cmd| + @response = `#{cmd}` +end + +Given /^the preprocessed files for "([^"]*)" are present and up to date$/ do |area| + File.exists?("#{area}.osrm").should == true + File.exists?("#{area}.osrm.names").should == true + File.exists?("#{area}.osrm.restrictions").should == true + File.exists?("#{area}.osrm.hsgr").should == true + File.exists?("#{area}.osrm.nodes").should == true + File.exists?("#{area}.osrm.ramIndex").should == true + File.exists?("#{area}.osrm.fileIndex").should == true +end + +Then /^I should see the file "([^"]*)"$/ do |file| + File.exists?(file).should == true +end + +When /^preprocessed files for "([^"]*)" has been removed$/ do |file| + FileUtils.rm_r Dir["#{file}.*"], :secure => true +end diff --git a/features/step_definitions/routing.rb b/features/step_definitions/routing.rb new file mode 100644 index 00000000000..91210a1b664 --- /dev/null +++ b/features/step_definitions/routing.rb @@ -0,0 +1,222 @@ + +When /^I request a route from ([^"]+) to ([^"]+)$/ do |a,b| + @response = request_route a,b + #puts @response.body + #@response +end + +When /^I request a route from "([^"]*)" to "([^"]*)"$/ do |a,b| + locations = OSMTestParserCallbacks.locations + raise "Locations hash is empty. To reference nodes by name, please preprocess the test file earlier in the test." unless locations + raise "Unknown node: #{a}" unless locations[a] + raise "Unknown node: #{b}" unless locations[b] + @response = request_route "#{locations[a][0]},#{locations[a][1]}", "#{locations[b][0]},#{locations[b][1]}" +end + +Then /^I should get a response/ do + @response.code.should == "200" + @response.body.should_not == nil + @response.body.should_not == '' +end + +Then /^response should be valid JSON$/ do + @json = JSON.parse @response.body +end + +Then /^response should be well-formed$/ do + @json['version'].class.should == Float + @json['status'].class.should == Fixnum + @json['status_message'].class.should == String + @json['route_summary'].class.should == Hash + @json['route_geometry'].class.should == String + @json['route_instructions'].class.should == Array + @json['via_points'].class.should == Array + @json['transactionId'].class.should == String +end + +Then /^a route should be found$/ do + @json['status'].should == 0 + @json['status_message'].should == "Found route between points" +end + +Then /^no route should be found$/ do + @json['status'].should == 207 + @json['status_message'].should == "Cannot find route between points" +end + +Then /^I should get a valid response$/ do + step "I should get a response" + step "response should be valid JSON" + step "response should be well-formed" + #step "no error should be reported in terminal" +end + +Then /^I should get a route$/ do + step "I should get a valid response" + step "a route should be found" + #puts @response.body +end + +Then /^I should not get a route$/ do + step "I should get a valid response" + step "no route should be found" +end + +Then /^the route should start at "([^']*)"$/ do |name| + @json['route_summary']['start_point'].should == name +end + +Then /^the route should end at "([^']*)"$/ do |name| + @json['route_summary']['end_point'].should == name +end + +Then /^distance should be between (\d+) and (\d+)$/ do |min,max| + @json['route_summary']['total_distance'].to_i.should >= min.to_i + @json['route_summary']['total_distance'].to_i.should <= max.to_i +end + +Then /^the distance should be close to (\d+)m$/ do |d| + @json['route_summary']['total_distance'].to_i.should >= d.to_i*0.95 + @json['route_summary']['total_distance'].to_i.should <= d.to_i/0.95 +end + +Then /^number of instructions should be (\d+)$/ do |n| + @json['route_instructions'].size.should == n +end + +Then /^there should be 1 turn$/ do + step 'there should be 1 turns' +end + +Then /^there should be (\d+) turns$/ do |n| + @json['route_instructions'].map {|t| t.first}.select {|t| t =~ /^Turn/ }.size.should == n.to_i +end + +Then /^there should be more than (\d+) turn$/ do |n| + @json['route_instructions'].map {|t| t.first}.select {|t| t =~ /^Turn/ }.size.should > n.to_i +end + +Then /^there should not be any turns$/ do + (@json['route_instructions'].size-1).should == 0 +end + +def sanitize_route route + route.split(',').map{|w| w.strip}.reject(&:empty?).join(', ') +end + +def computed_route + @json['route_instructions'].map { |r| r[1] }.reject(&:empty?).join(', ') +end + +Then /^the route should follow "([^"]*)"$/ do |route| + sanitize_route(route).should == computed_route +end + +Then /^the route should not follow "([^"]*)"$/ do |route| + sanitize_route(route).should_not == computed_route +end + +Then /^the route should include "([^"]*)"$/ do |route| + sanitize_route(route).should =~ /#{computed_route}/ +end + +Then /^the route should not include "([^"]*)"$/ do |route| + sanitize_route(route).should_not =~ /#{computed_route}/ +end + +Then /^the route should stay on "([^"]*)"$/ do |way| + step "the route should start at \"#{way}\"" + step "the route should end at \"#{way}\"" + step "the route should follow \"#{way}\"" + step "there should not be any turns" +end + +When /^I route between "([^"]*)" and "([^"]*)"$/ do |from,to| + reprocess + Dir.chdir 'test' do + from_node = name_node_hash[from] + to_node = name_node_hash[to] + a = "#{from_node.lon},#{from_node.lat}" + b = "#{to_node.lon},#{to_node.lat}" + @route = parse_response( request_route(a,b) ) + end +end + +Then /^"([^"]*)" should be returned$/ do |route| + @route.should == route.split(',').join(',') +end + +Then /^routability should be$/ do |table| + osrm_kill + build_ways_from_table table + reprocess + actual = [] + if table.headers&["forw","backw"] == [] + raise "*** routability tabel must contain either 'forw' or 'backw' column" + end + OSRMLauncher.new do + table.hashes.each_with_index do |row,i| + got = row.dup + attempts = [] + if table.headers.include? 'forw' + response = request_route("#{ORIGIN[1]},#{ORIGIN[0]+(1+WAY_SPACING*i)*ZOOM}","#{ORIGIN[1]},#{ORIGIN[0]+(2+WAY_SPACING*i)*ZOOM}") + got['forw'] = route_status response + if got['forw'] != row['forw'] + json = JSON.parse(response.body) + attempts << { :attempt => 'Forward', :query => @query, :response => response } + end + end + if table.headers.include? 'backw' + response = request_route("#{ORIGIN[1]},#{ORIGIN[0]+(2+WAY_SPACING*i)*ZOOM}","#{ORIGIN[1]},#{ORIGIN[0]+(1+WAY_SPACING*i)*ZOOM}") + got['backw'] = route_status response + if got['backw'] != row['backw'] + attempts << { :attempt => 'Backward', :query => @query, :response => response } + end + end + if got != row + log_fail row,got,attempts + end + actual << got + end + end + table.diff! actual +end + +When /^I route I should get$/ do |table| + osrm_kill + reprocess + actual = [] + OSRMLauncher.new do + table.hashes.each_with_index do |row,ri| + from_node = @name_node_hash[ row['from'] ] + raise "*** unknown from-node '#{row['from']}" unless from_node + to_node = @name_node_hash[ row['to'] ] + raise "*** unknown to-node '#{row['to']}" unless to_node + response = request_route("#{from_node.lat},#{from_node.lon}", "#{to_node.lat},#{to_node.lon}") + if response.code == "200" && response.body.empty? == false + json = JSON.parse response.body + if json['status'] == 0 + instructions = way_list json['route_instructions'] + end + end + + got = {'from' => row['from'], 'to' => row['to'] } + if table.headers.include? 'start' + got['start'] = instructions ? json['route_summary']['start_point'] : nil + end + if table.headers.include? 'end' + got['end'] = instructions ? json['route_summary']['end_point'] : nil + end + if table.headers.include? 'route' + got['route'] = (instructions || '').strip + end + + if got['route'] != row['route'] || got['start'] != row['start'] || got['end'] != row['end'] + failed = { :attempt => 'Backward', :query => @query, :response => response } + log_fail row,got,[failed] + end + actual << got + end + end + table.diff! actual +end \ No newline at end of file diff --git a/features/support/config.rb b/features/support/config.rb new file mode 100644 index 00000000000..c0212708955 --- /dev/null +++ b/features/support/config.rb @@ -0,0 +1,40 @@ +def speedprofile + @speedprofile ||= reset_speedprofile +end + +def reset_speedprofile + @speedprofile = {} + read_speedprofile DEFAULT_SPEEDPROFILE +end + +def read_speedprofile profile + @speedprofile = {} + @speedprofile_str = nil + s = File.read "speedprofiles/#{profile}.ini" + s.scan /(.*)=(.*)/ do |option| + @speedprofile[option[0].strip] = option[1].strip + end +end + +def speedprofile_str + @speedprofile_str ||= "[Scenario: #{@scenario_title}]\n" + @speedprofile.map { |k,v| " #{k} = #{v}" }.join("\n") +end + +def write_speedprofile + File.open( 'speedprofile.ini', 'w') {|f| f.write( speedprofile_str ) } +end + +def write_server_ini + s=<<-EOF +Threads = 1 +IP = 0.0.0.0 +Port = 5000 + +hsgrData=#{@osm_file}.osrm.hsgr +nodesData=#{@osm_file}.osrm.nodes +ramIndex=#{@osm_file}.osrm.ramIndex +fileIndex=#{@osm_file}.osrm.fileIndex +namesData=#{@osm_file}.osrm.names +EOF + File.open( 'server.ini', 'w') {|f| f.write( s ) } +end diff --git a/features/support/data.rb b/features/support/data.rb new file mode 100644 index 00000000000..a3559c86b9e --- /dev/null +++ b/features/support/data.rb @@ -0,0 +1,188 @@ +require 'OSM/objects' #osmlib gem +require 'OSM/Database' +require 'builder' + +OSM_USER = 'osrm' +OSM_GENERATOR = 'osrm-test' +OSM_UID = 1 +TEST_FOLDER = 'test' +DATA_FOLDER = 'cache' +PREPROCESS_LOG_FILE = 'preprocessing.log' +LOG_FILE = 'fail.log' +OSM_TIMESTAMP = '2000-00-00T00:00:00Z' +DEFAULT_SPEEDPROFILE = 'bicycle' +WAY_SPACING = 10 + +ORIGIN = [1,1] +NODE_SPACING = 100 #meters +ZOOM = 0.001*(NODE_SPACING.to_f/111.0) + +def build_ways_from_table table + #add one unconnected way for each row + table.hashes.each_with_index do |row,ri| + #NOTE: + #currently osrm crashes when processing an isolated oneway with just 2 nodes, so we use 4 + #this is relatated to the fact that a oneway deadend doesn't make a lot of sense + + #if we stack ways on different x coordinates, outability tests get messed up, because osrm might pick a neighboring way if the one test can't be used. + #instead we place all lines as a string on the same y coordinate. this prevents using neightboring ways. + + #a few nodes... + node1 = OSM::Node.new make_osm_id, OSM_USER, OSM_TIMESTAMP, ORIGIN[0]+(0+WAY_SPACING*ri)*ZOOM, ORIGIN[1] + node2 = OSM::Node.new make_osm_id, OSM_USER, OSM_TIMESTAMP, ORIGIN[0]+(1+WAY_SPACING*ri)*ZOOM, ORIGIN[1] + node3 = OSM::Node.new make_osm_id, OSM_USER, OSM_TIMESTAMP, ORIGIN[0]+(2+WAY_SPACING*ri)*ZOOM, ORIGIN[1] + node4 = OSM::Node.new make_osm_id, OSM_USER, OSM_TIMESTAMP, ORIGIN[0]+(3+WAY_SPACING*ri)*ZOOM, ORIGIN[1] + node1.uid = OSM_UID + node2.uid = OSM_UID + node3.uid = OSM_UID + node4.uid = OSM_UID + node1 << { :name => "a#{ri}" } + node2 << { :name => "b#{ri}" } + node3 << { :name => "c#{ri}" } + node4 << { :name => "d#{ri}" } + + osm_db << node1 + osm_db << node2 + osm_db << node3 + osm_db << node4 + + #...with a way between them + way = OSM::Way.new make_osm_id, OSM_USER, OSM_TIMESTAMP + way.uid = OSM_UID + way << node1 + way << node2 + way << node3 + way << node4 + tags = row.dup + tags.delete 'forw' + tags.delete 'backw' + tags['name'] = "abcd#{ri}" + tags.reject! { |k,v| v=='' } + way << tags + osm_db << way + end +end + +def find_node_by_name s + name_node_hash[s.to_s] +end + +def find_way_by_name s + name_way_hash[s.to_s] || name_way_hash[s.to_s.reverse] +end + +def reset_data + Dir.chdir TEST_FOLDER do + #clear_log + #clear_data_files + end + reset_speedprofile + reset_osm + @fingerprint = nil +end + +def make_osm_id + @osm_id = @osm_id+1 +end + +def reset_osm + osm_db.clear + name_node_hash.clear + name_way_hash.clear + @osm_str = nil + @osm_hash = nil + + ##ID -1 causes trouble, so add a few nodes to avoid it + #node = OSM::Node.new nil, OSM_USER, OSM_TIMESTAMP, 0,0 + #node = OSM::Node.new nil, OSM_USER, OSM_TIMESTAMP, 0,0 + @osm_id = 0 +end + +def clear_data_files + File.delete *Dir.glob("#{DATA_FOLDER}/test.*") +end + +def clear_log + File.delete *Dir.glob("*.log") +end + +def osm_db + @osm_db ||= OSM::Database.new +end + +def name_node_hash + @name_node_hash ||= {} +end + +def name_way_hash + @name_way_hash ||= {} +end + +def osm_str + return @osm_str if @osm_str + @osm_str = '' + doc = Builder::XmlMarkup.new :indent => 2, :target => @osm_str + doc.instruct! + osm_db.to_xml doc, OSM_GENERATOR + @osm_str +end + +def write_osm + #write .oms file if needed + Dir.mkdir DATA_FOLDER unless File.exist? DATA_FOLDER + @osm_file = "#{DATA_FOLDER}/#{fingerprint}" + unless File.exist?("#{@osm_file}.osm") + File.open( "#{@osm_file}.osm", 'w') {|f| f.write(osm_str) } + end +end + +def convert_osm_to_pbf + unless File.exist?("#{@osm_file}.osm.pbf") + log_preprocess_info + log "== Converting #{@osm_file}.osm to protobuffer format...", :preprocess + #redirect stdout and stderr to a log file avoid output in the cucumber console + unless system "osmosis --read-xml #{@osm_file}.osm --write-pbf #{@osm_file}.osm.pbf omitmetadata=true 1>>#{PREPROCESS_LOG_FILE} 2>>#{PREPROCESS_LOG_FILE}" + raise "Failed to convert to proto buffer format. Please see #{hash}.log for more info." + end + log '', :preprocess + end +end + +def extracted? + File.exist?("#{@osm_file}.osrm") && + File.exist?("#{@osm_file}.osrm.names") && + File.exist?("#{@osm_file}.osrm.restrictions") +end + +def prepared? + base = "#{DATA_FOLDER}/#{fingerprint}" + File.exist?("#{base}.osrm.hsgr") +end + +def reprocess + Dir.chdir TEST_FOLDER do + write_speedprofile + write_osm + convert_osm_to_pbf + unless extracted? + log_preprocess_info + log "== Extracting #{@osm_file}.osm...", :preprocess + unless system "../osrm-extract #{@osm_file}.osm.pbf 1>>#{PREPROCESS_LOG_FILE} 2>>#{PREPROCESS_LOG_FILE}" + log "*** Exited with code #{$?.exitstatus}.", :preprocess + raise "*** osrm-extract exited with code #{$?.exitstatus}. The file preprocess.log might contain more info." + end + log '', :preprocess + end + unless prepared? + log_preprocess_info + log "== Preparing #{@osm_file}.osm...", :preprocess + unless system "../osrm-prepare #{@osm_file}.osrm #{@osm_file}.restrictions 1>>#{PREPROCESS_LOG_FILE} 2>>#{PREPROCESS_LOG_FILE}" + log "*** Exited with code #{$?.exitstatus}.", :preprocess + raise "*** osrm-prepare exited with code #{$?.exitstatus}. The file preprocess.log might contain more info." + end + log '', :preprocess + end + log_preprocess_done + write_server_ini + end +end diff --git a/features/support/hash.rb b/features/support/hash.rb new file mode 100644 index 00000000000..54636fa74e1 --- /dev/null +++ b/features/support/hash.rb @@ -0,0 +1,37 @@ +require 'digest/sha1' + +def hash_of_file path + hash = Digest::SHA1.new + open(path,'r') do |io| + while !io.eof + buf = io.readpartial 1024 + hash.update buf + end + end + return hash.hexdigest +end + +def speedprofile_hash + @speedprofile_hash ||= Digest::SHA1.hexdigest speedprofile_str +end + +def osm_hash + @osm_hash ||= Digest::SHA1.hexdigest osm_str +end + +def osm_hash + @osm_hash ||= Digest::SHA1.hexdigest osm_str +end + +def bin_extract_hash + @bin_hash ||= hash_of_file '../osrm-extract' +end + +def bin_prepare_hash + @bin_hash ||= hash_of_file '../osrm-prepare' +end + +#combine state of data, speedprofile and binaries into a hash that identifies the exact test scenario +def fingerprint + @fingerprint ||= Digest::SHA1.hexdigest "#{bin_extract_hash}-#{bin_prepare_hash}-#{speedprofile_hash}-#{osm_hash}" +end diff --git a/features/support/hooks.rb b/features/support/hooks.rb new file mode 100644 index 00000000000..4786eef3817 --- /dev/null +++ b/features/support/hooks.rb @@ -0,0 +1,17 @@ +Before do |scenario| + @scenario_title = scenario.title + @scenario_time = Time.now.strftime("%Y-%m-%dT%H:%m:%SZ") + reset_data + @has_logged_preprocess_info = false + @has_logged_scenario_info = false +end + +Around('@routing') do |scenario, block| + Timeout.timeout(10) do + block.call + end +end + +After do + osrm_kill +end \ No newline at end of file diff --git a/features/support/launch.rb b/features/support/launch.rb new file mode 100644 index 00000000000..d125985ac26 --- /dev/null +++ b/features/support/launch.rb @@ -0,0 +1,71 @@ +require 'socket' +require 'sys/proctable' + +class OSRMLauncher + def initialize &block + Dir.chdir TEST_FOLDER do + osrm_up + yield + osrm_down + end + end +end + +def each_process name, &block + Sys::ProcTable.ps do |process| + if process.comm.strip == name.strip + yield process.pid.to_i, process.state.strip + end + end +end + +def osrm_up? + find_pid('osrm-routed') != nil +end + +def find_pid name + each_process(name) { |pid,state| return pid.to_i } + return nil +end + +def osrm_up + return if osrm_up? + pipe = IO.popen('../osrm-routed 1>>osrm-routed.log 2>>osrm-routed.log') + timeout = 5 + (timeout*10).times do + begin + socket = TCPSocket.new('localhost', 5000) + socket.puts 'ping' + rescue Errno::ECONNREFUSED + sleep 0.1 + end + end + sleep 0.1 +end + +def osrm_down + each_process('osrm-routed') { |pid,state| Process.kill 'TERM', pid } + each_process('osrm-prepare') { |pid,state| Process.kill 'TERM', pid } + each_process('osrm-extract') { |pid,state| Process.kill 'TERM', pid } + wait_for_shutdown 'osrm-routed' + wait_for_shutdown 'osrm-prepare' + wait_for_shutdown 'osrm-extract' +end + +def osrm_kill + each_process('osrm-routed') { |pid,state| Process.kill 'KILL', pid } + each_process('osrm-prepare') { |pid,state| Process.kill 'KILL', pid } + each_process('osrm-extract') { |pid,state| Process.kill 'KILL', pid } + wait_for_shutdown 'osrm-routed' + wait_for_shutdown 'osrm-prepare' + wait_for_shutdown 'osrm-extract' +end + +def wait_for_shutdown name + timeout = 10 + (timeout*10).times do + return if find_pid(name) == nil + sleep 0.1 + end + raise "*** Could not terminate #{name}." +end diff --git a/features/support/log.rb b/features/support/log.rb new file mode 100644 index 00000000000..45abe379527 --- /dev/null +++ b/features/support/log.rb @@ -0,0 +1,65 @@ +def log s='', type=nil + if type == :preprocess + file = PREPROCESS_LOG_FILE + else + file = LOG_FILE + end + File.open(file, 'a') {|f| f.write("#{s}\n") } +end + + +def log_scenario_fail_info + return if @has_logged_scenario_info + log "=========================================" + log "Failed scenario: #{@scenario_title}" + log "Time: #{@scenario_time}" + log + log '```xml' #so output can be posted directly to github comment fields + log osm_str.strip + log '```' + log + log speedprofile_str + log + @has_logged_scenario_info = true +end + +def log_fail expected,actual,failed + log_scenario_fail_info + log "== " + log "Expected: #{expected}" + log "Got: #{actual}" + log + failed.each do |fail| + log "Attempt: #{fail[:attempt]}" + log "Query: #{fail[:query]}" + log "Response: #{fail[:response].body}" + log + end +end + + +def log_preprocess_info + return if @has_logged_preprocess_info + log "=========================================", :preprocess + log "Preprocessing data for scenario: #{@scenario_title}", :preprocess + log "Time: #{@scenario_time}", :preprocess + log '', :preprocess + log "== OSM data:", :preprocess + log '```xml', :preprocess #so output can be posted directly to github comment fields + log osm_str, :preprocess + log '```', :preprocess + log '', :preprocess + log "== Speed profile:", :preprocess + log speedprofile_str.strip, :preprocess + log '', :preprocess + @has_logged_preprocess_info = true +end + +def log_preprocess str + log_preprocess_info + log str, :preprocess +end + +def log_preprocess_done +end + diff --git a/features/support/osmlib.rb b/features/support/osmlib.rb new file mode 100644 index 00000000000..6d59964d14a --- /dev/null +++ b/features/support/osmlib.rb @@ -0,0 +1,15 @@ + +#monkey-patch osmlib to fix a bug + +module OSM + class Way + def to_xml(xml) + xml.way(attributes) do + nodes.each do |node| + xml.nd(:ref => node) + end + tags.to_xml(xml) + end + end + end +end \ No newline at end of file diff --git a/features/support/route.rb b/features/support/route.rb new file mode 100644 index 00000000000..6ec107fa113 --- /dev/null +++ b/features/support/route.rb @@ -0,0 +1,68 @@ +require 'net/http' + +def request_route a,b + @query = "http://localhost:5000/viaroute&start=#{a}&dest=#{b}&output=json&geomformat=cmp" + #log @query + uri = URI.parse @query + Net::HTTP.get_response uri +rescue Errno::ECONNREFUSED => e + raise "*** osrm-routed is not running." +rescue Timeout::Error + raise "*** osrm-routed did not respond." +end + +def parse_response response + if response.code == "200" && response.body.empty? == false + json = JSON.parse response.body + if json['status'] == 0 + route = way_list json['route_instructions'] + if route.empty? + "Empty route: #{json['route_instructions']}" + else + "Route: #{route}" + end + elsif json['status'] == 207 + "No route" + else + "Status: #{json['status']}" + end + else + "HTTP: #{response.code}" + end +end + +def got_route? response + if response.code == "200" && !response.body.empty? + json = JSON.parse response.body + if json['status'] == 0 + return way_list( json['route_instructions']).empty? == false + end + end + false +end + +def route_status response + if response.code == "200" && !response.body.empty? + json = JSON.parse response.body + if json['status'] == 0 + if way_list( json['route_instructions']).empty? + return 'Empty route' + else + return 'x' + end + elsif json['status'] == 207 + '' + else + "Status #{json['status']}" + end + else + "HTTP #{response.code}" + end +end + +def way_list instructions + instructions. + #reject { |i| i[2]<=1 }. #FIXME temporary hack to ignore instructions with length==0 + map { |r| r[1] }. + reject(&:empty?).join(',') +end \ No newline at end of file diff --git a/features/weight.feature b/features/weight.feature new file mode 100644 index 00000000000..f54d6cc5278 --- /dev/null +++ b/features/weight.feature @@ -0,0 +1,28 @@ +@routing @weight +Feature: Choosing route based on length, speed, etc + + Scenario: Pick the geometrically shortest route, way types being equal + Given the nodes + | | s | | + | | t | | + | a | | b | + + And the ways + | nodes | + | atb | + | asb | + + When I route I should get + | from | to | route | + | a | b | atb | + | a | b | atb | + + Scenario: Pick the fastest way type, lengths being equal + Given the nodes + | a | s | + | p | b | + + And the ways + | nodes | highway | + | apb | primary | + | asb | secondary | diff --git a/sandbox/.gitignore b/sandbox/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/server.ini b/server.ini index fbf0108d261..2a6d1dac55c 100644 --- a/server.ini +++ b/server.ini @@ -2,8 +2,8 @@ Threads = 8 IP = 0.0.0.0 Port = 5000 -hsgrData=/opt/osm/germany.osrm.hsgr -nodesData=/opt/osm/germany.osrm.nodes -ramIndex=/opt/osm/germany.osrm.ramIndex -fileIndex=/opt/osm/germany.osrm.fileIndex -namesData=/opt/osm/germany.osrm.names +hsgrData=data/kbh.osrm.hsgr +nodesData=data/kbh.osrm.nodes +ramIndex=data/kbh.osrm.ramIndex +fileIndex=data/kbh.osrm.fileIndex +namesData=data/kbh.osrm.names diff --git a/speedprofile.ini b/speedprofile.ini index 2a504a0cd11..d6e6187b771 100644 --- a/speedprofile.ini +++ b/speedprofile.ini @@ -1,48 +1,27 @@ -[car] - motorway = 110 - motorway_link = 90 - trunk = 90 - trunk_link = 70 - primary = 70 - primary_link = 60 - secondary = 60 - secondary_link = 50 - tertiary = 55 - unclassified = 25 - residential = 40 - living_street = 10 - service = 30 - ferry = 5 - pier = 5 - obeyBollards = yes - obeyOneways = yes - useRestrictions = yes - accessTag = motorcar - excludeFromGrid = ferry - defaultSpeed = 50 - trafficLightPenalty = 15 -[bike] - trunk = 16 - trunk_link = 16 - primary = 16 - primary_link = 16 - secondary = 16 - secondary_link = 16 - tertiary = 16 - unclassified = 16 - residential = 16 - living_street = 16 - service = 16 - track = 16 - cycleway = 16 - path = 16 - ferry = 5 - pier = 5 - obeyOneways = yes - useRestrictions = no - accessTag = bicycle +[bicycle] + accessTag = bicycle + defaultSpeed = 17 + obeyOneways = yes + useRestrictions = yes + obeyBollards = no + + cycleway = 19 + primary = 19 + primary_link = 19 + secondary = 17 + secondary_link = 17 + tertiary = 15 + residential = 15 + unclassified = 15 + living_street = 13 + road = 13 + service = 12 + track = 12 + path = 12 + footway = 10 + pedestrian = 5 + pier = 5 + steps = 3 + ferry = 5 + excludeFromGrid = ferry - defaultSpeed = 5 - trafficLightPenalty = 15 - obeyBollards = no - \ No newline at end of file diff --git a/speedprofiles/bicycle.ini b/speedprofiles/bicycle.ini new file mode 100644 index 00000000000..d6e6187b771 --- /dev/null +++ b/speedprofiles/bicycle.ini @@ -0,0 +1,27 @@ +[bicycle] + accessTag = bicycle + defaultSpeed = 17 + obeyOneways = yes + useRestrictions = yes + obeyBollards = no + + cycleway = 19 + primary = 19 + primary_link = 19 + secondary = 17 + secondary_link = 17 + tertiary = 15 + residential = 15 + unclassified = 15 + living_street = 13 + road = 13 + service = 12 + track = 12 + path = 12 + footway = 10 + pedestrian = 5 + pier = 5 + steps = 3 + ferry = 5 + + excludeFromGrid = ferry diff --git a/speedprofiles/car.ini b/speedprofiles/car.ini new file mode 100644 index 00000000000..30b9fedc688 --- /dev/null +++ b/speedprofiles/car.ini @@ -0,0 +1,22 @@ +[car] + accessTag = motorcar + defaultSpeed = 50 + obeyOneways = yes + useRestrictions = yes + barrier = bollard + + motorway = 100 + motorway_link = 90 + trunk = 90 + trunk_link = 70 + primary = 70 + primary_link = 60 + secondary = 60 + secondary_link = 50 + tertiary = 50 + tertiary_link = 40 + road = 40 + residential = 40 + unclassified = 30 + service = 20 + living_street = 10 diff --git a/speedprofiles/ebike.ini b/speedprofiles/ebike.ini new file mode 100644 index 00000000000..7b53dbbf824 --- /dev/null +++ b/speedprofiles/ebike.ini @@ -0,0 +1,26 @@ +[ebike] + accessTag = bicycle + obeyOneways = yes + defaultSpeed = 25 + useRestrictions = no + + cycleway = 25 + trunk = 25 + trunk_link = 25 + primary = 25 + primary_link = 25 + secondary = 25 + secondary_link = 25 + tertiary = 25 + unclassified = 25 + residential = 25 + living_street = 20 + service = 20 + track = 20 + path = 15 + pier = 5 + pedestrian = 5 + footway = 5 + ferry = 5 + + excludeFromGrid = ferry diff --git a/test/.stxxl b/test/.stxxl new file mode 100644 index 00000000000..fc35567057c --- /dev/null +++ b/test/.stxxl @@ -0,0 +1 @@ +disk=/tmp/stxxl,1,syscall \ No newline at end of file diff --git a/test/contractor.ini b/test/contractor.ini new file mode 100644 index 00000000000..fa39ddf7383 --- /dev/null +++ b/test/contractor.ini @@ -0,0 +1,2 @@ +Threads = 4 +SRTM = data/srtm/Eurasia \ No newline at end of file diff --git a/test/extractor.ini b/test/extractor.ini new file mode 100644 index 00000000000..aa6ba2e304d --- /dev/null +++ b/test/extractor.ini @@ -0,0 +1 @@ +Memory = 1 \ No newline at end of file