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.
@@ -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