routing 0.0.2

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 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