googlemaps-services 1.0.0

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