googlemaps-services 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/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
|