google_maps_service 0.1.0

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.
@@ -0,0 +1,21 @@
1
+ module GoogleMapsService
2
+ class << self
3
+ attr_accessor :key, :client_id, :client_secret, :ssl, :connection_middleware
4
+
5
+ def configure
6
+ yield self
7
+ true
8
+ end
9
+ end
10
+
11
+ require 'google_maps_service/version'
12
+ require 'google_maps_service/errors'
13
+ require 'google_maps_service/convert'
14
+ require 'google_maps_service/directions'
15
+ require 'google_maps_service/distance_matrix'
16
+ require 'google_maps_service/elevation'
17
+ require 'google_maps_service/geocoding'
18
+ require 'google_maps_service/roads'
19
+ require 'google_maps_service/time_zone'
20
+ require 'google_maps_service/client'
21
+ end
@@ -0,0 +1,226 @@
1
+ require 'uri'
2
+ require 'hurley'
3
+ require 'multi_json'
4
+
5
+ module GoogleMapsService
6
+ class Client
7
+ USER_AGENT = "GoogleGeoApiClientRuby/#{GoogleMapsService::VERSION}"
8
+ DEFAULT_BASE_URL = "https://maps.googleapis.com"
9
+ RETRIABLE_STATUSES = [500, 503, 504]
10
+
11
+ include GoogleMapsService::Directions
12
+ include GoogleMapsService::DistanceMatrix
13
+ include GoogleMapsService::Elevation
14
+ include GoogleMapsService::Geocoding
15
+ include GoogleMapsService::Roads
16
+ include GoogleMapsService::TimeZone
17
+
18
+ # Secret key for accessing Google Maps Web Service.
19
+ # Can be obtained at https://developers.google.com/maps/documentation/geocoding/get-api-key#key
20
+ # @return [String]
21
+ attr_reader :key
22
+
23
+ # Client id for using Maps API for Work services.
24
+ # @return [String]
25
+ attr_reader :client_id
26
+
27
+ # Client secret for using Maps API for Work services.
28
+ # @return [String]
29
+ attr_reader :client_secret
30
+
31
+ def initialize(options={})
32
+ @key = options[:key] || GoogleMapsService.key
33
+ @client_id = options[:client_id] || GoogleMapsService.client_id
34
+ @client_secret = options[:client_secret] || GoogleMapsService.client_secret
35
+ end
36
+
37
+ # Get the current HTTP client
38
+ # @return [Hurley::Client]
39
+ def client
40
+ @client ||= new_client
41
+ end
42
+
43
+ protected
44
+
45
+ # Create a new HTTP client
46
+ # @return [Hurley::Client]
47
+ def new_client
48
+ client = Hurley::Client.new
49
+ client.request_options.query_class = Hurley::Query::Flat
50
+ client.header[:user_agent] = USER_AGENT
51
+ client
52
+ end
53
+
54
+ def get(path, params, base_url: DEFAULT_BASE_URL, accepts_client_id: true, custom_response_decoder: nil)
55
+ url = base_url + generate_auth_url(path, params, accepts_client_id)
56
+ response = client.get url
57
+
58
+ if custom_response_decoder
59
+ return custom_response_decoder.call(response)
60
+ end
61
+ return decode_response_body(response)
62
+ end
63
+
64
+ # Extract and parse body response as hash. Throw an error if there is something wrong with the response.
65
+ #
66
+ # @param [Hurley::Response] response Web API response.
67
+ #
68
+ # @return [Hash] Response body as hash. The hash key will be symbolized.
69
+ #
70
+ # @raise [GoogleMapsService::Error::RedirectError] The response redirects to another URL.
71
+ # @raise [GoogleMapsService::Error::RequestDeniedError] The credential (key or client id pair) is not valid.
72
+ # @raise [GoogleMapsService::Error::ClientError] The request is invalid and should not be retried without modification.
73
+ # @raise [GoogleMapsService::Error::ServerError] An error occurred on the server and the request can be retried.
74
+ # @raise [GoogleMapsService::Error::TransmissionError] Unknown response status code.
75
+ # @raise [GoogleMapsService::Error::RateLimitError] The quota for the credential is already pass the limit.
76
+ # @raise [GoogleMapsService::Error::ApiError] The Web API error.
77
+ def decode_response_body(response)
78
+ check_response_status_code(response)
79
+
80
+ body = MultiJson.load(response.body, :symbolize_keys => true)
81
+
82
+ api_status = body[:status]
83
+ if api_status == "OK" or api_status == "ZERO_RESULTS"
84
+ return body
85
+ end
86
+
87
+ if api_status == "OVER_QUERY_LIMIT"
88
+ raise GoogleMapsService::Error::RateLimitError.new(response), body[:error_message]
89
+ end
90
+
91
+ if api_status == "REQUEST_DENIED"
92
+ raise GoogleMapsService::Error::RequestDeniedError.new(response), body[:error_message]
93
+ end
94
+
95
+ if api_status == "INVALID_REQUEST"
96
+ raise GoogleMapsService::Error::InvalidRequestError.new(response), body[:error_message]
97
+ end
98
+
99
+ if body[:error_message]
100
+ raise GoogleMapsService::Error::ApiError.new(response), body[:error_message]
101
+ else
102
+ raise GoogleMapsService::Error::ApiError.new(response)
103
+ end
104
+ end
105
+
106
+ def check_response_status_code(response)
107
+ case response.status_code
108
+ when 200..300
109
+ # Do-nothing
110
+ when 301, 302, 303, 307
111
+ message ||= sprintf('Redirect to %s', response.header[:location])
112
+ raise GoogleMapsService::Error::RedirectError.new(response), message
113
+ when 401
114
+ message ||= 'Unauthorized'
115
+ raise GoogleMapsService::Error::ClientError.new(response)
116
+ when 304, 400, 402...500
117
+ message ||= 'Invalid request'
118
+ raise GoogleMapsService::Error::ClientError.new(response)
119
+ when 500..600
120
+ message ||= 'Server error'
121
+ raise GoogleMapsService::Error::ServerError.new(response)
122
+ else
123
+ message ||= 'Unknown error'
124
+ raise GoogleMapsService::Error::TransmissionError.new(response)
125
+ end
126
+ end
127
+
128
+ # Returns the path and query string portion of the request URL,
129
+ # first adding any necessary parameters.
130
+ #
131
+ # @param [String] path The path portion of the URL.
132
+ # @param [Hash] params URL parameters.
133
+ #
134
+ # @return [String]
135
+ def generate_auth_url(path, params, accepts_client_id)
136
+ # Deterministic ordering through sorting by key.
137
+ # Useful for tests, and in the future, any caching.
138
+ if params.kind_of?(Hash)
139
+ params = params.sort
140
+ else
141
+ params = params.dup
142
+ end
143
+
144
+ if accepts_client_id and @client_id and @client_secret
145
+ params << ["client", @client_id]
146
+
147
+ path = [path, self.class.urlencode_params(params)].join("?")
148
+ sig = self.class.sign_hmac(@client_secret, path)
149
+ return path + "&signature=" + sig
150
+ end
151
+
152
+ if @key
153
+ params << ["key", @key]
154
+ return path + "?" + self.class.urlencode_params(params)
155
+ end
156
+
157
+ raise ArgumentError, "Must provide API key for this API. It does not accept enterprise credentials."
158
+ end
159
+
160
+ # Returns a base64-encoded HMAC-SHA1 signature of a given string.
161
+ #
162
+ # @param [String] secret The key used for the signature, base64 encoded.
163
+ # @param [String] payload The payload to sign.
164
+ #
165
+ # @return [String]
166
+ def self.sign_hmac(secret, payload)
167
+ require 'base64'
168
+ require 'hmac'
169
+ require 'hmac-sha1'
170
+
171
+ secret = secret.encode('ASCII')
172
+ payload = payload.encode('ASCII')
173
+
174
+ # Decode the private key
175
+ raw_key = Base64.urlsafe_decode64(secret)
176
+
177
+ # Create a signature using the private key and the URL
178
+ sha1 = HMAC::SHA1.new(raw_key)
179
+ sha1 << payload
180
+ raw_signature = sha1.digest()
181
+
182
+ # Encode the signature into base64 for url use form.
183
+ signature = Base64.urlsafe_encode64(raw_signature)
184
+ return signature
185
+ end
186
+
187
+ # URL encodes the parameters.
188
+ # @param [Hash, Array<Array>] params The parameters
189
+ # @return [String]
190
+ def self.urlencode_params(params)
191
+ unquote_unreserved(URI.encode_www_form(params))
192
+ end
193
+
194
+ # The unreserved URI characters (RFC 3986)
195
+ UNRESERVED_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
196
+
197
+ # Un-escape any percent-escape sequences in a URI that are unreserved
198
+ # characters. This leaves all reserved, illegal and non-ASCII bytes encoded.
199
+ #
200
+ # @param [String] uri
201
+ #
202
+ # @return [String]
203
+ def self.unquote_unreserved(uri)
204
+ parts = uri.split('%')
205
+
206
+ (1..parts.length-1).each do |i|
207
+ h = parts[i][0..1]
208
+
209
+ if h.length == 2 and !h.match(/[^A-Za-z0-9]/)
210
+ c = h.to_i(16).chr
211
+
212
+ if UNRESERVED_SET.include?(c)
213
+ parts[i] = c + parts[i][2..-1]
214
+ else
215
+ parts[i] = "%#{parts[i]}"
216
+ end
217
+ else
218
+ parts[i] = "%#{parts[i]}"
219
+ end
220
+ end
221
+
222
+ return parts.join
223
+ end
224
+
225
+ end
226
+ end
@@ -0,0 +1,227 @@
1
+ module GoogleMapsService
2
+
3
+ # Converts Ruby types to string representations suitable for Maps API server.
4
+ module Convert
5
+ module_function
6
+
7
+ # Converts a lat/lon pair to a comma-separated string.
8
+ #
9
+ # @example
10
+ # >> GoogleMapsService::Convert.latlng({"lat": -33.8674869, "lng": 151.2069902})
11
+ # => "-33.867487,151.206990"
12
+ #
13
+ # @param [Hash, Array] arg The lat/lon hash or array pair.
14
+ #
15
+ # @return [String] Comma-separated lat/lng.
16
+ #
17
+ # @raise [ArgumentError] When argument is not lat/lng hash or array.
18
+ def latlng(arg)
19
+ return "%f,%f" % normalize_latlng(arg)
20
+ end
21
+
22
+ # Take the various lat/lng representations and return a tuple.
23
+ #
24
+ # Accepts various representations:
25
+ #
26
+ # 1. Hash with two entries - +lat+ and +lng+
27
+ # 2. Array or list - e.g. +[-33, 151]+
28
+ #
29
+ # @param [Hash, Array] arg The lat/lon hash or array pair.
30
+ #
31
+ # @return [Array] Pair of lat and lng array.
32
+ def normalize_latlng(arg)
33
+ if arg.kind_of?(Hash)
34
+ if arg.has_key?(:lat) and arg.has_key?(:lng)
35
+ return arg[:lat], arg[:lng]
36
+ end
37
+ if arg.has_key?(:latitude) and arg.has_key?(:longitude)
38
+ return arg[:latitude], arg[:longitude]
39
+ end
40
+ if arg.has_key?("lat") and arg.has_key?("lng")
41
+ return arg["lat"], arg["lng"]
42
+ end
43
+ if arg.has_key?("latitude") and arg.has_key?("longitude")
44
+ return arg["latitude"], arg["longitude"]
45
+ end
46
+ elsif arg.kind_of?(Array)
47
+ return arg[0], arg[1]
48
+ end
49
+
50
+ raise ArgumentError, "Expected a lat/lng Hash or Array, but got #{arg.class}"
51
+ end
52
+
53
+ # If arg is list-like, then joins it with sep.
54
+ #
55
+ # @param [String] sep Separator string.
56
+ # @param [Array, String] arg Value to coerce into a list.
57
+ #
58
+ # @return [String]
59
+ def join_list(sep, arg)
60
+ return as_list(arg).join(sep)
61
+ end
62
+
63
+ # Coerces arg into a list. If arg is already list-like, returns arg.
64
+ # Otherwise, returns a one-element list containing arg.
65
+ #
66
+ # @param [Object] arg
67
+ #
68
+ # @return [Array]
69
+ def as_list(arg)
70
+ if arg.kind_of?(Array)
71
+ return arg
72
+ end
73
+ return [arg]
74
+ end
75
+
76
+
77
+ # Converts the value into a unix time (seconds since unix epoch).
78
+ #
79
+ # @example
80
+ # >> GoogleMapsService::Convert.time(datetime.now())
81
+ # => "1409810596"
82
+ #
83
+ # @param [Time, Date, DateTime, Integer] arg The time.
84
+ #
85
+ # @return [String] String representation of epoch time
86
+ def time(arg)
87
+ if arg.kind_of?(DateTime)
88
+ arg = arg.to_time
89
+ end
90
+ return arg.to_i.to_s
91
+ end
92
+
93
+ # Converts a dict of components to the format expected by the Google Maps
94
+ # server.
95
+ #
96
+ # @example
97
+ # >> GoogleMapsService::Convert.components({"country": "US", "postal_code": "94043"})
98
+ # => "country:US|postal_code:94043"
99
+ #
100
+ # @param [Hash] arg The component filter.
101
+ #
102
+ # @return [String]
103
+ def components(arg)
104
+ if arg.kind_of?(Hash)
105
+ arg = arg.sort.map { |k, v| "#{k}:#{v}" }
106
+ return arg.join("|")
107
+ end
108
+
109
+ raise ArgumentError, "Expected a Hash for components, but got #{arg.class}"
110
+ end
111
+
112
+ # Converts a lat/lon bounds to a comma- and pipe-separated string.
113
+ #
114
+ # Accepts two representations:
115
+ #
116
+ # 1. String: pipe-separated pair of comma-separated lat/lon pairs.
117
+ # 2. Hash with two entries - "southwest" and "northeast". See {.latlng}
118
+ # for information on how these can be represented.
119
+ #
120
+ # For example:
121
+ #
122
+ # >> sydney_bounds = {
123
+ # ?> "northeast": {
124
+ # ?> "lat": -33.4245981,
125
+ # ?> "lng": 151.3426361
126
+ # ?> },
127
+ # ?> "southwest": {
128
+ # ?> "lat": -34.1692489,
129
+ # ?> "lng": 150.502229
130
+ # ?> }
131
+ # ?> }
132
+ # >> GoogleMapsService::Convert.bounds(sydney_bounds)
133
+ # => '-34.169249,150.502229|-33.424598,151.342636'
134
+ #
135
+ # @param [Hash] arg The bounds.
136
+ #
137
+ # @return [String]
138
+ def bounds(arg)
139
+ if arg.kind_of?(Hash)
140
+ if arg.has_key?("southwest") && arg.has_key?("northeast")
141
+ return "#{latlng(arg["southwest"])}|#{latlng(arg["northeast"])}"
142
+ elsif arg.has_key?(:southwest) && arg.has_key?(:northeast)
143
+ return "#{latlng(arg[:southwest])}|#{latlng(arg[:northeast])}"
144
+ end
145
+ end
146
+
147
+ raise ArgumentError, "Expected a bounds (southwest/northeast) Hash, but got #{arg.class}"
148
+ end
149
+
150
+ # Decodes a Polyline string into a list of lat/lng hash.
151
+ #
152
+ # See the developer docs for a detailed description of this encoding:
153
+ # https://developers.google.com/maps/documentation/utilities/polylinealgorithm
154
+ #
155
+ # @param [String] polyline An encoded polyline
156
+ #
157
+ # @return [Array] Array of hash with lat/lng keys
158
+ def decode_polyline(polyline)
159
+ points = []
160
+ index = lat = lng = 0
161
+
162
+ while index < polyline.length
163
+ result = 1
164
+ shift = 0
165
+ while true
166
+ b = polyline[index].ord - 63 - 1
167
+ index += 1
168
+ result += b << shift
169
+ shift += 5
170
+ break if b < 0x1f
171
+ end
172
+ lat += (result & 1) != 0 ? (~result >> 1) : (result >> 1)
173
+
174
+ result = 1
175
+ shift = 0
176
+ while true
177
+ b = polyline[index].ord - 63 - 1
178
+ index += 1
179
+ result += b << shift
180
+ shift += 5
181
+ break if b < 0x1f
182
+ end
183
+ lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1)
184
+
185
+ points << {lat: lat * 1e-5, lng: lng * 1e-5}
186
+ end
187
+
188
+ points
189
+ end
190
+
191
+ # Encodes a list of points into a polyline string.
192
+ #
193
+ # See the developer docs for a detailed description of this encoding:
194
+ # https://developers.google.com/maps/documentation/utilities/polylinealgorithm
195
+ #
196
+ # @param [Array<Hash>, Array<Array>] points A list of lat/lng pairs.
197
+ #
198
+ # @return [String]
199
+ def encode_polyline(points)
200
+ last_lat = last_lng = 0
201
+ result = ""
202
+
203
+ points.each do |point|
204
+ ll = normalize_latlng(point)
205
+ lat = (ll[0] * 1e5).round.to_i
206
+ lng = (ll[1] * 1e5).round.to_i
207
+ d_lat = lat - last_lat
208
+ d_lng = lng - last_lng
209
+
210
+ [d_lat, d_lng].each do |v|
211
+ v = (v < 0) ? ~(v << 1) : (v << 1)
212
+ while v >= 0x20
213
+ result += ((0x20 | (v & 0x1f)) + 63).chr
214
+ v >>= 5
215
+ end
216
+ result += (v + 63).chr
217
+ end
218
+
219
+ last_lat = lat
220
+ last_lng = lng
221
+ end
222
+
223
+ result
224
+ end
225
+
226
+ end
227
+ end