routing 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .navteq_host
6
+ .yardoc
7
+ .rspec
8
+ .rvmrc
9
+ Gemfile.lock
10
+ InstalledFiles
11
+ _yardoc
12
+ coverage
13
+ doc/
14
+ lib/bundler/man
15
+ pkg
16
+ rdoc
17
+ spec/reports
18
+ test/tmp
19
+ test/version_tmp
20
+ tmp
21
+ **/.DS_Store
22
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ gem "awesome_print"
6
+ gem "yard"
7
+ gem "redcarpet"
8
+ gem "guard-rspec"
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard 'rspec', :version => 2, :all_on_start => true, :all_after_pass => true do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 flinc AG
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Routing
2
+
3
+ Provides a generic interface for routing services that can by used to calculate directions between geolocations.
4
+
5
+ It aims to make parsing and use-case specific data handling easy trough an extendable middleware stack (think of rack middleware for your routing service).
6
+
7
+ ## Usage
8
+
9
+ ```ruby
10
+ start = Routing::GeoPoint.new(:lat => 49, :lng => 9)
11
+ destination = Routing::GeoPoint.new(:lat => 48, :lng => 8.9)
12
+
13
+ route = Routing.new.calculate(start, destination)
14
+ ```
15
+
16
+ ### Middleware
17
+
18
+ ```ruby
19
+ routing = Routing.new do |r|
20
+ r.use MyMiddleware.new
21
+ r.use MyRoutingCache.new
22
+ end
23
+
24
+ route = routing.calculate(start, destination)
25
+ ```
26
+
27
+ ### Custom Adapters
28
+
29
+ ```ruby
30
+ routing = Routing.new(MyAdapter.new)
31
+ route = routing.calculate(start, destination)
32
+ ```
33
+
34
+ ## Installation
35
+
36
+ Add this line to your application's Gemfile:
37
+
38
+ gem 'routing'
39
+
40
+ And then execute:
41
+
42
+ $ bundle
43
+
44
+ Or install it yourself as:
45
+
46
+ $ gem install routing
47
+
48
+ ## Contributing
49
+
50
+ 1. Fork it
51
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
52
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
53
+ 4. Push to the branch (`git push origin my-new-feature`)
54
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+ require 'yard'
5
+
6
+ RSpec::Core::RakeTask.new('spec')
7
+
8
+ YARD::Rake::YardocTask.new
9
+
10
+ task :default => :spec
data/lib/routing.rb ADDED
@@ -0,0 +1,77 @@
1
+ require "routing/version"
2
+
3
+ # The {Routing} class is the main entry point for the library.
4
+ class Routing
5
+
6
+ autoload :Adapter, "routing/adapter"
7
+ autoload :GeoPoint, "routing/geo_point"
8
+ autoload :Middleware, "routing/middleware"
9
+ autoload :Parser, "routing/parser"
10
+
11
+ class << self
12
+
13
+ # Sets the default adapter/routing service.
14
+ #
15
+ # @return [Object] Default adapter.
16
+ attr_writer :default_adapter
17
+
18
+ # The default adapter/routing service that is used, if no one is specified.
19
+ # Currently this is {Routing::Adapter::Navteq}.
20
+ #
21
+ # @return [Object] Current default adapter.
22
+ def default_adapter
23
+ @default_adapter ||= Routing::Adapter::Navteq.new
24
+ end
25
+
26
+ end
27
+
28
+ attr_accessor :middlewares
29
+
30
+ # Creates a new instance of the routing class
31
+ #
32
+ # @param [Object] adapter Adapter for the routing service that should be used, defaults to {Routing::Adapter::Navteq}.
33
+ def initialize(adapter = self.class.default_adapter)
34
+ @adapter = adapter
35
+ @middlewares = []
36
+
37
+ yield(self) if block_given?
38
+ end
39
+
40
+ # Calculates a route for the passed {GeoPoint}s.
41
+ # These will be passed through the middleware stack and will
42
+ # finally be given to the configured adapter which calculates a route out of it.
43
+ #
44
+ # @param [Array<GeoPoint>] geo_points
45
+ # An array of geo points to calculate a route of.
46
+ #
47
+ # @return [Array<GeoPoint>]
48
+ # An array of geo points that represent the calculated route.
49
+ def calculate(*geo_points)
50
+ calculate_with_stack(geo_points.flatten, middlewares + [@adapter])
51
+ end
52
+
53
+ # Adds an object to the middleware stack.
54
+ #
55
+ # @param [Routing::Middleware] middleware The middleware to append to the stack.
56
+ #
57
+ # @return [Array<Routing::Middleware>] Updated list of used middlewares.
58
+ def use(middleware)
59
+ @middlewares << middleware
60
+ end
61
+
62
+ private
63
+
64
+ # Helper method that will iterate through the middleware stack.
65
+ #
66
+ # @param [Array<GeoPoint>] geo_points
67
+ # The array of geo points to be passed to the middleware.
68
+ #
69
+ # @param [Routing::Middleware] stack
70
+ # The remaining stack of middlewares to iterate through.
71
+ def calculate_with_stack(geo_points, stack)
72
+ stack.shift.calculate(geo_points) do |gp|
73
+ calculate_with_stack(gp, stack)
74
+ end
75
+ end
76
+
77
+ end
@@ -0,0 +1,22 @@
1
+ class Routing
2
+
3
+ # The {Routing::Adapter} namespace holds classes that are responsible to accept an
4
+ # array of {Routing::GeoPoint}s and calculate a route based on these positions.
5
+ #
6
+ # Most commonly they will get the calculated route from a webservice and will create a compatible
7
+ # return value by using a matching {Routing::Parser}.
8
+ #
9
+ # The end-point of the middleware stack is always the chosen adapter of a {Routing} instance which has to return the
10
+ # calculated route in form of {Routing::GeoPoint} objects again.
11
+ #
12
+ # Creating your own adapter:
13
+ # To connect your own routing service, you can easily implement your own adapter.
14
+ # The only requirements are an instance method called *calculate* that will take an Array of {Routing::GeoPoint}s
15
+ # as only parameters and will return an Array of {Routing::GeoPoint}s when the calculation is done.
16
+ module Adapter
17
+
18
+ autoload :Navteq, "routing/adapter/navteq"
19
+ autoload :Test, "routing/adapter/test"
20
+
21
+ end
22
+ end
@@ -0,0 +1,83 @@
1
+ require 'time'
2
+ require 'faraday'
3
+
4
+ class Routing
5
+ module Adapter
6
+
7
+ # Adapter for a NAVTEQ LBSP Routing Service v6 server.
8
+ # It passes the {GeoPoint}s to the routing service and will return another
9
+ # Array of {GeoPoint}s, representing the calculated route.
10
+ class Navteq
11
+
12
+ ATTR_ACCESSIBLE = [ :host, :default_params ]
13
+
14
+ def initialize(options = {})
15
+ options.each do |attribute, value|
16
+ send("#{attribute}=", value) if ATTR_ACCESSIBLE.include?(attribute)
17
+ end
18
+ end
19
+
20
+ def calculate(geo_points)
21
+ parse get(geo_points)
22
+ end
23
+
24
+ def get(geo_points)
25
+ params = default_params.merge geo_points_to_params(geo_points)
26
+
27
+ response = connection.get do |request|
28
+ request.url(service_path)
29
+ request.params = params
30
+ end
31
+
32
+ response.body
33
+ end
34
+
35
+ attr_writer :host
36
+
37
+ def host
38
+ @host
39
+ end
40
+
41
+ def parse(response)
42
+ ::Routing::Parser::NavteqSimple.new(response).to_geo_points
43
+ end
44
+
45
+ attr_writer :default_params
46
+
47
+ def default_params
48
+ @default_params || {
49
+ departure: Time.now.utc.iso8601,
50
+ mode0: "fastest;car",
51
+ language: "de_DE",
52
+ legattributes: "all,-links",
53
+ maneuverattributes: "position,travelTime,length,time"
54
+ }
55
+ end
56
+
57
+ protected
58
+
59
+ # The path on the server to the routing service.
60
+ # This is determined by the server and should not be changed.
61
+ #
62
+ # @returns [String]
63
+ # Path to the routing service.
64
+ def service_path
65
+ "/routing/6.2/calculateroute.json"
66
+ end
67
+
68
+ def geo_points_to_params(geo_points)
69
+ Hash[geo_points.each_with_index.map { |point, i| [ "waypoint#{i}", "geo!#{point.lat},#{point.lng}" ] }]
70
+ end
71
+
72
+ def connection
73
+ @connection ||= Faraday.new(:url => host) do |builder|
74
+ builder.request :url_encoded
75
+ builder.response :logger
76
+ builder.adapter :net_http
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,24 @@
1
+ class Routing
2
+ module Adapter
3
+
4
+ # Simple test adapter, that returns an array of {GeoPoint}s
5
+ # with values which are based on the ones it recieved.
6
+ class Test
7
+
8
+ def calculate(geo_points)
9
+ geo_points.collect do |point|
10
+ GeoPoint.new(
11
+ :lat => point.lat,
12
+ :lng => point.lng,
13
+ :original_lat => point.lat,
14
+ :original_lng => point.lng,
15
+ :relative_time => 100,
16
+ :distance => 100,
17
+ :waypoint => true
18
+ )
19
+ end
20
+ end
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ class Routing
2
+
3
+ # A {GeoPoint} object is a very simple representation of a geospatial point
4
+ # that is part of a route or can be used as base to create one.
5
+ #
6
+ # It holds the most basic textual and geospatial information that is necessary
7
+ # to describe a section of a route.
8
+ #
9
+ # The complete roundtrip from a {Routing} object through the {Routing::Middleware} to a
10
+ # {Routing::Adapter} and back uses arrays of {GeoPoint} objects as arguments.
11
+ #
12
+ # Depending on your very own use-case, you can either extend the {GeoPoint} class or
13
+ # use another class that mimicks the behavior of {GeoPoint}.
14
+ # In fact, this is just a container class which is no hard requirement in general.
15
+ # If you decide to roll your own {Adapter}, {Parser} and maybe {Middleware},
16
+ # you could replace the class completely
17
+ class GeoPoint < Struct.new(:lat, :lng, :original_lat, :original_lng, :relative_time, :distance, :waypoint, :type)
18
+
19
+ # Creates a new GeoPoint instance.
20
+ #
21
+ # @param [Hash] attributes
22
+ # Automatically sets values for the attributes that match the keys of the hash.
23
+ def initialize(attributes = {})
24
+ attributes.each do |attribute, value|
25
+ self[attribute] = value if members.include?(attribute)
26
+ end
27
+ end
28
+
29
+ def waypoint?
30
+ !!waypoint
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ class Routing
2
+ # An abstract middleware class to illustrate how you can build your own.
3
+ #
4
+ # @abstract
5
+ class Middleware
6
+
7
+ # The only method your own middleware has to implement to work.
8
+ #
9
+ # @param [Array<GeoPoint>] geo_points
10
+ # The array of {GeoPoint}s that should be routed.
11
+ #
12
+ # @yield [Array<GeoPoint>]
13
+ # Passes the geo_points on to the next middleware in the stack.
14
+ # Please note, that you _always_ have to call yield in order to make
15
+ # the middleware stack proceed.
16
+ #
17
+ # If you will return a value before yielding, you will cancel the rest of the middleware stack.
18
+ #
19
+ # @return [Array<GeoPoint>]
20
+ # The array of {GeoPoint}s after they have been routed.
21
+ def calculate(geo_points)
22
+ # manipulate geo points before routing here
23
+ yield(geo_points) # hand control to the next middleware in the stack
24
+ # manipulate geo points after routing here
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,26 @@
1
+ class Routing
2
+ # The {Routing::Parser} namespace holds classes that are used to parse the
3
+ # response of a specific routing service.
4
+ #
5
+ # They are used by the matching adapters {Routing::Adapter} to create the array of {Routing::GeoPoint} objects
6
+ # to pass into the middleware.
7
+ module Parser
8
+
9
+ autoload :NavteqSimple, "routing/parser/navteq_simple"
10
+
11
+ # This error is thrown by the parsers if a calculated waypoint can't be matched
12
+ # to one of the initial passed geo points that were routed.
13
+ #
14
+ # The reason why this is important is that most routing services have a "snap-to-route"
15
+ # algorithm that will manipulate the coordinates of a passed geo point by moving it to
16
+ # the next routeable position.
17
+ # A parser should be able to determine which geo point of the response belongs to which geo point of the
18
+ # request. Otherwise this error will be thrown.
19
+ class NoMatchingMappedPositionFound < StandardError
20
+ end
21
+
22
+ class RoutingFailed < StandardError
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,112 @@
1
+ class Routing
2
+ module Parser
3
+
4
+ # A very simple parser implementation for a NAVTEQ LBSP Routing Service v6.
5
+ # It converts the json response of the routing service to an Array of {GeoPoint}s.
6
+ class NavteqSimple
7
+
8
+ # Creates a new instance of the parser.
9
+ #
10
+ # @param [String] response
11
+ # A json response string of a NAVTEQ routing server.
12
+ def initialize(response)
13
+ response = JSON.parse(response)
14
+ check_for_error(response)
15
+
16
+ @route = response["Response"]["Route"].first
17
+ @overall_covered_distance = 0
18
+ @overall_relative_time = 0
19
+
20
+ self
21
+ end
22
+
23
+ # Converts the server response in an Array of {GeoPoint}s
24
+ #
25
+ # @return [Array<GeoPoint>]
26
+ # List of {GeoPoint}s that represent the calculated route.
27
+ def to_geo_points
28
+ legs = @route["Leg"]
29
+ geo_points = legs.map { |leg| parse_leg(leg) }.flatten
30
+
31
+ # At last we add the destination point
32
+ geo_points << parse_maneuver(legs.last["Maneuver"].last, waypoint: true)
33
+ geo_points
34
+ end
35
+
36
+ private
37
+
38
+ # Parses is single leg of the route including all its maneuvers.
39
+ #
40
+ # @param [Hash] leg
41
+ # The route leg to parse.
42
+ #
43
+ # @return [Array<GeoPoint>]
44
+ # List of {GeoPoint}s that represent the passed Leg.
45
+ def parse_leg(leg)
46
+ # Skip the last maneuver as it is the same as the first one
47
+ # of the next maneuver.
48
+ # For the last leg we parse the last maneuver right at the end
49
+ maneuvers = leg["Maneuver"][0...-1]
50
+ maneuvers.map do |maneuver|
51
+ parse_maneuver(maneuver, :waypoint => (maneuver == maneuvers.first))
52
+ end
53
+ end
54
+
55
+ # Parses is single maneuver of a route leg.
56
+ #
57
+ # @param [Hash] maneuver
58
+ # The maneuver to parse.
59
+ #
60
+ # @param [Hash] attributes
61
+ # Additional attributes that should be set on the returned {GeoPoint}.
62
+ #
63
+ # @return [GeoPoint]
64
+ # A {GeoPoint} that represents the passed maneuver.
65
+ def parse_maneuver(maneuver, attributes = {})
66
+ geo_point = ::Routing::GeoPoint.new attributes.merge({
67
+ lat: maneuver["Position"]["Latitude"],
68
+ lng: maneuver["Position"]["Longitude"],
69
+ relative_time: @overall_relative_time,
70
+ distance: @overall_covered_distance
71
+ })
72
+
73
+ @overall_relative_time += maneuver["TravelTime"].to_i
74
+ @overall_covered_distance += maneuver["Length"].to_i
75
+
76
+ search_original_position(geo_point) if geo_point.waypoint?
77
+
78
+ geo_point
79
+ end
80
+
81
+ # Matches a parsed {GeoPoint} of the route response
82
+ # with the (unmapped) position of the
83
+ # corresponding {GeoPoint} of the request.
84
+ #
85
+ # @param [GeoPoint] geo_point
86
+ # Point of the response to find the initial position for.
87
+ #
88
+ # @return [GeoPoint]
89
+ # The passed in {GeoPoint}, enriched with the information about the original position in the request.
90
+ #
91
+ # @raise [NoMatchingMappedPositionFound] If no matching original position is found.
92
+ def search_original_position(geo_point)
93
+ matching_waypoint = @route["Waypoint"].detect do |waypoint|
94
+ waypoint["MappedPosition"]["Latitude"] == geo_point.lat &&
95
+ waypoint["MappedPosition"]["Longitude"] == geo_point.lng
96
+ end or raise NoMatchingMappedPositionFound
97
+
98
+ geo_point.original_lat = matching_waypoint["OriginalPosition"]["Latitude"]
99
+ geo_point.original_lng = matching_waypoint["OriginalPosition"]["Longitude"]
100
+
101
+ geo_point
102
+ end
103
+
104
+ def check_for_error(response)
105
+ if error = response['Error']
106
+ raise Routing::Parser::RoutingFailed.new("#{error['type']}(#{error['subtype']}) - #{error['Details']}")
107
+ end
108
+ end
109
+
110
+ end
111
+ end
112
+ end