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