google_distance_matrix 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/.gitignore +17 -0
  2. data/.rbenv-version +1 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +106 -0
  6. data/Rakefile +1 -0
  7. data/google_distance_matrix.gemspec +30 -0
  8. data/lib/google_distance_matrix.rb +38 -0
  9. data/lib/google_distance_matrix/client.rb +47 -0
  10. data/lib/google_distance_matrix/configuration.rb +68 -0
  11. data/lib/google_distance_matrix/errors.rb +88 -0
  12. data/lib/google_distance_matrix/log_subscriber.rb +14 -0
  13. data/lib/google_distance_matrix/logger.rb +32 -0
  14. data/lib/google_distance_matrix/matrix.rb +122 -0
  15. data/lib/google_distance_matrix/place.rb +101 -0
  16. data/lib/google_distance_matrix/places.rb +43 -0
  17. data/lib/google_distance_matrix/railtie.rb +9 -0
  18. data/lib/google_distance_matrix/route.rb +49 -0
  19. data/lib/google_distance_matrix/routes_finder.rb +149 -0
  20. data/lib/google_distance_matrix/url_builder.rb +63 -0
  21. data/lib/google_distance_matrix/version.rb +3 -0
  22. data/spec/lib/google_distance_matrix/client_spec.rb +67 -0
  23. data/spec/lib/google_distance_matrix/configuration_spec.rb +63 -0
  24. data/spec/lib/google_distance_matrix/logger_spec.rb +38 -0
  25. data/spec/lib/google_distance_matrix/matrix_spec.rb +169 -0
  26. data/spec/lib/google_distance_matrix/place_spec.rb +93 -0
  27. data/spec/lib/google_distance_matrix/places_spec.rb +77 -0
  28. data/spec/lib/google_distance_matrix/route_spec.rb +28 -0
  29. data/spec/lib/google_distance_matrix/routes_finder_spec.rb +190 -0
  30. data/spec/lib/google_distance_matrix/url_builder_spec.rb +105 -0
  31. data/spec/request_recordings/success +62 -0
  32. data/spec/request_recordings/zero_results +57 -0
  33. data/spec/spec_helper.rb +24 -0
  34. 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,9 @@
1
+ module GoogleDistanceMatrix
2
+ class Railtie < Rails::Railtie
3
+ initializer "google_distance_matrix.logger_setup" do
4
+ GoogleDistanceMatrix.configure_defaults do |config|
5
+ config.logger = Rails.logger
6
+ end
7
+ end
8
+ end
9
+ 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