google_maps_service_ruby 0.6.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,82 @@
1
+ require "google_maps_service/convert"
2
+
3
+ module GoogleMapsService::Apis
4
+ # Performs requests to the Google Maps Geocoding API.
5
+ module Geocoding
6
+ # Geocoding is the process of converting addresses
7
+ # (like `"1600 Amphitheatre Parkway, Mountain View, CA"`) into geographic
8
+ # coordinates (like latitude 37.423021 and longitude -122.083739), which you
9
+ # can use to place markers or position the map.
10
+ #
11
+ # @example Geocode an address
12
+ # results = client.geocode('Sydney')
13
+ #
14
+ # @example Geocode a component only
15
+ # results = client.geocode(nil, components: {administrative_area: 'TX', country: 'US'})
16
+ #
17
+ # @example Geocode an address and component
18
+ # results = client.geocode('Sydney', components: {administrative_area: 'TX', country: 'US'})
19
+ #
20
+ # @example Multiple parameters
21
+ # results = client.geocode('Sydney',
22
+ # components: {administrative_area: 'TX', country: 'US'},
23
+ # bounds: {
24
+ # northeast: {lat: 32.7183997, lng: -97.26864001970849},
25
+ # southwest: {lat: 32.7052583, lng: -97.27133798029149}
26
+ # },
27
+ # region: 'us')
28
+ #
29
+ # @param [String] address The address to geocode. You must specify either this value and/or `components`.
30
+ # @param [Hash] components A component filter for which you wish to obtain a geocode,
31
+ # for example: `{'administrative_area': 'TX','country': 'US'}`
32
+ # @param [String, Hash] bounds The bounding box of the viewport within which to bias geocode
33
+ # results more prominently. Accept string or hash with `northeast` and `southwest` keys.
34
+ # @param [String] region The region code, specified as a ccTLD (_top-level domain_)
35
+ # two-character value.
36
+ # @param [String] language The language in which to return results.
37
+ #
38
+ # @return [Array] Array of geocoding results.
39
+ def geocode(address, components: nil, bounds: nil, region: nil, language: nil)
40
+ params = {}
41
+
42
+ params[:address] = address if address
43
+ params[:components] = GoogleMapsService::Convert.components(components) if components
44
+ params[:bounds] = GoogleMapsService::Convert.bounds(bounds) if bounds
45
+ params[:region] = region if region
46
+ params[:language] = language if language
47
+
48
+ get("/maps/api/geocode/json", params)[:results]
49
+ end
50
+
51
+ # Reverse geocoding is the process of converting geographic coordinates into a
52
+ # human-readable address.
53
+ #
54
+ # @example Simple lat/lon pair
55
+ # client.reverse_geocode({lat: 40.714224, lng: -73.961452})
56
+ #
57
+ # @example Multiple parameters
58
+ # client.reverse_geocode([40.714224, -73.961452],
59
+ # location_type: ['ROOFTOP', 'RANGE_INTERPOLATED'],
60
+ # result_type: ['street_address', 'route'],
61
+ # language: 'es')
62
+ #
63
+ # @param [Hash, Array] latlng The latitude/longitude value for which you wish to obtain
64
+ # the closest, human-readable address.
65
+ # @param [String, Array<String>] location_type One or more location types to restrict results to.
66
+ # @param [String, Array<String>] result_type One or more address types to restrict results to.
67
+ # @param [String] language The language in which to return results.
68
+ #
69
+ # @return [Array] Array of reverse geocoding results.
70
+ def reverse_geocode(latlng, location_type: nil, result_type: nil, language: nil)
71
+ params = {
72
+ latlng: GoogleMapsService::Convert.latlng(latlng)
73
+ }
74
+
75
+ params[:result_type] = GoogleMapsService::Convert.join_list("|", result_type) if result_type
76
+ params[:location_type] = GoogleMapsService::Convert.join_list("|", location_type) if location_type
77
+ params[:language] = language if language
78
+
79
+ get("/maps/api/geocode/json", params)[:results]
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,183 @@
1
+ require "multi_json"
2
+
3
+ module GoogleMapsService::Apis
4
+ # Performs requests to the Google Maps Roads API.
5
+ module Roads
6
+ # Base URL of Google Maps Roads API
7
+ ROADS_BASE_URL = "https://roads.googleapis.com"
8
+
9
+ # Snaps a path to the most likely roads travelled.
10
+ #
11
+ # Takes up to 100 GPS points collected along a route, and returns a similar
12
+ # set of data with the points snapped to the most likely roads the vehicle
13
+ # was traveling along.
14
+ #
15
+ # @example Single point snap
16
+ # results = client.snap_to_roads([40.714728, -73.998672])
17
+ #
18
+ # @example Multi points snap
19
+ # path = [
20
+ # [-33.8671, 151.20714],
21
+ # [-33.86708, 151.20683000000002],
22
+ # [-33.867070000000005, 151.20674000000002],
23
+ # [-33.86703, 151.20625]
24
+ # ]
25
+ # results = client.snap_to_roads(path, interpolate: true)
26
+ #
27
+ # @param [Array] path The path to be snapped. Array of latitude/longitude pairs.
28
+ # @param [Boolean] interpolate Whether to interpolate a path to include all points
29
+ # forming the full road-geometry. When true, additional interpolated
30
+ # points will also be returned, resulting in a path that smoothly
31
+ # follows the geometry of the road, even around corners and through
32
+ # tunnels. Interpolated paths may contain more points than the
33
+ # original path.
34
+ #
35
+ # @return [Array] Array of snapped points.
36
+ def snap_to_roads(path, interpolate: false)
37
+ path = GoogleMapsService::Convert.waypoints(path)
38
+
39
+ params = {
40
+ path: path
41
+ }
42
+
43
+ params[:interpolate] = "true" if interpolate
44
+
45
+ get("/v1/snapToRoads", params,
46
+ base_url: ROADS_BASE_URL,
47
+ accepts_client_id: false,
48
+ custom_response_decoder: method(:extract_roads_body))[:snappedPoints]
49
+ end
50
+
51
+ # Returns the posted speed limit (in km/h) for given road segments.
52
+ #
53
+ # @example Multi places snap
54
+ # place_ids = [
55
+ # 'ChIJ0wawjUCuEmsRgfqC5Wd9ARM',
56
+ # 'ChIJ6cs2kkCuEmsRUfqC5Wd9ARM'
57
+ # ]
58
+ # results = client.speed_limits(place_ids)
59
+ #
60
+ # @param [String, Array<String>] place_ids The Place ID of the road segment. Place IDs are returned
61
+ # by the snap_to_roads function. You can pass up to 100 Place IDs.
62
+ #
63
+ # @return [Array] Array of speed limits.
64
+ def speed_limits(place_ids)
65
+ params = GoogleMapsService::Convert.as_list(place_ids).map { |place_id| ["placeId", place_id] }
66
+
67
+ get("/v1/speedLimits", params,
68
+ base_url: ROADS_BASE_URL,
69
+ accepts_client_id: false,
70
+ custom_response_decoder: method(:extract_roads_body))[:speedLimits]
71
+ end
72
+
73
+ # Returns the posted speed limit (in km/h) for given road segments.
74
+ #
75
+ # The provided points will first be snapped to the most likely roads the
76
+ # vehicle was traveling along.
77
+ #
78
+ # @example Multi points snap
79
+ # path = [
80
+ # [-33.8671, 151.20714],
81
+ # [-33.86708, 151.20683000000002],
82
+ # [-33.867070000000005, 151.20674000000002],
83
+ # [-33.86703, 151.20625]
84
+ # ]
85
+ # results = client.snapped_speed_limits(path)
86
+ #
87
+ # @param [Hash, Array] path The path of points to be snapped. A list of (or single)
88
+ # latitude/longitude tuples.
89
+ #
90
+ # @return [Hash] A hash with both a list of speed limits and a list of the snapped
91
+ # points.
92
+ def snapped_speed_limits(path)
93
+ path = GoogleMapsService::Convert.waypoints(path)
94
+
95
+ params = {
96
+ path: path
97
+ }
98
+
99
+ get("/v1/speedLimits", params,
100
+ base_url: ROADS_BASE_URL,
101
+ accepts_client_id: false,
102
+ custom_response_decoder: method(:extract_roads_body))
103
+ end
104
+
105
+ # Returns the nearest road segments for provided points.
106
+ # The points passed do not need to be part of a continuous path.
107
+ #
108
+ # @example Single point snap
109
+ # results = client.nearest_roads([40.714728, -73.998672])
110
+ #
111
+ # @example Multi points snap
112
+ # points = [
113
+ # [-33.8671, 151.20714],
114
+ # [-33.86708, 151.20683000000002],
115
+ # [-33.867070000000005, 151.20674000000002],
116
+ # [-33.86703, 151.20625]
117
+ # ]
118
+ # results = client.nearest_roads(points)
119
+ #
120
+ # @param [Array] points The points to be used for nearest road segment lookup. Array of latitude/longitude pairs
121
+ # which do not need to be a part of continuous part.
122
+ # Takes up to 100 independent coordinates, and returns the closest road segment for each point.
123
+ #
124
+ # @return [Array] Array of snapped points.
125
+
126
+ def nearest_roads(points)
127
+ points = GoogleMapsService::Convert.waypoints(points)
128
+
129
+ params = {
130
+ points: points
131
+ }
132
+
133
+ get("/v1/nearestRoads", params,
134
+ base_url: ROADS_BASE_URL,
135
+ accepts_client_id: false,
136
+ custom_response_decoder: method(:extract_roads_body))[:snappedPoints]
137
+ end
138
+
139
+ private
140
+
141
+ # Extracts a result from a Roads API HTTP response.
142
+ def extract_roads_body(response)
143
+ begin
144
+ body = MultiJson.load(response.body, symbolize_keys: true)
145
+ rescue
146
+ unless response.code == "200"
147
+ check_response_status_code(response)
148
+ end
149
+ raise GoogleMapsService::Error::ApiError.new(response), "Received a malformed response."
150
+ end
151
+
152
+ check_roads_body_error(response, body)
153
+
154
+ unless response.code == "200"
155
+ raise GoogleMapsService::Error::ApiError.new(response)
156
+ end
157
+ body
158
+ end
159
+
160
+ # Check response body for error status.
161
+ #
162
+ # @param [Net::HTTPResponse] response Response object.
163
+ # @param [Hash] body Response body.
164
+ def check_roads_body_error(response, body)
165
+ error = body[:error]
166
+ return unless error
167
+
168
+ case error[:status]
169
+ when "INVALID_ARGUMENT"
170
+ if error[:message] == "The provided API key is invalid."
171
+ raise GoogleMapsService::Error::RequestDeniedError.new(response), error[:message]
172
+ end
173
+ raise GoogleMapsService::Error::InvalidRequestError.new(response), error[:message]
174
+ when "PERMISSION_DENIED"
175
+ raise GoogleMapsService::Error::RequestDeniedError.new(response), error[:message]
176
+ when "RESOURCE_EXHAUSTED"
177
+ raise GoogleMapsService::Error::RateLimitError.new(response), error[:message]
178
+ else
179
+ raise GoogleMapsService::Error::ApiError.new(response), error[:message]
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,39 @@
1
+ require "date"
2
+
3
+ module GoogleMapsService::Apis
4
+ # Performs requests to the Google Maps TimeZone API."""
5
+ module TimeZone
6
+ # Get time zone for a location on the earth, as well as that location's
7
+ # time offset from UTC.
8
+ #
9
+ # @example Current time zone
10
+ # timezone = client.timezone([39.603481, -119.682251])
11
+ #
12
+ # @example Time zone at certain time
13
+ # timezone = client.timezone([39.603481, -119.682251], timestamp: Time.at(1608))
14
+ #
15
+ # @param [Hash, Array] location The latitude/longitude value representing the location to
16
+ # look up.
17
+ # @param [Integer, DateTime] timestamp Timestamp specifies the desired time as seconds since
18
+ # midnight, January 1, 1970 UTC. The Time Zone API uses the timestamp to
19
+ # determine whether or not Daylight Savings should be applied. Times
20
+ # before 1970 can be expressed as negative values. Optional. Defaults to
21
+ # `Time.now`.
22
+ # @param [String] language The language in which to return results.
23
+ #
24
+ # @return [Hash] Time zone object.
25
+ def timezone(location, timestamp: Time.now, language: nil)
26
+ location = GoogleMapsService::Convert.latlng(location)
27
+ timestamp = GoogleMapsService::Convert.time(timestamp)
28
+
29
+ params = {
30
+ location: location,
31
+ timestamp: timestamp
32
+ }
33
+
34
+ params[:language] = language if language
35
+
36
+ get("/maps/api/timezone/json", params)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ module GoogleMapsService
2
+ # Collections of Google Maps Web Services
3
+ module Apis
4
+ end
5
+ end
@@ -0,0 +1,255 @@
1
+ require "multi_json"
2
+ require "net/http"
3
+ require "retriable"
4
+ require "google_maps_service/errors"
5
+ require "google_maps_service/convert"
6
+ require "google_maps_service/url"
7
+ require "google_maps_service/apis/directions"
8
+ require "google_maps_service/apis/distance_matrix"
9
+ require "google_maps_service/apis/elevation"
10
+ require "google_maps_service/apis/geocoding"
11
+ require "google_maps_service/apis/roads"
12
+ require "google_maps_service/apis/time_zone"
13
+
14
+ module GoogleMapsService
15
+ # Core client functionality, common across all API requests (including performing
16
+ # HTTP requests).
17
+ class Client
18
+ # Default Google Maps Web Service base endpoints
19
+ DEFAULT_BASE_URL = "https://maps.googleapis.com"
20
+
21
+ # Errors those could be retriable.
22
+ RETRIABLE_ERRORS = [GoogleMapsService::Error::ServerError, GoogleMapsService::Error::RateLimitError]
23
+
24
+ include GoogleMapsService::Apis::Directions
25
+ include GoogleMapsService::Apis::DistanceMatrix
26
+ include GoogleMapsService::Apis::Elevation
27
+ include GoogleMapsService::Apis::Geocoding
28
+ include GoogleMapsService::Apis::Roads
29
+ include GoogleMapsService::Apis::TimeZone
30
+
31
+ # Secret key for accessing Google Maps Web Service.
32
+ # Can be obtained at https://developers.google.com/maps/documentation/geocoding/get-api-key#key.
33
+ # @return [String]
34
+ attr_accessor :key
35
+
36
+ # Client id for using Maps API for Work services.
37
+ # @return [String]
38
+ attr_accessor :client_id
39
+
40
+ # Client secret for using Maps API for Work services.
41
+ # @return [String]
42
+ attr_accessor :client_secret
43
+
44
+ # Timeout across multiple retriable requests, in seconds.
45
+ # @return [Integer]
46
+ attr_accessor :retry_timeout
47
+
48
+ # Number of queries per second permitted.
49
+ # If the rate limit is reached, the client will sleep for
50
+ # the appropriate amount of time before it runs the current query.
51
+ # @return [Integer]
52
+ attr_reader :queries_per_second
53
+
54
+ # Construct Google Maps Web Service API client.
55
+ #
56
+ # @example Setup API keys
57
+ # gmaps = GoogleMapsService::Client.new(key: 'Add your key here')
58
+ #
59
+ # @example Setup client IDs
60
+ # gmaps = GoogleMapsService::Client.new(
61
+ # client_id: 'Add your client id here',
62
+ # client_secret: 'Add your client secret here'
63
+ # )
64
+ #
65
+ # @example Setup time out and QPS limit
66
+ # gmaps = GoogleMapsService::Client.new(
67
+ # key: 'Add your key here',
68
+ # retry_timeout: 20,
69
+ # queries_per_second: 10
70
+ # )
71
+ #
72
+ # @option options [String] :key Secret key for accessing Google Maps Web Service.
73
+ # Can be obtained at https://developers.google.com/maps/documentation/geocoding/get-api-key#key.
74
+ # @option options [String] :client_id Client id for using Maps API for Work services.
75
+ # @option options [String] :client_secret Client secret for using Maps API for Work services.
76
+ # @option options [Integer] :retry_timeout Timeout across multiple retriable requests, in seconds.
77
+ # @option options [Integer] :queries_per_second Number of queries per second permitted.
78
+ def initialize(**options)
79
+ [:key, :client_id, :client_secret,
80
+ :retry_timeout, :queries_per_second].each do |key|
81
+ instance_variable_set("@#{key}".to_sym, options[key] || GoogleMapsService.instance_variable_get("@#{key}"))
82
+ end
83
+ [:request_options, :ssl_options, :connection].each do |key|
84
+ if options.has_key?(key)
85
+ raise "GoogleMapsService::Client.new no longer supports #{key}."
86
+ end
87
+ end
88
+
89
+ initialize_query_tickets
90
+ end
91
+
92
+ # Get the current HTTP client.
93
+ # @deprecated
94
+ def client
95
+ raise "GoogleMapsService::Client.client is no longer implemented."
96
+ end
97
+
98
+ protected
99
+
100
+ # Initialize QPS queue. QPS queue is a "tickets" for calling API
101
+ def initialize_query_tickets
102
+ if @queries_per_second
103
+ @qps_queue = SizedQueue.new @queries_per_second
104
+ @queries_per_second.times do
105
+ @qps_queue << 0
106
+ end
107
+ end
108
+ end
109
+
110
+ # Create a new HTTP client.
111
+ # @deprecated
112
+ def new_client
113
+ raise "GoogleMapsService::Client.new_client is no longer implemented."
114
+ end
115
+
116
+ # Build the user agent header
117
+ # @return [String]
118
+ def user_agent
119
+ @user_agent ||= sprintf("google-maps-services-ruby/%s %s",
120
+ GoogleMapsService::VERSION,
121
+ GoogleMapsService::OS_VERSION)
122
+ end
123
+
124
+ # Make API call.
125
+ #
126
+ # @param [String] path Url path.
127
+ # @param [String] params Request parameters.
128
+ # @param [String] base_url Base Google Maps Web Service API endpoint url.
129
+ # @param [Boolean] accepts_client_id Sign the request using API {#keys} instead of {#client_id}.
130
+ # @param [Method] custom_response_decoder Custom method to decode raw API response.
131
+ #
132
+ # @return [Object] Decoded response body.
133
+ def get(path, params, base_url: DEFAULT_BASE_URL, accepts_client_id: true, custom_response_decoder: nil)
134
+ url = URI(base_url + generate_auth_url(path, params, accepts_client_id))
135
+
136
+ Retriable.retriable timeout: @retry_timeout, on: RETRIABLE_ERRORS do |try|
137
+ begin
138
+ request_query_ticket
139
+ request = Net::HTTP::Get.new(url)
140
+ request["User-Agent"] = user_agent
141
+ response = Net::HTTP.start(url.hostname, url.port, use_ssl: url.scheme == "https") do |http|
142
+ http.request(request)
143
+ end
144
+ ensure
145
+ release_query_ticket
146
+ end
147
+
148
+ return custom_response_decoder.call(response) if custom_response_decoder
149
+ decode_response_body(response)
150
+ end
151
+ end
152
+
153
+ # Get/wait the request "ticket" if QPS is configured.
154
+ # Check for previous request time, it must be more than a second ago before calling new request.
155
+ #
156
+ # @return [void]
157
+ def request_query_ticket
158
+ if @qps_queue
159
+ elapsed_since_earliest = Time.now - @qps_queue.pop
160
+ sleep(1 - elapsed_since_earliest) if elapsed_since_earliest.to_f < 1
161
+ end
162
+ end
163
+
164
+ # Release request "ticket".
165
+ #
166
+ # @return [void]
167
+ def release_query_ticket
168
+ @qps_queue << Time.now if @qps_queue
169
+ end
170
+
171
+ # Returns the path and query string portion of the request URL,
172
+ # first adding any necessary parameters.
173
+ #
174
+ # @param [String] path The path portion of the URL.
175
+ # @param [Hash] params URL parameters.
176
+ # @param [Boolean] accepts_client_id Sign the request using API {#keys} instead of {#client_id}.
177
+ #
178
+ # @return [String]
179
+ def generate_auth_url(path, params, accepts_client_id)
180
+ # Deterministic ordering through sorting by key.
181
+ # Useful for tests, and in the future, any caching.
182
+ params = if params.is_a?(Hash)
183
+ params.sort
184
+ else
185
+ params.dup
186
+ end
187
+
188
+ if accepts_client_id && @client_id && @client_secret
189
+ params << ["client", @client_id]
190
+
191
+ path = [path, GoogleMapsService::Url.urlencode_params(params)].join("?")
192
+ sig = GoogleMapsService::Url.sign_hmac(@client_secret, path)
193
+ return path + "&signature=" + sig
194
+ end
195
+
196
+ if @key
197
+ params << ["key", @key]
198
+ return path + "?" + GoogleMapsService::Url.urlencode_params(params)
199
+ end
200
+
201
+ raise ArgumentError, "Must provide API key for this API. It does not accept enterprise credentials."
202
+ end
203
+
204
+ # Extract and parse body response as hash. Throw an error if there is something wrong with the response.
205
+ #
206
+ # @param [Net::HTTPResponse] response Web API response.
207
+ #
208
+ # @return [Hash] Response body as hash. The hash key will be symbolized.
209
+ def decode_response_body(response)
210
+ check_response_status_code(response)
211
+ body = MultiJson.load(response.body, symbolize_keys: true)
212
+ check_body_error(response, body)
213
+ body
214
+ end
215
+
216
+ # Check HTTP response status code. Raise error if the status is not 2xx.
217
+ #
218
+ # @param [Net::HTTPResponse] response Web API response.
219
+ def check_response_status_code(response)
220
+ case response.code.to_i
221
+ when 200..300
222
+ # Do-nothing
223
+ when 301, 302, 303, 307
224
+ raise GoogleMapsService::Error::RedirectError.new(response), sprintf("Redirect to %s", response.header[:location])
225
+ when 401
226
+ raise GoogleMapsService::Error::ClientError.new(response), "Unauthorized"
227
+ when 304, 400, 402...500
228
+ raise GoogleMapsService::Error::ClientError.new(response), "Invalid request"
229
+ when 500..600
230
+ raise GoogleMapsService::Error::ServerError.new(response), "Server error"
231
+ end
232
+ end
233
+
234
+ # Check response body for error status.
235
+ #
236
+ # @param [Net::HTTPResponse] response Response object.
237
+ # @param [Hash] body Response body.
238
+ #
239
+ # @return [void]
240
+ def check_body_error(response, body)
241
+ case body[:status]
242
+ when "OK", "ZERO_RESULTS"
243
+ # Do-nothing
244
+ when "OVER_QUERY_LIMIT"
245
+ raise GoogleMapsService::Error::RateLimitError.new(response), body[:error_message]
246
+ when "REQUEST_DENIED"
247
+ raise GoogleMapsService::Error::RequestDeniedError.new(response), body[:error_message]
248
+ when "INVALID_REQUEST"
249
+ raise GoogleMapsService::Error::InvalidRequestError.new(response), body[:error_message]
250
+ else
251
+ raise GoogleMapsService::Error::ApiError.new(response), body[:error_message]
252
+ end
253
+ end
254
+ end
255
+ end