google_maps_service 0.1.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.
- 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 +3 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/README.md +132 -0
- data/Rakefile +12 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/google_maps_service.gemspec +30 -0
- data/lib/google_maps_service.rb +21 -0
- data/lib/google_maps_service/client.rb +226 -0
- data/lib/google_maps_service/convert.rb +227 -0
- data/lib/google_maps_service/directions.rb +93 -0
- data/lib/google_maps_service/distance_matrix.rb +85 -0
- data/lib/google_maps_service/elevation.rb +57 -0
- data/lib/google_maps_service/errors.rb +40 -0
- data/lib/google_maps_service/geocoding.rb +58 -0
- data/lib/google_maps_service/roads.rb +141 -0
- data/lib/google_maps_service/time_zone.rb +36 -0
- data/lib/google_maps_service/version.rb +3 -0
- metadata +215 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
module GoogleMapsService
|
2
|
+
class << self
|
3
|
+
attr_accessor :key, :client_id, :client_secret, :ssl, :connection_middleware
|
4
|
+
|
5
|
+
def configure
|
6
|
+
yield self
|
7
|
+
true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'google_maps_service/version'
|
12
|
+
require 'google_maps_service/errors'
|
13
|
+
require 'google_maps_service/convert'
|
14
|
+
require 'google_maps_service/directions'
|
15
|
+
require 'google_maps_service/distance_matrix'
|
16
|
+
require 'google_maps_service/elevation'
|
17
|
+
require 'google_maps_service/geocoding'
|
18
|
+
require 'google_maps_service/roads'
|
19
|
+
require 'google_maps_service/time_zone'
|
20
|
+
require 'google_maps_service/client'
|
21
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'hurley'
|
3
|
+
require 'multi_json'
|
4
|
+
|
5
|
+
module GoogleMapsService
|
6
|
+
class Client
|
7
|
+
USER_AGENT = "GoogleGeoApiClientRuby/#{GoogleMapsService::VERSION}"
|
8
|
+
DEFAULT_BASE_URL = "https://maps.googleapis.com"
|
9
|
+
RETRIABLE_STATUSES = [500, 503, 504]
|
10
|
+
|
11
|
+
include GoogleMapsService::Directions
|
12
|
+
include GoogleMapsService::DistanceMatrix
|
13
|
+
include GoogleMapsService::Elevation
|
14
|
+
include GoogleMapsService::Geocoding
|
15
|
+
include GoogleMapsService::Roads
|
16
|
+
include GoogleMapsService::TimeZone
|
17
|
+
|
18
|
+
# Secret key for accessing Google Maps Web Service.
|
19
|
+
# Can be obtained at https://developers.google.com/maps/documentation/geocoding/get-api-key#key
|
20
|
+
# @return [String]
|
21
|
+
attr_reader :key
|
22
|
+
|
23
|
+
# Client id for using Maps API for Work services.
|
24
|
+
# @return [String]
|
25
|
+
attr_reader :client_id
|
26
|
+
|
27
|
+
# Client secret for using Maps API for Work services.
|
28
|
+
# @return [String]
|
29
|
+
attr_reader :client_secret
|
30
|
+
|
31
|
+
def initialize(options={})
|
32
|
+
@key = options[:key] || GoogleMapsService.key
|
33
|
+
@client_id = options[:client_id] || GoogleMapsService.client_id
|
34
|
+
@client_secret = options[:client_secret] || GoogleMapsService.client_secret
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get the current HTTP client
|
38
|
+
# @return [Hurley::Client]
|
39
|
+
def client
|
40
|
+
@client ||= new_client
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
# Create a new HTTP client
|
46
|
+
# @return [Hurley::Client]
|
47
|
+
def new_client
|
48
|
+
client = Hurley::Client.new
|
49
|
+
client.request_options.query_class = Hurley::Query::Flat
|
50
|
+
client.header[:user_agent] = USER_AGENT
|
51
|
+
client
|
52
|
+
end
|
53
|
+
|
54
|
+
def get(path, params, base_url: DEFAULT_BASE_URL, accepts_client_id: true, custom_response_decoder: nil)
|
55
|
+
url = base_url + generate_auth_url(path, params, accepts_client_id)
|
56
|
+
response = client.get url
|
57
|
+
|
58
|
+
if custom_response_decoder
|
59
|
+
return custom_response_decoder.call(response)
|
60
|
+
end
|
61
|
+
return decode_response_body(response)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Extract and parse body response as hash. Throw an error if there is something wrong with the response.
|
65
|
+
#
|
66
|
+
# @param [Hurley::Response] response Web API response.
|
67
|
+
#
|
68
|
+
# @return [Hash] Response body as hash. The hash key will be symbolized.
|
69
|
+
#
|
70
|
+
# @raise [GoogleMapsService::Error::RedirectError] The response redirects to another URL.
|
71
|
+
# @raise [GoogleMapsService::Error::RequestDeniedError] The credential (key or client id pair) is not valid.
|
72
|
+
# @raise [GoogleMapsService::Error::ClientError] The request is invalid and should not be retried without modification.
|
73
|
+
# @raise [GoogleMapsService::Error::ServerError] An error occurred on the server and the request can be retried.
|
74
|
+
# @raise [GoogleMapsService::Error::TransmissionError] Unknown response status code.
|
75
|
+
# @raise [GoogleMapsService::Error::RateLimitError] The quota for the credential is already pass the limit.
|
76
|
+
# @raise [GoogleMapsService::Error::ApiError] The Web API error.
|
77
|
+
def decode_response_body(response)
|
78
|
+
check_response_status_code(response)
|
79
|
+
|
80
|
+
body = MultiJson.load(response.body, :symbolize_keys => true)
|
81
|
+
|
82
|
+
api_status = body[:status]
|
83
|
+
if api_status == "OK" or api_status == "ZERO_RESULTS"
|
84
|
+
return body
|
85
|
+
end
|
86
|
+
|
87
|
+
if api_status == "OVER_QUERY_LIMIT"
|
88
|
+
raise GoogleMapsService::Error::RateLimitError.new(response), body[:error_message]
|
89
|
+
end
|
90
|
+
|
91
|
+
if api_status == "REQUEST_DENIED"
|
92
|
+
raise GoogleMapsService::Error::RequestDeniedError.new(response), body[:error_message]
|
93
|
+
end
|
94
|
+
|
95
|
+
if api_status == "INVALID_REQUEST"
|
96
|
+
raise GoogleMapsService::Error::InvalidRequestError.new(response), body[:error_message]
|
97
|
+
end
|
98
|
+
|
99
|
+
if body[:error_message]
|
100
|
+
raise GoogleMapsService::Error::ApiError.new(response), body[:error_message]
|
101
|
+
else
|
102
|
+
raise GoogleMapsService::Error::ApiError.new(response)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def check_response_status_code(response)
|
107
|
+
case response.status_code
|
108
|
+
when 200..300
|
109
|
+
# Do-nothing
|
110
|
+
when 301, 302, 303, 307
|
111
|
+
message ||= sprintf('Redirect to %s', response.header[:location])
|
112
|
+
raise GoogleMapsService::Error::RedirectError.new(response), message
|
113
|
+
when 401
|
114
|
+
message ||= 'Unauthorized'
|
115
|
+
raise GoogleMapsService::Error::ClientError.new(response)
|
116
|
+
when 304, 400, 402...500
|
117
|
+
message ||= 'Invalid request'
|
118
|
+
raise GoogleMapsService::Error::ClientError.new(response)
|
119
|
+
when 500..600
|
120
|
+
message ||= 'Server error'
|
121
|
+
raise GoogleMapsService::Error::ServerError.new(response)
|
122
|
+
else
|
123
|
+
message ||= 'Unknown error'
|
124
|
+
raise GoogleMapsService::Error::TransmissionError.new(response)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns the path and query string portion of the request URL,
|
129
|
+
# first adding any necessary parameters.
|
130
|
+
#
|
131
|
+
# @param [String] path The path portion of the URL.
|
132
|
+
# @param [Hash] params URL parameters.
|
133
|
+
#
|
134
|
+
# @return [String]
|
135
|
+
def generate_auth_url(path, params, accepts_client_id)
|
136
|
+
# Deterministic ordering through sorting by key.
|
137
|
+
# Useful for tests, and in the future, any caching.
|
138
|
+
if params.kind_of?(Hash)
|
139
|
+
params = params.sort
|
140
|
+
else
|
141
|
+
params = params.dup
|
142
|
+
end
|
143
|
+
|
144
|
+
if accepts_client_id and @client_id and @client_secret
|
145
|
+
params << ["client", @client_id]
|
146
|
+
|
147
|
+
path = [path, self.class.urlencode_params(params)].join("?")
|
148
|
+
sig = self.class.sign_hmac(@client_secret, path)
|
149
|
+
return path + "&signature=" + sig
|
150
|
+
end
|
151
|
+
|
152
|
+
if @key
|
153
|
+
params << ["key", @key]
|
154
|
+
return path + "?" + self.class.urlencode_params(params)
|
155
|
+
end
|
156
|
+
|
157
|
+
raise ArgumentError, "Must provide API key for this API. It does not accept enterprise credentials."
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns a base64-encoded HMAC-SHA1 signature of a given string.
|
161
|
+
#
|
162
|
+
# @param [String] secret The key used for the signature, base64 encoded.
|
163
|
+
# @param [String] payload The payload to sign.
|
164
|
+
#
|
165
|
+
# @return [String]
|
166
|
+
def self.sign_hmac(secret, payload)
|
167
|
+
require 'base64'
|
168
|
+
require 'hmac'
|
169
|
+
require 'hmac-sha1'
|
170
|
+
|
171
|
+
secret = secret.encode('ASCII')
|
172
|
+
payload = payload.encode('ASCII')
|
173
|
+
|
174
|
+
# Decode the private key
|
175
|
+
raw_key = Base64.urlsafe_decode64(secret)
|
176
|
+
|
177
|
+
# Create a signature using the private key and the URL
|
178
|
+
sha1 = HMAC::SHA1.new(raw_key)
|
179
|
+
sha1 << payload
|
180
|
+
raw_signature = sha1.digest()
|
181
|
+
|
182
|
+
# Encode the signature into base64 for url use form.
|
183
|
+
signature = Base64.urlsafe_encode64(raw_signature)
|
184
|
+
return signature
|
185
|
+
end
|
186
|
+
|
187
|
+
# URL encodes the parameters.
|
188
|
+
# @param [Hash, Array<Array>] params The parameters
|
189
|
+
# @return [String]
|
190
|
+
def self.urlencode_params(params)
|
191
|
+
unquote_unreserved(URI.encode_www_form(params))
|
192
|
+
end
|
193
|
+
|
194
|
+
# The unreserved URI characters (RFC 3986)
|
195
|
+
UNRESERVED_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
196
|
+
|
197
|
+
# Un-escape any percent-escape sequences in a URI that are unreserved
|
198
|
+
# characters. This leaves all reserved, illegal and non-ASCII bytes encoded.
|
199
|
+
#
|
200
|
+
# @param [String] uri
|
201
|
+
#
|
202
|
+
# @return [String]
|
203
|
+
def self.unquote_unreserved(uri)
|
204
|
+
parts = uri.split('%')
|
205
|
+
|
206
|
+
(1..parts.length-1).each do |i|
|
207
|
+
h = parts[i][0..1]
|
208
|
+
|
209
|
+
if h.length == 2 and !h.match(/[^A-Za-z0-9]/)
|
210
|
+
c = h.to_i(16).chr
|
211
|
+
|
212
|
+
if UNRESERVED_SET.include?(c)
|
213
|
+
parts[i] = c + parts[i][2..-1]
|
214
|
+
else
|
215
|
+
parts[i] = "%#{parts[i]}"
|
216
|
+
end
|
217
|
+
else
|
218
|
+
parts[i] = "%#{parts[i]}"
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
return parts.join
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
module GoogleMapsService
|
2
|
+
|
3
|
+
# Converts Ruby types to string representations suitable for Maps API server.
|
4
|
+
module Convert
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# Converts a lat/lon pair to a comma-separated string.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# >> GoogleMapsService::Convert.latlng({"lat": -33.8674869, "lng": 151.2069902})
|
11
|
+
# => "-33.867487,151.206990"
|
12
|
+
#
|
13
|
+
# @param [Hash, Array] arg The lat/lon hash or array pair.
|
14
|
+
#
|
15
|
+
# @return [String] Comma-separated lat/lng.
|
16
|
+
#
|
17
|
+
# @raise [ArgumentError] When argument is not lat/lng hash or array.
|
18
|
+
def latlng(arg)
|
19
|
+
return "%f,%f" % normalize_latlng(arg)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Take the various lat/lng representations and return a tuple.
|
23
|
+
#
|
24
|
+
# Accepts various representations:
|
25
|
+
#
|
26
|
+
# 1. Hash with two entries - +lat+ and +lng+
|
27
|
+
# 2. Array or list - e.g. +[-33, 151]+
|
28
|
+
#
|
29
|
+
# @param [Hash, Array] arg The lat/lon hash or array pair.
|
30
|
+
#
|
31
|
+
# @return [Array] Pair of lat and lng array.
|
32
|
+
def normalize_latlng(arg)
|
33
|
+
if arg.kind_of?(Hash)
|
34
|
+
if arg.has_key?(:lat) and arg.has_key?(:lng)
|
35
|
+
return arg[:lat], arg[:lng]
|
36
|
+
end
|
37
|
+
if arg.has_key?(:latitude) and arg.has_key?(:longitude)
|
38
|
+
return arg[:latitude], arg[:longitude]
|
39
|
+
end
|
40
|
+
if arg.has_key?("lat") and arg.has_key?("lng")
|
41
|
+
return arg["lat"], arg["lng"]
|
42
|
+
end
|
43
|
+
if arg.has_key?("latitude") and arg.has_key?("longitude")
|
44
|
+
return arg["latitude"], arg["longitude"]
|
45
|
+
end
|
46
|
+
elsif arg.kind_of?(Array)
|
47
|
+
return arg[0], arg[1]
|
48
|
+
end
|
49
|
+
|
50
|
+
raise ArgumentError, "Expected a lat/lng Hash or Array, but got #{arg.class}"
|
51
|
+
end
|
52
|
+
|
53
|
+
# If arg is list-like, then joins it with sep.
|
54
|
+
#
|
55
|
+
# @param [String] sep Separator string.
|
56
|
+
# @param [Array, String] arg Value to coerce into a list.
|
57
|
+
#
|
58
|
+
# @return [String]
|
59
|
+
def join_list(sep, arg)
|
60
|
+
return as_list(arg).join(sep)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Coerces arg into a list. If arg is already list-like, returns arg.
|
64
|
+
# Otherwise, returns a one-element list containing arg.
|
65
|
+
#
|
66
|
+
# @param [Object] arg
|
67
|
+
#
|
68
|
+
# @return [Array]
|
69
|
+
def as_list(arg)
|
70
|
+
if arg.kind_of?(Array)
|
71
|
+
return arg
|
72
|
+
end
|
73
|
+
return [arg]
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
# Converts the value into a unix time (seconds since unix epoch).
|
78
|
+
#
|
79
|
+
# @example
|
80
|
+
# >> GoogleMapsService::Convert.time(datetime.now())
|
81
|
+
# => "1409810596"
|
82
|
+
#
|
83
|
+
# @param [Time, Date, DateTime, Integer] arg The time.
|
84
|
+
#
|
85
|
+
# @return [String] String representation of epoch time
|
86
|
+
def time(arg)
|
87
|
+
if arg.kind_of?(DateTime)
|
88
|
+
arg = arg.to_time
|
89
|
+
end
|
90
|
+
return arg.to_i.to_s
|
91
|
+
end
|
92
|
+
|
93
|
+
# Converts a dict of components to the format expected by the Google Maps
|
94
|
+
# server.
|
95
|
+
#
|
96
|
+
# @example
|
97
|
+
# >> GoogleMapsService::Convert.components({"country": "US", "postal_code": "94043"})
|
98
|
+
# => "country:US|postal_code:94043"
|
99
|
+
#
|
100
|
+
# @param [Hash] arg The component filter.
|
101
|
+
#
|
102
|
+
# @return [String]
|
103
|
+
def components(arg)
|
104
|
+
if arg.kind_of?(Hash)
|
105
|
+
arg = arg.sort.map { |k, v| "#{k}:#{v}" }
|
106
|
+
return arg.join("|")
|
107
|
+
end
|
108
|
+
|
109
|
+
raise ArgumentError, "Expected a Hash for components, but got #{arg.class}"
|
110
|
+
end
|
111
|
+
|
112
|
+
# Converts a lat/lon bounds to a comma- and pipe-separated string.
|
113
|
+
#
|
114
|
+
# Accepts two representations:
|
115
|
+
#
|
116
|
+
# 1. String: pipe-separated pair of comma-separated lat/lon pairs.
|
117
|
+
# 2. Hash with two entries - "southwest" and "northeast". See {.latlng}
|
118
|
+
# for information on how these can be represented.
|
119
|
+
#
|
120
|
+
# For example:
|
121
|
+
#
|
122
|
+
# >> sydney_bounds = {
|
123
|
+
# ?> "northeast": {
|
124
|
+
# ?> "lat": -33.4245981,
|
125
|
+
# ?> "lng": 151.3426361
|
126
|
+
# ?> },
|
127
|
+
# ?> "southwest": {
|
128
|
+
# ?> "lat": -34.1692489,
|
129
|
+
# ?> "lng": 150.502229
|
130
|
+
# ?> }
|
131
|
+
# ?> }
|
132
|
+
# >> GoogleMapsService::Convert.bounds(sydney_bounds)
|
133
|
+
# => '-34.169249,150.502229|-33.424598,151.342636'
|
134
|
+
#
|
135
|
+
# @param [Hash] arg The bounds.
|
136
|
+
#
|
137
|
+
# @return [String]
|
138
|
+
def bounds(arg)
|
139
|
+
if arg.kind_of?(Hash)
|
140
|
+
if arg.has_key?("southwest") && arg.has_key?("northeast")
|
141
|
+
return "#{latlng(arg["southwest"])}|#{latlng(arg["northeast"])}"
|
142
|
+
elsif arg.has_key?(:southwest) && arg.has_key?(:northeast)
|
143
|
+
return "#{latlng(arg[:southwest])}|#{latlng(arg[:northeast])}"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
raise ArgumentError, "Expected a bounds (southwest/northeast) Hash, but got #{arg.class}"
|
148
|
+
end
|
149
|
+
|
150
|
+
# Decodes a Polyline string into a list of lat/lng hash.
|
151
|
+
#
|
152
|
+
# See the developer docs for a detailed description of this encoding:
|
153
|
+
# https://developers.google.com/maps/documentation/utilities/polylinealgorithm
|
154
|
+
#
|
155
|
+
# @param [String] polyline An encoded polyline
|
156
|
+
#
|
157
|
+
# @return [Array] Array of hash with lat/lng keys
|
158
|
+
def decode_polyline(polyline)
|
159
|
+
points = []
|
160
|
+
index = lat = lng = 0
|
161
|
+
|
162
|
+
while index < polyline.length
|
163
|
+
result = 1
|
164
|
+
shift = 0
|
165
|
+
while true
|
166
|
+
b = polyline[index].ord - 63 - 1
|
167
|
+
index += 1
|
168
|
+
result += b << shift
|
169
|
+
shift += 5
|
170
|
+
break if b < 0x1f
|
171
|
+
end
|
172
|
+
lat += (result & 1) != 0 ? (~result >> 1) : (result >> 1)
|
173
|
+
|
174
|
+
result = 1
|
175
|
+
shift = 0
|
176
|
+
while true
|
177
|
+
b = polyline[index].ord - 63 - 1
|
178
|
+
index += 1
|
179
|
+
result += b << shift
|
180
|
+
shift += 5
|
181
|
+
break if b < 0x1f
|
182
|
+
end
|
183
|
+
lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1)
|
184
|
+
|
185
|
+
points << {lat: lat * 1e-5, lng: lng * 1e-5}
|
186
|
+
end
|
187
|
+
|
188
|
+
points
|
189
|
+
end
|
190
|
+
|
191
|
+
# Encodes a list of points into a polyline string.
|
192
|
+
#
|
193
|
+
# See the developer docs for a detailed description of this encoding:
|
194
|
+
# https://developers.google.com/maps/documentation/utilities/polylinealgorithm
|
195
|
+
#
|
196
|
+
# @param [Array<Hash>, Array<Array>] points A list of lat/lng pairs.
|
197
|
+
#
|
198
|
+
# @return [String]
|
199
|
+
def encode_polyline(points)
|
200
|
+
last_lat = last_lng = 0
|
201
|
+
result = ""
|
202
|
+
|
203
|
+
points.each do |point|
|
204
|
+
ll = normalize_latlng(point)
|
205
|
+
lat = (ll[0] * 1e5).round.to_i
|
206
|
+
lng = (ll[1] * 1e5).round.to_i
|
207
|
+
d_lat = lat - last_lat
|
208
|
+
d_lng = lng - last_lng
|
209
|
+
|
210
|
+
[d_lat, d_lng].each do |v|
|
211
|
+
v = (v < 0) ? ~(v << 1) : (v << 1)
|
212
|
+
while v >= 0x20
|
213
|
+
result += ((0x20 | (v & 0x1f)) + 63).chr
|
214
|
+
v >>= 5
|
215
|
+
end
|
216
|
+
result += (v + 63).chr
|
217
|
+
end
|
218
|
+
|
219
|
+
last_lat = lat
|
220
|
+
last_lng = lng
|
221
|
+
end
|
222
|
+
|
223
|
+
result
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
end
|