google_maps_service 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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