googlemaps-services 1.0.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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c10843ca035ec6fe71bd6edc2a338d2c0eee8c22
4
+ data.tar.gz: 092b6d0d9398c95482a74ad88fd4192af0061517
5
+ SHA512:
6
+ metadata.gz: c08995e5da233ce70dc80694b8b5d479cb5a9ce05a253aef8df59bfa696a8e6f5861f1fa9495b393cf21163dd7eb84fc8aa056fe3691c2f552571db4cdff23ac
7
+ data.tar.gz: 046dca085ea8c8d7000e7bab4adbadbbd680758feb3d752e919e823a4ac5bfc1144c312527db141a7a501c05277b35db294c7acff4d87e8e7a503ee2eec5fc6d
@@ -0,0 +1 @@
1
+ require "googlemaps/services/version"
@@ -0,0 +1,254 @@
1
+ require "googlemaps/services/exceptions"
2
+ require "googlemaps/services/version"
3
+ require "googlemaps/services/util"
4
+ require "net/http"
5
+ require "json"
6
+
7
+ # Core functionality, common across all API requests.
8
+ #
9
+ # @since 1.0.0
10
+ module GoogleMaps
11
+ # Core services that connect to Google Maps API web services.
12
+ #
13
+ # @since 1.0.0
14
+ module Services
15
+ $USER_AGENT = "GoogleMapsRubyClient/" + VERSION
16
+ $DEFAULT_BASE_URL = "https://maps.googleapis.com"
17
+ $RETRIABLE_STATUSES = [500, 503, 504]
18
+
19
+ # Performs requests to the Google Maps API web services.
20
+ class GoogleClient
21
+ include GoogleMaps::Services::Exceptions
22
+
23
+ # @return [Symbol] API key. Required, unless "client_id" and "client_secret" are set.
24
+ attr_accessor :key
25
+ # @return [Symbol] timeout Combined connect and read timeout for HTTP requests, in seconds.
26
+ attr_accessor :timeout
27
+ # @return [Symbol] Client ID (for Maps API for Work).
28
+ attr_accessor :client_id
29
+ # @return [Symbol] base64-encoded client secret (for Maps API for Work).
30
+ attr_accessor :client_secret
31
+ # @return [Symbol] attribute used for tracking purposes. Can only be used with a Client ID.
32
+ attr_accessor :channel
33
+ # @return [Symbol] timeout across multiple retriable requests, in seconds.
34
+ attr_accessor :retry_timeout
35
+ # @return [Symbol] extra options for Net::HTTP client.
36
+ attr_accessor :request_opts
37
+ # @return [Symbol] number of queries per second permitted. If the rate limit is reached, the client will sleep for the appropriate amout of time before it runs the current query.
38
+ attr_accessor :queries_per_second
39
+ # @return [Symbol] keeps track of sent queries.
40
+ attr_accessor :sent_times
41
+
42
+ def initialize(key:, client_id: nil, client_secret: nil, timeout: nil,
43
+ connect_timeout: nil, read_timeout: nil,retry_timeout: 60, request_opts: nil,
44
+ queries_per_second: 10, channel: nil)
45
+ if !key && !(client_secret && client_id)
46
+ raise StandardError, "Must provide API key or enterprise credentials when creationg client."
47
+ end
48
+
49
+ if key && !key.start_with?("AIza")
50
+ raise StandardError, "Invalid API key provided."
51
+ end
52
+
53
+ if channel
54
+ if !client_id
55
+ raise StandardError, "The channel argument must be used with a client ID."
56
+ end
57
+
58
+ if !/^[a-zA-Z0-9._-]*$/.match(channel)
59
+ raise StandardError, "The channel argument must be an ASCII alphanumeric string. The period (.), underscore (_) and hyphen (-) characters are allowed."
60
+ end
61
+ end
62
+
63
+ self.key = key
64
+
65
+ if timeout && (connect_timeout || read_timeout)
66
+ raise StandardError, "Specify either timeout, or connect_timeout and read_timeout."
67
+ end
68
+
69
+ if connect_timeout && read_timeout
70
+ self.timeout = { :connect_timeout => connect_timeout, :read_timeout => read_timeout }
71
+ else
72
+ self.timeout = timeout
73
+ end
74
+
75
+ self.client_id = client_id
76
+ self.client_secret = client_secret
77
+ self.channel = channel
78
+ self.retry_timeout = retry_timeout
79
+ self.request_opts = request_opts || {}
80
+ self.request_opts.merge!({
81
+ :headers => {"User-Agent" => $USER_AGENT},
82
+ :timeout => self.timeout,
83
+ :verify => true
84
+ })
85
+
86
+ self.queries_per_second = queries_per_second
87
+ self.sent_times = Array.new
88
+ end
89
+
90
+ # Performs HTTP GET requests with credentials, returning the body as JSON or XML
91
+ #
92
+ # @param [String] url URL path for the request. Should begin with a slash.
93
+ # @param [Hash] params HTTP GET parameters.
94
+ # @param [Time] first_request_time The time of the first request (nil if no retries have occurred).
95
+ # @param [Integer] retry_counter The number of this retry, or zero for first attempt.
96
+ # @param [String] base_url The base URL for the request. Defaults to the Google Maps API server. Should not have a trailing slash.
97
+ # @param [TrueClass, FalseClass] accepts_clientid Flag whether this call supports the client/signature params. Some APIs require API keys (e.g. Roads).
98
+ # @param [Proc] extract_body A function that extracts the body from the request. If the request was not successful, the function should raise a
99
+ # GoogleMaps::Services::Exceptions::HTTËrror or GoogleMaps::Services::Exceptions::APIError as appropriate.
100
+ # @param [Hash] request_opts Additional options for the Net::HTTP client.
101
+ #
102
+ # @return [Hash, Array] response body, either in JSON or XML.
103
+ def get(url:, params:, first_request_time: nil, retry_counter: nil, base_url: $DEFAULT_BASE_URL,
104
+ accepts_clientid: true, extract_body: nil, request_opts: nil)
105
+ if !first_request_time
106
+ first_request_time = Util.current_time
107
+ end
108
+
109
+ elapsed = Time.now - first_request_time
110
+ if elapsed > self.retry_timeout
111
+ raise Timeout
112
+ end
113
+
114
+ if retry_counter && retry_counter > 0
115
+ # 0.5 * (1.5 ^ i) is an increased sleep time of 1.5x per iteration,
116
+ # starting at 0.5s when retry_counter=0. The first retry will occur
117
+ # at 1, so subtract that first.
118
+ delay_seconds = 0.5 * 1.5 ** (retry_counter - 1)
119
+ # Jitter this value by 50% and pause.
120
+ sleep(delay_seconds * (random.random() + 0.5))
121
+ end
122
+
123
+ authed_url = generate_auth_url(url, params, accepts_clientid)
124
+
125
+ # Default to the client-level self.request_opts, with method-level
126
+ # request_opts arg overriding.
127
+ request_opts = self.request_opts.merge(request_opts || {})
128
+
129
+ # Construct the Request URI
130
+ uri = URI.parse(base_url + authed_url)
131
+
132
+ # Add request headers
133
+ req = Net::HTTP::Get.new(uri.to_s)
134
+
135
+ request_opts[:headers].each { |header,value| req.add_field(header, value) }
136
+
137
+ http = Net::HTTP.new(uri.host, uri.port)
138
+ http.use_ssl = (uri.scheme == "https")
139
+ # Get HTTP response
140
+ resp = http.request(req)
141
+
142
+ # Handle response errors
143
+ case resp
144
+ when Net::HTTPRequestTimeOut
145
+ raise Timeout
146
+ when Exception
147
+ raise TransportError, "HTTP GET request failed."
148
+ end
149
+
150
+ if $RETRIABLE_STATUSES.include? resp.code.to_i
151
+ # Retry request
152
+ self.get(url, params, first_request_time, retry_counter + 1,
153
+ base_url, accepts_clientid, extract_body)
154
+ end
155
+
156
+ # Check if the time of the nth previous query (where n is queries_per_second)
157
+ # is under a second ago - if so, sleep for the difference.
158
+ if self.sent_times && (self.sent_times.length == self.queries_per_second)
159
+ elapsed_since_earliest = Util.current_time - self.sent_times[0]
160
+ if elapsed_since_earliest < 1
161
+ sleep(1 - elapsed_since_earliest)
162
+ end
163
+ end
164
+
165
+ begin
166
+ # Extract HTTP response body
167
+ if extract_body
168
+ result = extract_body.call(resp)
169
+ else
170
+ result = get_json_body(resp)
171
+ end
172
+ self.sent_times.push(Util.current_time)
173
+ return result
174
+ rescue RetriableRequest
175
+ # retry request
176
+ return self.get(url, params, first_request_time, retry_counter + 1,
177
+ base_url, accepts_clientid, extract_body)
178
+ end
179
+ end
180
+
181
+ # Extracts the JSON body of the HTTP response.
182
+ #
183
+ # @private
184
+ #
185
+ # @param [Net::HTTPResponse] resp HTTP response object.
186
+ #
187
+ # @return [Hash, Array] Valid JSON response.
188
+ def get_json_body(resp)
189
+ status_code = resp.code.to_i
190
+ if status_code >= 300 && status_code < 400
191
+ return resp["location"]
192
+ end
193
+
194
+ if resp.code.to_i != 200
195
+ raise HTTPError.new(resp.code)
196
+ end
197
+
198
+ # Parse the response body
199
+ begin
200
+ body = JSON.parse(resp.body)
201
+ rescue JSON::ParserError
202
+ raise APIError.new(status_code), "Received a malformed response."
203
+ end
204
+
205
+ api_status = body["status"]
206
+ if api_status == "OK" || api_status == "ZERO_RESULTS"
207
+ return body
208
+ end
209
+
210
+ if api_status == "OVER_QUERY_LIMIT"
211
+ raise RetriableRequest
212
+ end
213
+
214
+ if body.key?("error_message")
215
+ raise APIError.new(api_status), body["error_message"]
216
+ else
217
+ raise APIError.new(api_status)
218
+ end
219
+ end
220
+
221
+ # Returns the path and query string portion of the request URL, first adding any necessary parameters.
222
+ #
223
+ # @private
224
+ #
225
+ # @param [String] path The path portion of the URL.
226
+ # @param [Hash] params URL parameters.
227
+ # @param [TrueClass, FalseClass] accepts_clientid Flag whether to use a Client ID or not.
228
+ #
229
+ # @return [String] the final request path.
230
+ def generate_auth_url(path, params={}, accepts_clientid)
231
+ if accepts_clientid && self.client_id && self.client_secret
232
+ if self.channel
233
+ params["channel"] = self.channel
234
+ end
235
+ params["client"] = self.client_id
236
+
237
+ path = [path, Util.urlencode_params(params)].join("?")
238
+ sig = Util.sign_hmac(self.client_secret, path)
239
+ return path + "&signature=" + sig
240
+ end
241
+
242
+ if self.key
243
+ params["key"] = self.key
244
+ return path + "?" + Util.urlencode_params(params)
245
+ end
246
+
247
+ raise StandardError, "Must provide API key for this API. It does not accept enterprise credentials."
248
+ end
249
+
250
+ private :get_json_body, :generate_auth_url
251
+ end
252
+
253
+ end
254
+ end
@@ -0,0 +1,119 @@
1
+ require "googlemaps/services/util"
2
+
3
+ module GoogleMaps
4
+ module Services
5
+ $TRAVEL_MODES = ["driving", "walking", "bicycling", "transit"]
6
+
7
+ # Performs requests to the Google Maps Directions API.
8
+ #
9
+ # @example
10
+ # directions = GoogleMaps::Services::Directions.new(client)
11
+ # result = directions.query(origin: "Brussels", destination: {:lat => 52.520645, :lng => 13.409779})
12
+ class Directions
13
+ # @return [Symbol] The HTTP client.
14
+ attr_accessor :client
15
+
16
+ def initialize(client)
17
+ self.client = client
18
+ end
19
+
20
+ # Get directions between an origin point and a destination point.
21
+ #
22
+ # @param [String, Hash] origin The address or lat/lng hash value from which to calculate directions
23
+ # @param [String, Hash] destination The address or lat/lng value from which to calculate directions
24
+ # @param [String] mode The mode of transport to use when calculating directions. One of "driving",
25
+ # "walking", "bicycling" or "transit".
26
+ # @param [Array] waypoints Specifies an array of waypoints. Waypoints alter a route by routing it through
27
+ # the specified location(s). A location can be a String or a lat/lng hash.
28
+ # @param [TrueClass, FalseClass] alternatives If true, more than one route may be returned in the response.
29
+ # @param [Array] avoid Indicates that the calculated route(s) should avoid the indicated featues.
30
+ # @param [String] language The language in which to return results.
31
+ # @param [String] units Specifies the unit system to use when displaying results. "metric" or "imperial".
32
+ # @param [String] region The region code, specified as a ccTLD (top-level domain - two character value).
33
+ # @param [Integer, Time] departure_time Specifies the desired time of departure.
34
+ # @param [Integer, Time] arrival_time Specifies the desired time of arrival for transit directions.
35
+ # Note: you cannot specify both departure_time and arrival_time.
36
+ # @param [TrueClass, FalseClass] optimize_waypoints optimize the provided route by rearranging the waypoints in a more efficient order.
37
+ # @param [Array] transit_mode Specifies one or more preferred modes of transit. This parameter may only be specified for requests where the mode is transit.
38
+ # Valid values are "bus", "subway", "train", "tram", "rail".
39
+ # "rail" is equivalent to ["train", "tram", "subway"].
40
+ # @param [String] transit_routing_preference Specifies preferences for transit requests. Valid values are "less_walking" or "fewer_transfers".
41
+ # @param [String] traffic_model Specifies the predictive travel time model to use. Valid values are "best_guess" or "optimistic" or "pessimistic".
42
+ # The traffic_model parameter may only be specified for requests where the travel mode is driving, and where the
43
+ # request includes a departure_time.
44
+ #
45
+ # @return [Hash] Valid JSON or XML response.
46
+ def query(origin:, destination:, mode: nil, waypoints: nil, alternatives: false,
47
+ avoid: nil, language: nil, units: nil, region: nil, departure_time: nil,
48
+ arrival_time: nil, optimize_waypoints: false, transit_mode: nil,
49
+ transit_routing_preference: nil, traffic_model: nil)
50
+ params = {
51
+ "origin" => Convert.to_latlng(origin),
52
+ "destination" => Convert.to_latlng(destination)
53
+ }
54
+
55
+ if mode
56
+ if !$TRAVEL_MODES.include? mode
57
+ raise StandardError, "invalid travel mode."
58
+ end
59
+ params["mode"] = mode
60
+ end
61
+
62
+ if waypoints
63
+ waypoints = Convert.piped_location(waypoints)
64
+ if optimize_waypoints
65
+ waypoints = "optimize:true|" + waypoints
66
+ end
67
+ params["waypoints"] = waypoints
68
+ end
69
+
70
+ if alternatives
71
+ params["alternatives"] = true
72
+ end
73
+
74
+ if avoid
75
+ params["avoid"] = Convert.join_array("|", avoid)
76
+ end
77
+
78
+ if language
79
+ params["language"] = language
80
+ end
81
+
82
+ if units
83
+ params["units"] = units
84
+ end
85
+
86
+ if region
87
+ params["region"] = region
88
+ end
89
+
90
+ if departure_time
91
+ params["departure_time"] = Convert.unix_time(departure_time)
92
+ end
93
+
94
+ if arrival_time
95
+ params["arrival_time"] = Convert.unix_time(arrival_time)
96
+ end
97
+
98
+ if departure_time && arrival_time
99
+ raise StandardError, "should not specify both departure_time and arrival_time."
100
+ end
101
+
102
+ if transit_mode
103
+ params["transit_mode"] = Convert.join_array("|", transit_mode)
104
+ end
105
+
106
+ if transit_routing_preference
107
+ params["transit_routing_preference"] = transit_routing_preference
108
+ end
109
+
110
+ if traffic_model
111
+ params["traffic_model"] = traffic_model
112
+ end
113
+
114
+ self.client.get(url: "/maps/api/directions/json", params: params)["routes"]
115
+ end
116
+ end
117
+
118
+ end
119
+ end
@@ -0,0 +1,98 @@
1
+ require "googlemaps/services/util"
2
+
3
+ module GoogleMaps
4
+ module Services
5
+
6
+ $AVOIDS = ["tolls", "highways", "ferries"]
7
+
8
+ # Performs requests to the Google Maps Distance Matrix API.
9
+ #
10
+ # @example
11
+ # distancematrix = GoogleMaps::Services::DistanceMatrix(client)
12
+ # result = distancematrix.query(origins: ["Brussels", "Ghent"], destinations: ["Bruges"])
13
+ class DistanceMatrix
14
+ # @return [Symbol] the HTTP client.
15
+ attr_accessor :client
16
+
17
+ def initialize(client)
18
+ self.client = client
19
+ end
20
+
21
+ # Gets travel distance and time for a matrix of origins and destinations.
22
+ #
23
+ # @param [Array] origins One or more locations and/or lat/lng values, from which to calculate distance and time.
24
+ # If you pass an address as a string, the service will geocode the string and convert it to a lat/lng coordinate to calculate directions.
25
+ # @param [Array] destinations One or more addresses and/or lat/lng values, to which to calculate distance and time.
26
+ # If you pass an address as a string, the service will geocode the string and convert it to a lat/lng coordinate to calculate directions.
27
+ # @param [String] mode Specifies the mode of transport to use when calculating directions. Valid values are "driving", "walking", "transit" or "bicycling".
28
+ # @param [String] language The language in which to return results.
29
+ # @param [String] avoid Indicates that the calculated route(s) should avoid the indicated features. Valid values are "tolls", "highways" or "ferries".
30
+ # @param [String] units Specifies the unit system to use when displaying results. Valid values are "metric" or "imperial".
31
+ # @param [Integer, Time, Date] departure_time Specifies the desired time of departure.
32
+ # @param [Integer, Time, Date] arrival_time Specifies the desired time of arrival for transit directions. Note: you can't specify both departure_time and arrival_time.
33
+ # @param [Array] transit_mode Specifies one or more preferred modes of transit. his parameter may only be specified for requests where the mode is transit.
34
+ # Valid values are "bus", "subway", "train", "tram", "rail". "rail" is equivalent to ["train", "tram", "subway"].
35
+ # @param [String] transit_routing_preference Specifies preferences for transit requests. Valid values are "less_walking" or "fewer_transfers".
36
+ # @param [String] traffic_model Specifies the predictive travel time model to use. Valid values are "best_guess" or "optimistic" or "pessimistic".
37
+ # The traffic_model parameter may only be specified for requests where the travel mode is driving, and where the request includes a departure_time.
38
+ #
39
+ # @return [Hash] Matrix of distances.
40
+ def query(origins:, destinations:, mode: nil, language: nil, avoid: nil,
41
+ units: nil, departure_time: nil, arrival_time: nil, transit_mode: nil,
42
+ transit_routing_preference: nil, traffic_model: nil)
43
+ params = {
44
+ "origins" => Convert.piped_location(origins),
45
+ "destinations" => Convert.piped_location(destinations)
46
+ }
47
+
48
+ if mode
49
+ if !$TRAVEL_MODES.include? mode
50
+ raise StandardError, "Invalid travel mode."
51
+ end
52
+ params["mode"] = mode
53
+ end
54
+
55
+ if language
56
+ params["language"] = language
57
+ end
58
+
59
+ if avoid
60
+ if !$AVOIDS.include? avoid
61
+ raise StandardError, "Invalid route restriction."
62
+ end
63
+ params["avoid"] = avoid
64
+ end
65
+
66
+ if units
67
+ params["units"] = units
68
+ end
69
+
70
+ if departure_time
71
+ params["departure_time"] = Convert.unix_time(departure_time)
72
+ end
73
+
74
+ if arrival_time
75
+ params["arrival_time"] = Convert.unix_time(arrival_time)
76
+ end
77
+
78
+ if departure_time && arrival_time
79
+ raise StandardError, "Should not specify both departure_time and arrival_time."
80
+ end
81
+
82
+ if transit_mode
83
+ params["transit_mode"] = Convert.join_arrayt("|", transit_mode)
84
+ end
85
+
86
+ if transit_routing_preference
87
+ params["transit_routing_preference"] = transit_routing_preference
88
+ end
89
+
90
+ if traffic_model
91
+ params["traffic_model"] = traffic_model
92
+ end
93
+
94
+ self.client.get(url: "/maps/api/distancematrix/json", params: params)
95
+ end
96
+ end
97
+ end
98
+ end