googlemaps-services 1.0.0

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