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