googlemaps-services 1.0.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/lib/googlemaps/services.rb +1 -0
- data/lib/googlemaps/services/client.rb +254 -0
- data/lib/googlemaps/services/directions.rb +119 -0
- data/lib/googlemaps/services/distancematrix.rb +98 -0
- data/lib/googlemaps/services/elevation.rb +56 -0
- data/lib/googlemaps/services/exceptions.rb +55 -0
- data/lib/googlemaps/services/geocoding.rb +106 -0
- data/lib/googlemaps/services/places.rb +245 -0
- data/lib/googlemaps/services/roads.rb +115 -0
- data/lib/googlemaps/services/timezone.rb +43 -0
- data/lib/googlemaps/services/util.rb +300 -0
- data/lib/googlemaps/services/version.rb +5 -0
- metadata +99 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
require "googlemaps/services/exceptions"
|
2
|
+
require "googlemaps/services/util"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module GoogleMaps
|
6
|
+
module Services
|
7
|
+
|
8
|
+
$ROADS_BASE_URL = "https://roads.googleapis.com"
|
9
|
+
|
10
|
+
# Performs requests to the Google Maps Roads API.
|
11
|
+
class Roads
|
12
|
+
include GoogleMaps::Services::Exceptions
|
13
|
+
|
14
|
+
# Extracts a result from a Roads API HTTP response.
|
15
|
+
@@_roads_extract = Proc.new { |resp|
|
16
|
+
status_code = resp.code.to_i
|
17
|
+
begin
|
18
|
+
body = JSON.parse(resp.body)
|
19
|
+
rescue JSON::ParserError
|
20
|
+
raise APIError.new(status_code), "Received malformed response."
|
21
|
+
end
|
22
|
+
|
23
|
+
if body.key?("error")
|
24
|
+
error = body["error"]
|
25
|
+
status = error["status"]
|
26
|
+
|
27
|
+
if status == "RESOURCE_EXHAUSTED"
|
28
|
+
raise RetriableRequest
|
29
|
+
end
|
30
|
+
|
31
|
+
if error.key?("message")
|
32
|
+
raise APIError.new(status), error["message"]
|
33
|
+
else
|
34
|
+
raise APIError.new(status)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
if status_code != 200
|
39
|
+
raise HTTPError.new(status_code)
|
40
|
+
end
|
41
|
+
body
|
42
|
+
}
|
43
|
+
|
44
|
+
# @return [Symbol] the HTTP client.
|
45
|
+
attr_accessor :client
|
46
|
+
|
47
|
+
def initialize(client)
|
48
|
+
self.client = client
|
49
|
+
end
|
50
|
+
|
51
|
+
# Snaps a path to the most likely roads travelled. Takes up to 100 GPS points collected along a route,
|
52
|
+
# and returns a similar set of data with the points snapped to the most likely roads the vehicle was traveling along.
|
53
|
+
#
|
54
|
+
# @param [Array] path The path to be snapped.
|
55
|
+
# @param [TrueClass, FalseClass] interpolate Whether to interpolate a path to include all points forming the full road-geometry.
|
56
|
+
# When true, additional interpolated points will also be returned, resulting in a path
|
57
|
+
# that smoothly follows the geometry of the road, even around corners and through tunnels.
|
58
|
+
# Interpolated paths may contain more points than the original path.
|
59
|
+
#
|
60
|
+
# @return [Array] Array of snapped points.
|
61
|
+
def snap_to_roads(path:, interpolate: false)
|
62
|
+
params = { "path" => Convert.piped_location(path) }
|
63
|
+
|
64
|
+
if interpolate
|
65
|
+
params["interpolate"] = "true"
|
66
|
+
end
|
67
|
+
|
68
|
+
self.client.get(url: "/v1/snapToRoads", params: params, base_url: $ROADS_BASE_URL,
|
69
|
+
accepts_clientid: false, extract_body: @@_roads_extract)["snappedPoints"]
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the posted speed limit (in km/h) for given road segments.
|
73
|
+
#
|
74
|
+
# @param [Array] place_ids The Place ID of the road segment. Place IDs are returned by the snap_to_roads function.
|
75
|
+
# You can pass up to 100 Place IDs.
|
76
|
+
#
|
77
|
+
# @return [Array] Array of speed limits.
|
78
|
+
def speed_limits(place_ids:)
|
79
|
+
raise StandardError, "#{__method__.to_s} expected an Array for place_ids." unless place_ids.is_a? Array
|
80
|
+
|
81
|
+
params = { "placeId" => place_ids }
|
82
|
+
|
83
|
+
self.client.get(url: "/v1/speedLimits", params: params, base_url: $ROADS_BASE_URL,
|
84
|
+
accepts_clientid: false, extract_body: @@_roads_extract)["speedLimits"]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns the posted speed limit (in km/h) for given road segments.
|
88
|
+
# The provided points will first be snapped to the most likely roads the vehicle was traveling along.
|
89
|
+
#
|
90
|
+
# @param [Array] path The path of points to be snapped.
|
91
|
+
#
|
92
|
+
# @return [Hash] Hash with an array of speed limits and an array of the snapped points.
|
93
|
+
def snapped_speed_limits(path:)
|
94
|
+
params = { "path" => Convert.piped_location(path) }
|
95
|
+
|
96
|
+
self.client.get(url: "/v1/speedLimits", params: params, base_url: $ROADS_BASE_URL,
|
97
|
+
accepts_clientid: false, extract_body: @@_roads_extract)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Find the closest road segments for each point.
|
101
|
+
# Takes up to 100 independent coordinates, and returns the closest road segment for each point.
|
102
|
+
# The points passed do not need to be part of a continuous path.
|
103
|
+
#
|
104
|
+
# @param [Array] points The points for which the nearest road segments are to be located.
|
105
|
+
#
|
106
|
+
# @return [Array] An array of snapped points.
|
107
|
+
def nearest_roads(points:)
|
108
|
+
params = { "points" => Convert.piped_location(points) }
|
109
|
+
|
110
|
+
self.client.get(url: "/v1/nearestRoads", params: params, base_url: $ROADS_BASE_URL,
|
111
|
+
accepts_clientid: false, extract_body: @@_roads_extract)["snappedPoints"]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "googlemaps/services/util"
|
2
|
+
|
3
|
+
module GoogleMaps
|
4
|
+
module Services
|
5
|
+
|
6
|
+
# Performs requests to the Google Maps Timezone API.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# timezone = GoogleMaps::Services::Timezone(client)
|
10
|
+
# result = timezone.query(location: "38.908133,-77.047119")
|
11
|
+
class Timezone
|
12
|
+
# @return [Symbol] The HTTP client.
|
13
|
+
attr_accessor :client
|
14
|
+
|
15
|
+
def initialize(client)
|
16
|
+
self.client = client
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get time zone for a location on the earth, as well as that location's time offset from UTC.
|
20
|
+
#
|
21
|
+
# @param [String, Hash] location The lat/lng value representing the location to look up.
|
22
|
+
# @param [Integer, Time, Date] timestamp Specifies the desired time as seconds since midnight, January 1, 1970 UTC.
|
23
|
+
# The Time Zone API uses the timestamp to determine whether or not Daylight Savings should be applied.
|
24
|
+
# Times before 1970 can be expressed as negative values. Optional. Defaults to Util.current_utctime.
|
25
|
+
# @param [String] language The language in which to return results.
|
26
|
+
#
|
27
|
+
# @return [Hash] Valid JSON or XML response.
|
28
|
+
def query(location:, timestamp: nil, language: nil)
|
29
|
+
params = {
|
30
|
+
"location" => Convert.to_latlng(location),
|
31
|
+
"timestamp" => Convert.unix_time(timestamp || Util.current_utctime)
|
32
|
+
}
|
33
|
+
|
34
|
+
if language
|
35
|
+
params["language"] = language
|
36
|
+
end
|
37
|
+
|
38
|
+
self.client.get(url: "/maps/api/timezone/json", params: params)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,300 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "openssl"
|
3
|
+
require "base64"
|
4
|
+
require "date"
|
5
|
+
require "erb"
|
6
|
+
|
7
|
+
module GoogleMaps
|
8
|
+
module Services
|
9
|
+
|
10
|
+
module HashDot
|
11
|
+
def method_missing(meth, *args, &block)
|
12
|
+
if has_key?(meth.to_s)
|
13
|
+
self[meth.to_s]
|
14
|
+
else
|
15
|
+
raise NoMethodError, 'undefined method #{meth} for #{self}'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Performs Array boxing.
|
21
|
+
class ArrayBox
|
22
|
+
# Wrap its argument in an array unless it is already an array or (array-like).
|
23
|
+
#
|
24
|
+
# @param [Object] object Object to wrap.
|
25
|
+
# @example Wrap any Object in a array
|
26
|
+
# ArrayBox.wrap(nil) # []
|
27
|
+
# ArrayBox.wrap([1, 2, 3]) # [1, 2, 3]
|
28
|
+
# ArrayBox.wrap(1) # [1]
|
29
|
+
#
|
30
|
+
# @return [Array] an array.
|
31
|
+
def self.wrap(object)
|
32
|
+
if object.nil?
|
33
|
+
[]
|
34
|
+
elsif object.respond_to? :to_ary
|
35
|
+
object.to_ary || [object]
|
36
|
+
else
|
37
|
+
[object]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Set of utility methods.
|
43
|
+
class Util
|
44
|
+
# Returns the current time
|
45
|
+
#
|
46
|
+
# Rails extends the Time and DateTime objects, and includes the "current" property
|
47
|
+
# for retrieving the time the Rails environment is set to (default = UTC), as opposed to
|
48
|
+
# the server time (Could be anything).
|
49
|
+
#
|
50
|
+
# @return [Time] a new Time object for the current time.
|
51
|
+
def self.current_time
|
52
|
+
(Time.respond_to? :current) ? Time.current : Time.now
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the current UTC time.
|
56
|
+
#
|
57
|
+
# @return [Time] a new Time object for the current UTC (GMT) time.
|
58
|
+
def self.current_utctime
|
59
|
+
(Time.respond_to? :current) ? Time.current.utc : Time.now.utc
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the current time in unix format (seconds since unix epoch).
|
63
|
+
#
|
64
|
+
# @return [Integer] number of seconds since unix epoch.
|
65
|
+
def self.current_unix_time
|
66
|
+
current_time.to_i
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a base64-encoded HMAC-SHA1 signature of a given string.
|
70
|
+
#
|
71
|
+
# @param [String] secret The key used for the signature, base64 encoded.
|
72
|
+
# @param [String] payload The payload to sign.
|
73
|
+
#
|
74
|
+
# @return [String] a base64-encoded signature string.
|
75
|
+
def self.sign_hmac(secret, payload)
|
76
|
+
payload = payload.encode('ascii')
|
77
|
+
secret = secret.encode('ascii')
|
78
|
+
digest = OpenSSL::Digest.new('sha1')
|
79
|
+
sig = OpenSSL::HMAC.digest(digest, Base64.urlsafe_decode64(secret), payload)
|
80
|
+
return Base64.urlsafe_encode64(sig).encode('utf-8')
|
81
|
+
end
|
82
|
+
|
83
|
+
# URL encodes the parameters.
|
84
|
+
#
|
85
|
+
# @param [Hash] params The parameters.
|
86
|
+
#
|
87
|
+
# @return [String] URL-encoded string.
|
88
|
+
def self.urlencode_params(params)
|
89
|
+
URI.encode_www_form(params)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Converts Ruby types to string representations suitable for Google Maps API server.
|
94
|
+
class Convert
|
95
|
+
# Converts the value into a unix time (seconds since unix epoch).
|
96
|
+
#
|
97
|
+
# @param [Integer, Time, Date] val value to convert to unix time format.
|
98
|
+
# @example converts value to unix time
|
99
|
+
# Convert.unix_time(1472809264)
|
100
|
+
# Convert.unix_time(Time.now)
|
101
|
+
# Convert.unix_time(Date.parse("2016-09-02"))
|
102
|
+
#
|
103
|
+
# @return [String] seconds since unix epoch.
|
104
|
+
def self.unix_time(val)
|
105
|
+
if val.is_a? Integer
|
106
|
+
val.to_s
|
107
|
+
elsif val.is_a? Time
|
108
|
+
val.to_i.to_s
|
109
|
+
elsif val.is_a? Date
|
110
|
+
val.to_time.to_i.to_s
|
111
|
+
else
|
112
|
+
raise TypeError, "#{__method__.to_s} expected value to be Integer, Time or Date."
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Converts a lat/lng value to a comma-separated string.
|
117
|
+
#
|
118
|
+
# @param [String, Hash] arg The lat/lng value.
|
119
|
+
# @example Convert lat/lng value to comma-separated string
|
120
|
+
# Convert.to_latlng("45.458878,-39.56487")
|
121
|
+
# Convert.to_latlng("Brussels")
|
122
|
+
# Convert.to_latlng({ :lat => 45.458878, :lng => -39.56487 })
|
123
|
+
#
|
124
|
+
# @return [String] comma-separated string.
|
125
|
+
def self.to_latlng(arg)
|
126
|
+
if arg.is_a? String
|
127
|
+
arg
|
128
|
+
elsif arg.is_a? Hash
|
129
|
+
"#{self.format_float(arg[:lat])},#{self.format_float(arg[:lng])}"
|
130
|
+
else
|
131
|
+
raise TypeError, "#{__method__.to_s} expected location to be String or Hash."
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Formats a float value to as short as possible.
|
136
|
+
#
|
137
|
+
# @param [Float] arg The lat or lng float.
|
138
|
+
# @example Formats the lat or lng float
|
139
|
+
# Convert.format_float(45.1289700)
|
140
|
+
#
|
141
|
+
# @return [String] formatted value of lat or lng float
|
142
|
+
def self.format_float(arg)
|
143
|
+
arg.to_s.chomp("0").chomp(".")
|
144
|
+
end
|
145
|
+
|
146
|
+
# Joins an array of locations into a pipe separated string, handling
|
147
|
+
# the various formats supported for lat/lng values.
|
148
|
+
#
|
149
|
+
# @param [Array] arg Array of locations.
|
150
|
+
# @example Joins the locations array to pipe-separated string
|
151
|
+
# arr = [{ :lat => -33.987486, :lng => 151.217990}, "Brussels"]
|
152
|
+
# Convert.piped_location(arr) # '-33.987486,151.21799|Brussels'
|
153
|
+
#
|
154
|
+
# @return [String] pipe-separated string.
|
155
|
+
def self.piped_location(arg)
|
156
|
+
raise TypeError, "#{__method__.to_s} expected argument to be an Array." unless arg.instance_of? ::Array
|
157
|
+
arg.map { |location| to_latlng(location) }.join("|")
|
158
|
+
end
|
159
|
+
|
160
|
+
# If arg is array-like, then joins it with sep
|
161
|
+
#
|
162
|
+
# @param [String] sep Separator string.
|
163
|
+
# @param [Object] arg Object to coerce into an array.
|
164
|
+
#
|
165
|
+
# @return [String] a joined string.
|
166
|
+
def self.join_array(sep, arg)
|
167
|
+
ArrayBox.wrap(arg).join(sep)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Converts a Hash of components to the format expect by the Google Maps API server.
|
171
|
+
#
|
172
|
+
# @param [Hash] arg The component filter.
|
173
|
+
# @example Converts a components hash to server-friendly string
|
174
|
+
# c = {"country" => ["US", "BE"], "postal_code" => 7452}
|
175
|
+
# Convert.components(c) # 'country:BE|country:US|postal_code:7452'
|
176
|
+
#
|
177
|
+
# @return [String] Server-friendly string representation
|
178
|
+
def self.components(arg)
|
179
|
+
raise TypeError, "#{__method__.to_s} expected a Hash of components." unless arg.is_a? Hash
|
180
|
+
|
181
|
+
arg.map { |c, val|
|
182
|
+
ArrayBox.wrap(val).map {|elem| "#{c}:#{elem}"}
|
183
|
+
.sort_by(&:downcase)
|
184
|
+
}.join("|")
|
185
|
+
end
|
186
|
+
|
187
|
+
# Converts a lat/lng bounds to a comma- and pipe-separated string.
|
188
|
+
#
|
189
|
+
# @param [Hash] arg The bounds. A hash with two entries - "southwest" and "northeast".
|
190
|
+
#
|
191
|
+
# @example Converts lat/lng bounds to comma- and pipe-separated string
|
192
|
+
# sydney_bounds = {
|
193
|
+
# :northeast => { :lat => -33.4245981, :lng => 151.3426361 },
|
194
|
+
# :southwest => { :lat => -34.1692489, :lng => 150.502229 }
|
195
|
+
# }
|
196
|
+
# Convert.bounds(sydney_bounds) # '-34.169249,150.502229|-33.424598,151.342636'
|
197
|
+
#
|
198
|
+
# @return [String] comma- and pipe-separated string.
|
199
|
+
def self.bounds(arg)
|
200
|
+
raise TypeError, "#{__method__.to_s} expected a Hash of bounds." unless arg.is_a? Hash
|
201
|
+
"#{to_latlng(arg[:southwest])}|#{to_latlng(arg[:northeast])}"
|
202
|
+
end
|
203
|
+
|
204
|
+
# Encodes an array of points into a polyline string.
|
205
|
+
#
|
206
|
+
# See the developer docs for a detailed description of this algorithm:
|
207
|
+
# https://developers.google.com/maps/documentation/utilities/polylinealgorithm
|
208
|
+
#
|
209
|
+
# @param [Array] points Array of lat/lng hashes.
|
210
|
+
#
|
211
|
+
# @return [String] a polyline string.
|
212
|
+
def self.encode_polyline(points)
|
213
|
+
raise TypeError, "#{__method__.to_s} expected an Array of points." unless points.is_a? Array
|
214
|
+
last_lat, last_lng = 0, 0
|
215
|
+
result = ""
|
216
|
+
points.each { |point|
|
217
|
+
lat = (point[:lat] * 1e5).round.to_i
|
218
|
+
lng = (point[:lng] * 1e5).round.to_i
|
219
|
+
delta_lat = lat - last_lat
|
220
|
+
delta_lng = lng - last_lng
|
221
|
+
|
222
|
+
[delta_lat, delta_lng].each { |val|
|
223
|
+
val = (val < 0) ? ~(val << 1) : (val << 1)
|
224
|
+
while val >= 0x20
|
225
|
+
result += ((0x20 | (val & 0x1f)) + 63).chr
|
226
|
+
val >>= 5
|
227
|
+
end
|
228
|
+
result += (val + 63).chr
|
229
|
+
}
|
230
|
+
|
231
|
+
last_lat = lat
|
232
|
+
last_lng = lng
|
233
|
+
}
|
234
|
+
result
|
235
|
+
end
|
236
|
+
|
237
|
+
# Decodes a Polyline string into an array of lat/lng hashes.
|
238
|
+
#
|
239
|
+
# See the developer docs for a detailed description of this algorithm:
|
240
|
+
# https://developers.google.com/maps/documentation/utilities/polylinealgorithm
|
241
|
+
#
|
242
|
+
# @param [String] polyline An encoded polyline.
|
243
|
+
#
|
244
|
+
# @return [Array] an array of lat/lng hashes.
|
245
|
+
def self.decode_polyline(polyline)
|
246
|
+
raise TypeError, "#{__method__.to_s} expected an argument of type String." unless polyline.is_a? String
|
247
|
+
points = Array.new
|
248
|
+
index, lat, lng = 0, 0, 0
|
249
|
+
|
250
|
+
while index < polyline.length
|
251
|
+
result = 1
|
252
|
+
shift = 0
|
253
|
+
while true
|
254
|
+
b = polyline[index].ord - 63 - 1
|
255
|
+
index += 1
|
256
|
+
result += (b << shift)
|
257
|
+
shift += 5
|
258
|
+
if b < 0x1f
|
259
|
+
break
|
260
|
+
end
|
261
|
+
end
|
262
|
+
lat += (result & 1) != 0 ? (~result >> 1) : (result >> 1)
|
263
|
+
|
264
|
+
result = 1
|
265
|
+
shift = 0
|
266
|
+
while true
|
267
|
+
b = polyline[index].ord - 63 - 1
|
268
|
+
index += 1
|
269
|
+
result += (b << shift)
|
270
|
+
shift += 5
|
271
|
+
if b < 0x1f
|
272
|
+
break
|
273
|
+
end
|
274
|
+
end
|
275
|
+
lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1)
|
276
|
+
|
277
|
+
points.push({:lat => lat * 1e-5, :lng => lng * 1e-5})
|
278
|
+
end
|
279
|
+
points
|
280
|
+
end
|
281
|
+
|
282
|
+
# Returns the shortest representation of the given locations.
|
283
|
+
#
|
284
|
+
# The Elevation API limits requests to 2000 characters, and accepts
|
285
|
+
# multiple locations either as pipe-delimited lat/lng values, or
|
286
|
+
# an encoded polyline, so we determine which is shortest and use it.
|
287
|
+
#
|
288
|
+
# @param [Array] locations The lat/lng array.
|
289
|
+
#
|
290
|
+
# @return [String] shortest path.
|
291
|
+
def self.shortest_path(locations)
|
292
|
+
raise TypeError, "#{__method__.to_s} expected an Array of locations." unless locations.is_a? Array
|
293
|
+
encoded = "enc:#{encode_polyline(locations)}"
|
294
|
+
unencoded = piped_location(locations)
|
295
|
+
encoded.length < unencoded.length ? encoded : unencoded
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
end
|
300
|
+
end
|