google_distance_matrix 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rbenv-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +106 -0
- data/Rakefile +1 -0
- data/google_distance_matrix.gemspec +30 -0
- data/lib/google_distance_matrix.rb +38 -0
- data/lib/google_distance_matrix/client.rb +47 -0
- data/lib/google_distance_matrix/configuration.rb +68 -0
- data/lib/google_distance_matrix/errors.rb +88 -0
- data/lib/google_distance_matrix/log_subscriber.rb +14 -0
- data/lib/google_distance_matrix/logger.rb +32 -0
- data/lib/google_distance_matrix/matrix.rb +122 -0
- data/lib/google_distance_matrix/place.rb +101 -0
- data/lib/google_distance_matrix/places.rb +43 -0
- data/lib/google_distance_matrix/railtie.rb +9 -0
- data/lib/google_distance_matrix/route.rb +49 -0
- data/lib/google_distance_matrix/routes_finder.rb +149 -0
- data/lib/google_distance_matrix/url_builder.rb +63 -0
- data/lib/google_distance_matrix/version.rb +3 -0
- data/spec/lib/google_distance_matrix/client_spec.rb +67 -0
- data/spec/lib/google_distance_matrix/configuration_spec.rb +63 -0
- data/spec/lib/google_distance_matrix/logger_spec.rb +38 -0
- data/spec/lib/google_distance_matrix/matrix_spec.rb +169 -0
- data/spec/lib/google_distance_matrix/place_spec.rb +93 -0
- data/spec/lib/google_distance_matrix/places_spec.rb +77 -0
- data/spec/lib/google_distance_matrix/route_spec.rb +28 -0
- data/spec/lib/google_distance_matrix/routes_finder_spec.rb +190 -0
- data/spec/lib/google_distance_matrix/url_builder_spec.rb +105 -0
- data/spec/request_recordings/success +62 -0
- data/spec/request_recordings/zero_results +57 -0
- data/spec/spec_helper.rb +24 -0
- metadata +225 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module GoogleDistanceMatrix
|
2
|
+
class Logger
|
3
|
+
PREFIXES = %w[google_distance_matrix]
|
4
|
+
LEVELS = %w[fatal error warn info debug]
|
5
|
+
|
6
|
+
attr_reader :backend
|
7
|
+
|
8
|
+
def initialize(backend = nil)
|
9
|
+
@backend = backend
|
10
|
+
end
|
11
|
+
|
12
|
+
LEVELS.each do |level|
|
13
|
+
define_method level do |*args|
|
14
|
+
options = args.extract_options!.with_indifferent_access
|
15
|
+
|
16
|
+
msg = args.first
|
17
|
+
tags = PREFIXES.dup.concat Array.wrap(options[:tag])
|
18
|
+
|
19
|
+
backend.public_send level, tag_msg(msg, tags) if backend
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def tag_msg(msg, tags)
|
27
|
+
msg_buffer = tags.map { |tag| "[#{tag}]" }
|
28
|
+
msg_buffer << msg
|
29
|
+
msg_buffer.join " "
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module GoogleDistanceMatrix
|
2
|
+
# Public: Represents a distance matrix.
|
3
|
+
#
|
4
|
+
# Enables you to set up a origins and destinations and get
|
5
|
+
# a distance matrix from Google. For documentation see
|
6
|
+
# https://developers.google.com/maps/documentation/distancematrix
|
7
|
+
#
|
8
|
+
# Examples
|
9
|
+
#
|
10
|
+
# origin_1 = GoogleDistanceMatrix::Place.new address: "Karl Johans gate, Oslo"
|
11
|
+
# origin_2 = GoogleDistanceMatrix::Place.new address: "Askerveien 1, Asker"
|
12
|
+
#
|
13
|
+
# destination_1 = GoogleDistanceMatrix::Place.new address: "Drammensveien 1, Oslo"
|
14
|
+
# destination_2 = GoogleDistanceMatrix::Place.new lat: 1.4, lng: 1.3
|
15
|
+
#
|
16
|
+
# matrix = GoogleDistanceMatrix::Matrix.new(
|
17
|
+
# origins: [origin_1, origin_2],
|
18
|
+
# destinations: [destination_1, destination_2]
|
19
|
+
# )
|
20
|
+
#
|
21
|
+
# You may configure the matrix. See GoogleDistanceMatrix::Configuration for options.
|
22
|
+
#
|
23
|
+
# matrix.configure do |config|
|
24
|
+
# config.sensor = true
|
25
|
+
# config.mode = "walking"
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# You can set default configuration by doing: GoogleDistanceMatrix.configure_defaults { |c| c.sensor = true }
|
29
|
+
#
|
30
|
+
#
|
31
|
+
# Query API and get the matrix back
|
32
|
+
#
|
33
|
+
# matrix.data # Returns a two dimensional array.
|
34
|
+
# # Rows are ordered according to the values in the origins.
|
35
|
+
# # Each row corresponds to an origin, and each element within that row corresponds to
|
36
|
+
# # a pairing of the origin with a destination.
|
37
|
+
#
|
38
|
+
#
|
39
|
+
class Matrix
|
40
|
+
include ActiveModel::Validations
|
41
|
+
|
42
|
+
validates :origins, length: {minimum: 1, too_short: "must have at least one origin"}
|
43
|
+
validates :destinations, length: {minimum: 1, too_short: "must have at least one destination"}
|
44
|
+
validate { errors.add(:configuration, "is invalid") if configuration.invalid? }
|
45
|
+
|
46
|
+
attr_reader :origins, :destinations, :configuration
|
47
|
+
|
48
|
+
def initialize(attributes = {})
|
49
|
+
attributes = attributes.with_indifferent_access
|
50
|
+
|
51
|
+
@origins = Places.new attributes[:origins]
|
52
|
+
@destinations = Places.new attributes[:destinations]
|
53
|
+
@configuration = attributes[:configuration] || GoogleDistanceMatrix.default_configuration.dup
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
delegate :route_for, :routes_for, to: :routes_finder
|
58
|
+
delegate :route_for!, :routes_for!, to: :routes_finder
|
59
|
+
delegate :shortest_route_by_distance_to, :shortest_route_by_duration_to, to: :routes_finder
|
60
|
+
delegate :shortest_route_by_distance_to!, :shortest_route_by_duration_to!, to: :routes_finder
|
61
|
+
|
62
|
+
|
63
|
+
# Public: The data for this matrix.
|
64
|
+
#
|
65
|
+
# Returns a two dimensional array, the matrix's data
|
66
|
+
def data
|
67
|
+
@data ||= load_matrix
|
68
|
+
end
|
69
|
+
|
70
|
+
def reload
|
71
|
+
@data = load_matrix
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
def loaded?
|
76
|
+
@data.present?
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
|
81
|
+
def configure
|
82
|
+
yield configuration
|
83
|
+
end
|
84
|
+
|
85
|
+
def url
|
86
|
+
UrlBuilder.new(self).url
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def inspect
|
91
|
+
attributes = %w[origins destinations]
|
92
|
+
attributes << "data" if loaded?
|
93
|
+
inspection = attributes.map { |a| "#{a}: #{public_send(a).inspect}" }.join ', '
|
94
|
+
|
95
|
+
"#<#{self.class} #{inspection}>"
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def routes_finder
|
102
|
+
@routes_finder ||= RoutesFinder.new self
|
103
|
+
end
|
104
|
+
|
105
|
+
def load_matrix
|
106
|
+
parsed = JSON.parse client.get(url).body
|
107
|
+
|
108
|
+
parsed["rows"].each_with_index.map do |row, origin_index|
|
109
|
+
origin = origins[origin_index]
|
110
|
+
|
111
|
+
row["elements"].each_with_index.map do |element, destination_index|
|
112
|
+
route_attributes = element.merge(origin: origin, destination: destinations[destination_index])
|
113
|
+
Route.new route_attributes
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def client
|
119
|
+
@client ||= Client.new
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# Public: Represents a place and knows how to convert it to param.
|
2
|
+
#
|
3
|
+
# Examples
|
4
|
+
#
|
5
|
+
# GoogleDistanceMatrix::Place.new address: "My address"
|
6
|
+
# GoogleDistanceMatrix::Place.new lat: 1, lng: 3
|
7
|
+
#
|
8
|
+
# You may also build places by other objects responding to lat and lng or address.
|
9
|
+
# If your object responds to all of the attributes we'll use lat and lng as data
|
10
|
+
# for the Place.
|
11
|
+
#
|
12
|
+
# GoogleDistanceMatrix::Place.new object
|
13
|
+
module GoogleDistanceMatrix
|
14
|
+
class Place
|
15
|
+
ATTRIBUTES = %w[address lat lng]
|
16
|
+
|
17
|
+
attr_reader *ATTRIBUTES, :extracted_attributes_from
|
18
|
+
|
19
|
+
def initialize(attributes_or_object)
|
20
|
+
if respond_to_needed_attributes? attributes_or_object
|
21
|
+
extract_and_assign_attributes_from_object attributes_or_object
|
22
|
+
elsif attributes_or_object.is_a? Hash
|
23
|
+
assign_attributes attributes_or_object
|
24
|
+
else
|
25
|
+
fail ArgumentError, "Must be either hash or object responding to lat, lng or address. "
|
26
|
+
end
|
27
|
+
|
28
|
+
validate_attributes
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_param(options = {})
|
32
|
+
options = options.with_indifferent_access
|
33
|
+
address.present? ? address : lat_lng(options[:lat_lng_scale]).join(',')
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def eql?(other)
|
38
|
+
if address.present?
|
39
|
+
address == other.address
|
40
|
+
else
|
41
|
+
lat_lng == other.lat_lng
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def lat_lng(scale = nil)
|
46
|
+
[lat, lng].map do |v|
|
47
|
+
if scale
|
48
|
+
v = v.to_f.round scale
|
49
|
+
v == v.to_i ? v.to_i : v
|
50
|
+
else
|
51
|
+
v
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def inspect
|
57
|
+
inspection = (ATTRIBUTES | [:extracted_attributes_from]).reject { |a| public_send(a).blank? }.map { |a| "#{a}: #{public_send(a).inspect}" }.join ', '
|
58
|
+
|
59
|
+
"#<#{self.class} #{inspection}>"
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def respond_to_needed_attributes?(object)
|
65
|
+
(object.respond_to?(:lat) && object.respond_to?(:lng)) || object.respond_to?(:address)
|
66
|
+
end
|
67
|
+
|
68
|
+
def extract_and_assign_attributes_from_object(object)
|
69
|
+
attrs = Hash[ATTRIBUTES.map do |attr_name|
|
70
|
+
if object.respond_to? attr_name
|
71
|
+
[attr_name, object.public_send(attr_name)]
|
72
|
+
end
|
73
|
+
end.compact]
|
74
|
+
|
75
|
+
if attrs.has_key?('lat') || attrs.has_key?('lng')
|
76
|
+
attrs.delete 'address'
|
77
|
+
end
|
78
|
+
|
79
|
+
@extracted_attributes_from = object
|
80
|
+
assign_attributes attrs
|
81
|
+
end
|
82
|
+
|
83
|
+
def assign_attributes(attributes)
|
84
|
+
attributes = attributes.with_indifferent_access
|
85
|
+
|
86
|
+
@address = attributes[:address]
|
87
|
+
@lat = attributes[:lat]
|
88
|
+
@lng = attributes[:lng]
|
89
|
+
end
|
90
|
+
|
91
|
+
def validate_attributes
|
92
|
+
unless address.present? || (lat.present? && lng.present?)
|
93
|
+
fail ArgumentError, "Must provide an address, or lat and lng."
|
94
|
+
end
|
95
|
+
|
96
|
+
if address.present? && lat.present? && lng.present?
|
97
|
+
fail ArgumentError, "Cannot provide address, lat and lng."
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module GoogleDistanceMatrix
|
2
|
+
class Places
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize(places = [])
|
6
|
+
@places = []
|
7
|
+
concat Array.wrap(places)
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
delegate :each, :[], :length, :index, :pop, :shift, :delete_at, :compact, :inspect, to: :places
|
12
|
+
|
13
|
+
[:<<, :push, :unshift].each do |method|
|
14
|
+
define_method method do |*args|
|
15
|
+
args = ensure_args_are_places args
|
16
|
+
|
17
|
+
places.public_send(method, *args)
|
18
|
+
|
19
|
+
places.uniq!
|
20
|
+
self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def concat(other)
|
25
|
+
push *other
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :places
|
32
|
+
|
33
|
+
def ensure_args_are_places(args)
|
34
|
+
args.map do |arg|
|
35
|
+
if arg.is_a? Place
|
36
|
+
arg
|
37
|
+
else
|
38
|
+
Place.new arg
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module GoogleDistanceMatrix
|
2
|
+
# Public: Thin wrapper class for an element in the matrix.
|
3
|
+
#
|
4
|
+
# The route has the data the element contains, pluss it references
|
5
|
+
# it's origin and destination.
|
6
|
+
#
|
7
|
+
class Route
|
8
|
+
STATUSES = %w[ok zero_results not_found]
|
9
|
+
|
10
|
+
ATTRIBUTES = %w[
|
11
|
+
origin destination
|
12
|
+
status distance_text distance_in_meters duration_text duration_in_seconds
|
13
|
+
]
|
14
|
+
|
15
|
+
attr_reader *ATTRIBUTES
|
16
|
+
|
17
|
+
delegate *(STATUSES.map { |s| s + '?' }), to: :status, allow_nil: true
|
18
|
+
|
19
|
+
|
20
|
+
def initialize(attributes = {})
|
21
|
+
attributes = attributes.with_indifferent_access
|
22
|
+
|
23
|
+
@origin = attributes[:origin]
|
24
|
+
@destination = attributes[:destination]
|
25
|
+
|
26
|
+
@status = ActiveSupport::StringInquirer.new attributes[:status].downcase
|
27
|
+
|
28
|
+
if ok?
|
29
|
+
@distance_text = attributes[:distance][:text]
|
30
|
+
@distance_in_meters = attributes[:distance][:value]
|
31
|
+
@duration_text = attributes[:duration][:text]
|
32
|
+
@duration_in_seconds = attributes[:duration][:value]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
{distance_value: :distance_in_meters, duration_value: :duration_in_seconds}.each_pair do |old_attr, new_attr|
|
37
|
+
define_method old_attr do
|
38
|
+
ActiveSupport::Deprecation.warn "#{old_attr} is being replaced by #{new_attr}. Please use #{new_attr}."
|
39
|
+
public_send new_attr
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def inspect
|
44
|
+
inspection = ATTRIBUTES.reject { |a| public_send(a).blank? }.map { |a| "#{a}: #{public_send(a).inspect}" }.join ', '
|
45
|
+
|
46
|
+
"#<#{self.class} #{inspection}>"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module GoogleDistanceMatrix
|
2
|
+
# Public: Has logic for doing finder operations on a matrix.
|
3
|
+
class RoutesFinder
|
4
|
+
|
5
|
+
attr_reader :matrix
|
6
|
+
delegate :data, :origins, :destinations, to: :matrix
|
7
|
+
|
8
|
+
|
9
|
+
def initialize(matrix)
|
10
|
+
@matrix = matrix
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
# Public: Finds routes for given place.
|
16
|
+
#
|
17
|
+
# place - Either an origin or destination, or an object which you built the place from
|
18
|
+
#
|
19
|
+
# Returns the place's routes
|
20
|
+
def routes_for(place_or_object_place_was_built_from)
|
21
|
+
place = ensure_place place_or_object_place_was_built_from
|
22
|
+
|
23
|
+
if origins.include? place
|
24
|
+
routes_for_origin place
|
25
|
+
elsif destinations.include? place
|
26
|
+
routes_for_destination place
|
27
|
+
else
|
28
|
+
fail ArgumentError, "Given place not an origin nor destination."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Public: Finds routes for given place.
|
33
|
+
#
|
34
|
+
# Behaviour is same as without a bang, except it fails unless all routes are ok.
|
35
|
+
#
|
36
|
+
def routes_for!(place_or_object_place_was_built_from)
|
37
|
+
routes_for(place_or_object_place_was_built_from).tap do |routes|
|
38
|
+
routes.each do |route|
|
39
|
+
fail_unless_route_is_ok route
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Public: Finds a route for you based on one origin and destination
|
46
|
+
#
|
47
|
+
# origin - A place representing the origin, or an object which you built the origin from
|
48
|
+
# destination - A place representing the destination, or an object which you built the destination from
|
49
|
+
#
|
50
|
+
# A Route for given origin and destination
|
51
|
+
def route_for(options = {})
|
52
|
+
options = options.with_indifferent_access
|
53
|
+
|
54
|
+
origin = ensure_place options[:origin]
|
55
|
+
destination = ensure_place options[:destination]
|
56
|
+
|
57
|
+
if origin.nil? || destination.nil?
|
58
|
+
fail ArgumentError, "Must provide origin and destination"
|
59
|
+
end
|
60
|
+
|
61
|
+
routes_for(origin).detect { |route| route.destination == destination }
|
62
|
+
end
|
63
|
+
|
64
|
+
# Public: Finds a route for you based on one origin and destination
|
65
|
+
#
|
66
|
+
# Behaviour is same as without a bang, except it fails unless route are ok.
|
67
|
+
#
|
68
|
+
def route_for!(options = {})
|
69
|
+
route_for(options).tap do |route|
|
70
|
+
fail_unless_route_is_ok route
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def shortest_route_by_distance_to(place_or_object_place_was_built_from)
|
76
|
+
routes = routes_for place_or_object_place_was_built_from
|
77
|
+
select_ok_routes(routes).min_by &:distance_in_meters
|
78
|
+
end
|
79
|
+
|
80
|
+
def shortest_route_by_distance_to!(place_or_object_place_was_built_from)
|
81
|
+
routes_for!(place_or_object_place_was_built_from).min_by &:distance_in_meters
|
82
|
+
end
|
83
|
+
|
84
|
+
def shortest_route_by_duration_to(place_or_object_place_was_built_from)
|
85
|
+
routes = routes_for place_or_object_place_was_built_from
|
86
|
+
select_ok_routes(routes).min_by &:duration_in_seconds
|
87
|
+
end
|
88
|
+
|
89
|
+
def shortest_route_by_duration_to!(place_or_object_place_was_built_from)
|
90
|
+
routes_for!(place_or_object_place_was_built_from).min_by &:duration_in_seconds
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def ensure_place(object)
|
99
|
+
if object.is_a? Place
|
100
|
+
object
|
101
|
+
else
|
102
|
+
find_place_for_object(origins, object) ||
|
103
|
+
find_place_for_object(destinations, object)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def find_place_for_object(collection, object)
|
108
|
+
collection.detect do |place|
|
109
|
+
place.extracted_attributes_from.present? &&
|
110
|
+
place.extracted_attributes_from == object
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def routes_for_origin(origin)
|
115
|
+
index = origins.index origin
|
116
|
+
fail ArgumentError, "Given origin is not i matrix."if index.nil?
|
117
|
+
|
118
|
+
data[index]
|
119
|
+
end
|
120
|
+
|
121
|
+
def routes_for_destination(destination)
|
122
|
+
index = destinations.index destination
|
123
|
+
fail ArgumentError, "Given destination is not i matrix." if index.nil?
|
124
|
+
|
125
|
+
[].tap do |routes|
|
126
|
+
data.each { |row| routes << row[index] }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def find_route_for_origin_and_destination(origin, destination)
|
131
|
+
origin_index = origins.index origin
|
132
|
+
destination_index = destinations.index destination
|
133
|
+
|
134
|
+
if origin_index.nil? || destination_index.nil?
|
135
|
+
fail ArgumentError, "Given origin or destination is not i matrix."
|
136
|
+
end
|
137
|
+
|
138
|
+
[data[origin_index][destination_index]]
|
139
|
+
end
|
140
|
+
|
141
|
+
def fail_unless_route_is_ok(route)
|
142
|
+
fail InvalidRoute.new route unless route.ok?
|
143
|
+
end
|
144
|
+
|
145
|
+
def select_ok_routes(routes)
|
146
|
+
routes.select &:ok?
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|