google_distance_matrix 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/.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
|