google_maps_service_ruby 0.6.0

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