google_maps_apis 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,294 @@
1
+ require 'faraday'
2
+ #require 'faraday/retry'
3
+ require 'multi_json'
4
+ require 'thread'
5
+
6
+ require 'google_maps_apis/errors'
7
+ require 'google_maps_apis/convert'
8
+ require 'google_maps_apis/url'
9
+ require 'google_maps_apis/services/directions'
10
+ require 'google_maps_apis/services/distance_matrix'
11
+ require 'google_maps_apis/services/elevation'
12
+ require 'google_maps_apis/services/geocoding'
13
+ require 'google_maps_apis/services/roads'
14
+ require 'google_maps_apis/services/time_zone'
15
+ require 'google_maps_apis/services/places'
16
+
17
+ module GoogleMapsApis
18
+
19
+ # Core client functionality, common across all API requests (including performing
20
+ # HTTP requests).
21
+ class Client
22
+ # Default Google Maps Web Service base endpoints
23
+ DEFAULT_BASE_URL = 'https://maps.googleapis.com'
24
+
25
+ # Errors those could be retriable.
26
+ RETRIABLE_ERRORS = [GoogleMapsApis::Error::ServerError, GoogleMapsApis::Error::RateLimitError]
27
+
28
+ include GoogleMapsApis::Services::Directions
29
+ include GoogleMapsApis::Services::DistanceMatrix
30
+ include GoogleMapsApis::Services::Elevation
31
+ include GoogleMapsApis::Services::Geocoding
32
+ include GoogleMapsApis::Services::Roads
33
+ include GoogleMapsApis::Services::TimeZone
34
+ include GoogleMapsApis::Services::Places
35
+
36
+ # Secret key for accessing Google Maps Web Service.
37
+ # Can be obtained at https://developers.google.com/maps/documentation/geocoding/get-api-key#key.
38
+ # @return [String]
39
+ attr_accessor :key
40
+
41
+ # Client id for using Maps API for Work services.
42
+ # @return [String]
43
+ attr_accessor :client_id
44
+
45
+ # Client secret for using Maps API for Work services.
46
+ # @return [String]
47
+ attr_accessor :client_secret
48
+
49
+ # Timeout across multiple retriable requests, in seconds.
50
+ # @return [Integer]
51
+ attr_accessor :retry_timeout
52
+
53
+ # Number of queries per second permitted.
54
+ # If the rate limit is reached, the client will sleep for
55
+ # the appropriate amount of time before it runs the current query.
56
+ # @return [Integer]
57
+ attr_reader :queries_per_second
58
+
59
+ # Construct Google Maps Web Service API client.
60
+ #
61
+ # You can configure `Hurley::Client` through `request_options` and `ssl_options` parameters.
62
+ # You can also directly get the `Hurley::Client` object via {#client} method.
63
+ #
64
+ # @example Setup API keys
65
+ # gmaps = GoogleMapsApis::Client.new(key: 'Add your key here')
66
+ #
67
+ # @example Setup client IDs
68
+ # gmaps = GoogleMapsApis::Client.new(
69
+ # client_id: 'Add your client id here',
70
+ # client_secret: 'Add your client secret here'
71
+ # )
72
+ #
73
+ # @example Setup time out and QPS limit
74
+ # gmaps = GoogleMapsApis::Client.new(
75
+ # key: 'Add your key here',
76
+ # retry_timeout: 20,
77
+ # queries_per_second: 10
78
+ # )
79
+ #
80
+ # @example Request behind proxy
81
+ # request_options = Hurley::RequestOptions.new
82
+ # request_options.proxy = Hurley::Url.parse 'http://user:password@proxy.example.com:3128'
83
+ #
84
+ # gmaps = GoogleMapsApis::Client.new(
85
+ # key: 'Add your key here',
86
+ # request_options: request_options
87
+ # )
88
+ #
89
+ # @example Using Excon and Http Cache
90
+ # require 'hurley-excon' # https://github.com/lostisland/hurley-excon
91
+ # require 'hurley/http_cache' # https://github.com/plataformatec/hurley-http-cache
92
+ #
93
+ # gmaps = GoogleMapsApis::Client.new(
94
+ # key: 'Add your key here',
95
+ # connection: Hurley::HttpCache.new(HurleyExcon::Connection.new)
96
+ # )
97
+ #
98
+ # @option options [String] :key Secret key for accessing Google Maps Web Service.
99
+ # Can be obtained at https://developers.google.com/maps/documentation/geocoding/get-api-key#key.
100
+ # @option options [String] :client_id Client id for using Maps API for Work services.
101
+ # @option options [String] :client_secret Client secret for using Maps API for Work services.
102
+ # @option options [Integer] :retry_timeout Timeout across multiple retriable requests, in seconds.
103
+ # @option options [Integer] :queries_per_second Number of queries per second permitted.
104
+ #
105
+ # @option options [Hurley::RequestOptions] :request_options HTTP client request options.
106
+ # See https://github.com/lostisland/hurley/blob/master/lib/hurley/options.rb.
107
+ # @option options [Hurley::SslOptions] :ssl_options HTTP client SSL options.
108
+ # See https://github.com/lostisland/hurley/blob/master/lib/hurley/options.rb.
109
+ # @option options [Object] :connection HTTP client connection.
110
+ # By default, the default Hurley's HTTP client connection (Net::Http) will be used.
111
+ # See https://github.com/lostisland/hurley/blob/master/README.md#connections.
112
+ def initialize(**options)
113
+ [:key, :client_id, :client_secret,
114
+ :retry_timeout, :queries_per_second,
115
+ :request_options, :ssl_options, :connection].each do |key|
116
+ self.instance_variable_set("@#{key}".to_sym, options[key] || GoogleMapsApis.instance_variable_get("@#{key}"))
117
+ end
118
+
119
+ initialize_query_tickets
120
+ end
121
+
122
+ # Get the current HTTP client.
123
+ # @return [Faraday::Client]
124
+ def client
125
+ @client ||= new_client
126
+ end
127
+
128
+ protected
129
+
130
+ # Initialize QPS queue. QPS queue is a "tickets" for calling API
131
+ def initialize_query_tickets
132
+ if @queries_per_second
133
+ @qps_queue = SizedQueue.new @queries_per_second
134
+ @queries_per_second.times do
135
+ @qps_queue << 0
136
+ end
137
+ end
138
+ end
139
+
140
+ # Create a new HTTP client.
141
+ # @return [Faraday::Connection]
142
+ def new_client
143
+ client = Faraday.new(DEFAULT_BASE_URL)
144
+ #client.request_options.query_class = Hurley::Query::Flat
145
+ #client.request_options.redirection_limit = 0
146
+ client.headers[:user_agent] = user_agent
147
+
148
+ #client.connection = @connection if @connection
149
+ #@request_options.each_pair {|key, value| client.request_options[key] = value } if @request_options
150
+ #@ssl_options.each_pair {|key, value| client.ssl_options[key] = value } if @ssl_options
151
+
152
+ client
153
+ end
154
+
155
+ # Build the user agent header
156
+ # @return [String]
157
+ def user_agent
158
+ sprintf('google-maps-services-ruby/%s %s',
159
+ GoogleMapsApis::VERSION,
160
+ GoogleMapsApis::OS_VERSION)
161
+ end
162
+
163
+ # Make API call.
164
+ #
165
+ # @param [String] path Url path.
166
+ # @param [String] params Request parameters.
167
+ # @param [String] base_url Base Google Maps Web Service API endpoint url.
168
+ # @param [Boolean] accepts_client_id Sign the request using API {#keys} instead of {#client_id}.
169
+ # @param [Method] custom_response_decoder Custom method to decode raw API response.
170
+ #
171
+ # @return [Object] Decoded response body.
172
+ def get(path, params, base_url: DEFAULT_BASE_URL, accepts_client_id: true, custom_response_decoder: nil)
173
+ url = base_url + generate_auth_url(path, params, accepts_client_id)
174
+
175
+ #Retriable.retriable timeout: @retry_timeout, on: RETRIABLE_ERRORS do |try|
176
+ #begin
177
+ #request_query_ticket
178
+ response = client.get url
179
+ #ensure
180
+ #release_query_ticket
181
+ #end
182
+
183
+ return custom_response_decoder.call(response) if custom_response_decoder
184
+ decode_response_body(response)
185
+ #end
186
+ end
187
+
188
+ # Get/wait the request "ticket" if QPS is configured.
189
+ # Check for previous request time, it must be more than a second ago before calling new request.
190
+ #
191
+ # @return [void]
192
+ def request_query_ticket
193
+ if @qps_queue
194
+ elapsed_since_earliest = Time.now - @qps_queue.pop
195
+ sleep(1 - elapsed_since_earliest) if elapsed_since_earliest.to_f < 1
196
+ end
197
+ end
198
+
199
+ # Release request "ticket".
200
+ #
201
+ # @return [void]
202
+ def release_query_ticket
203
+ @qps_queue << Time.now if @qps_queue
204
+ end
205
+
206
+ # Returns the path and query string portion of the request URL,
207
+ # first adding any necessary parameters.
208
+ #
209
+ # @param [String] path The path portion of the URL.
210
+ # @param [Hash] params URL parameters.
211
+ # @param [Boolean] accepts_client_id Sign the request using API {#keys} instead of {#client_id}.
212
+ #
213
+ # @return [String]
214
+ def generate_auth_url(path, params, accepts_client_id)
215
+ # Deterministic ordering through sorting by key.
216
+ # Useful for tests, and in the future, any caching.
217
+ if params.kind_of?(Hash)
218
+ params = params.sort
219
+ else
220
+ params = params.dup
221
+ end
222
+
223
+ if accepts_client_id and @client_id and @client_secret
224
+ params << ["client", @client_id]
225
+
226
+ path = [path, GoogleMapsApis::Url.urlencode_params(params)].join("?")
227
+ sig = GoogleMapsApis::Url.sign_hmac(@client_secret, path)
228
+ return path + "&signature=" + sig
229
+ end
230
+
231
+ if @key
232
+ params << ["key", @key]
233
+ return path + "?" + GoogleMapsApis::Url.urlencode_params(params)
234
+ end
235
+
236
+ raise ArgumentError, "Must provide API key for this API. It does not accept enterprise credentials."
237
+ end
238
+
239
+ # Extract and parse body response as hash. Throw an error if there is something wrong with the response.
240
+ #
241
+ # @param [Faraday::Response] response Web API response.
242
+ #
243
+ # @return [Hash] Response body as hash. The hash key will be symbolized.
244
+ def decode_response_body(response)
245
+ check_response_status(response)
246
+ body = MultiJson.load(response.body, :symbolize_keys => true)
247
+ check_body_error(response, body)
248
+ body
249
+ end
250
+
251
+ # Check HTTP response status code. Raise error if the status is not 2xx.
252
+ #
253
+ # @param [Faraday::Response] response Web API response.
254
+ def check_response_status(response)
255
+ case response.status
256
+ when 200..300
257
+ # Do-nothing
258
+ when 301...308
259
+ raise GoogleMapsApis::Error::RedirectError.new(response),
260
+ (GoogleMapsApis::Error::RedirectError::ERRORS_3XX[response.status.to_s] + sprintf('Redirect to %s', response.headers[:location]))
261
+ when 400...409,415,422,429
262
+ raise GoogleMapsApis::Error::ClientError.new(response),
263
+ GoogleMapsApis::Error::ClientError::ERRORS_4XX[response.status.to_s]
264
+ when 410...500
265
+ raise GoogleMapsApis::Error::ClientError.new(response), 'Invalid request'
266
+ when 500..600
267
+ raise GoogleMapsApis::Error::ServerError.new(response), 'Server error'
268
+ end
269
+ end
270
+
271
+ # Check response body for error status.
272
+ #
273
+ # @param [Faraday::Response] response Response object.
274
+ # @param [Hash] body Response body.
275
+ #
276
+ # @return [void]
277
+ def check_body_error(response, body)
278
+ case body[:status]
279
+ when 'OK', 'ZERO_RESULTS'
280
+ # Do-nothing
281
+ when 'OVER_QUERY_LIMIT'
282
+ raise GoogleMapsApis::Error::RateLimitError.new(response), body[:error_message]
283
+ when 'REQUEST_DENIED'
284
+ raise GoogleMapsApis::Error::RequestDeniedError.new(response), body[:error_message]
285
+ when 'INVALID_REQUEST'
286
+ raise GoogleMapsApis::Error::InvalidRequestError.new(response), body[:error_message]
287
+ when 'NOT_FOUND'
288
+ raise GoogleMapsApis::Error::NotFoundError.new(response), (body[:error_message] || 'ADDRESS NOT FOUND')
289
+ else
290
+ raise GoogleMapsApis::Error::ApiError.new(response), body[:error_message]
291
+ end
292
+ end
293
+ end
294
+ end
@@ -0,0 +1,176 @@
1
+ module GoogleMapsApis
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
+ # >> GoogleMapsApis::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
+ lat = arg[:lat] || arg[:latitude] || arg["lat"] || arg["latitude"]
35
+ lng = arg[:lng] || arg[:longitude] || arg["lng"] || arg["longitude"]
36
+ return lat, lng
37
+ elsif arg.kind_of?(Array)
38
+ return arg[0], arg[1]
39
+ end
40
+
41
+ raise ArgumentError, "Expected a lat/lng Hash or Array, but got #{arg.class}"
42
+ end
43
+
44
+ # If arg is list-like, then joins it with sep.
45
+ #
46
+ # @param [String] sep Separator string.
47
+ # @param [Array, String] arg Value to coerce into a list.
48
+ #
49
+ # @return [String]
50
+ def join_list(sep, arg)
51
+ return as_list(arg).join(sep)
52
+ end
53
+
54
+ # Coerces arg into a list. If arg is already list-like, returns arg.
55
+ # Otherwise, returns a one-element list containing arg.
56
+ #
57
+ # @param [Object] arg
58
+ #
59
+ # @return [Array]
60
+ def as_list(arg)
61
+ if arg.kind_of?(Array)
62
+ return arg
63
+ end
64
+ return [arg]
65
+ end
66
+
67
+
68
+ # Converts the value into a unix time (seconds since unix epoch).
69
+ #
70
+ # @example
71
+ # >> GoogleMapsApis::Convert.time(datetime.now())
72
+ # => "1409810596"
73
+ #
74
+ # @param [Time, Date, DateTime, Integer] arg The time.
75
+ #
76
+ # @return [String] String representation of epoch time
77
+ def time(arg)
78
+ if arg.kind_of?(DateTime)
79
+ arg = arg.to_time
80
+ end
81
+ return arg.to_i.to_s
82
+ end
83
+
84
+ # Converts a dict of components to the format expected by the Google Maps
85
+ # server.
86
+ #
87
+ # @example
88
+ # >> GoogleMapsApis::Convert.components({"country": "US", "postal_code": "94043"})
89
+ # => "country:US|postal_code:94043"
90
+ #
91
+ # @param [Hash] arg The component filter.
92
+ #
93
+ # @return [String]
94
+ def components(arg)
95
+ if arg.kind_of?(Hash)
96
+ arg = arg.sort.map { |k, v| "#{k}:#{v}" }
97
+ return arg.join("|")
98
+ end
99
+
100
+ raise ArgumentError, "Expected a Hash for components, but got #{arg.class}"
101
+ end
102
+
103
+ # Converts a lat/lon bounds to a comma- and pipe-separated string.
104
+ #
105
+ # Accepts two representations:
106
+ #
107
+ # 1. String: pipe-separated pair of comma-separated lat/lon pairs.
108
+ # 2. Hash with two entries - "southwest" and "northeast". See {.latlng}
109
+ # for information on how these can be represented.
110
+ #
111
+ # For example:
112
+ #
113
+ # >> sydney_bounds = {
114
+ # ?> "northeast": {
115
+ # ?> "lat": -33.4245981,
116
+ # ?> "lng": 151.3426361
117
+ # ?> },
118
+ # ?> "southwest": {
119
+ # ?> "lat": -34.1692489,
120
+ # ?> "lng": 150.502229
121
+ # ?> }
122
+ # ?> }
123
+ # >> GoogleMapsApis::Convert.bounds(sydney_bounds)
124
+ # => '-34.169249,150.502229|-33.424598,151.342636'
125
+ #
126
+ # @param [Hash] arg The bounds.
127
+ #
128
+ # @return [String]
129
+ def bounds(arg)
130
+ if arg.kind_of?(Hash)
131
+ southwest = arg[:southwest] || arg["southwest"]
132
+ northeast = arg[:northeast] || arg["northeast"]
133
+ return "#{latlng(southwest)}|#{latlng(northeast)}"
134
+ end
135
+
136
+ raise ArgumentError, "Expected a bounds (southwest/northeast) Hash, but got #{arg.class}"
137
+ end
138
+
139
+ # Converts a waypoints to the format expected by the Google Maps server.
140
+ #
141
+ # Accept two representation of waypoint:
142
+ #
143
+ # 1. String: Name of place or comma-separated lat/lon pair.
144
+ # 2. Hash/Array: Lat/lon pair.
145
+ #
146
+ # @param [Array, String, Hash] waypoint Path.
147
+ #
148
+ # @return [String]
149
+ def waypoint(waypoint)
150
+ if waypoint.kind_of?(String)
151
+ return waypoint
152
+ end
153
+ return GoogleMapsApis::Convert.latlng(waypoint)
154
+ end
155
+
156
+ # Converts an array of waypoints (path) to the format expected by the Google Maps
157
+ # server.
158
+ #
159
+ # Accept two representation of waypoint:
160
+ #
161
+ # 1. String: Name of place or comma-separated lat/lon pair.
162
+ # 2. Hash/Array: Lat/lon pair.
163
+ #
164
+ # @param [Array, String, Hash] waypoints Path.
165
+ #
166
+ # @return [String]
167
+ def waypoints(waypoints)
168
+ if waypoints.kind_of?(Array) and waypoints.length == 2 and waypoints[0].kind_of?(Numeric) and waypoints[1].kind_of?(Numeric)
169
+ waypoints = [waypoints]
170
+ end
171
+
172
+ waypoints = as_list(waypoints)
173
+ return join_list('|', waypoints.map { |k| waypoint(k) })
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,82 @@
1
+ module GoogleMapsApis
2
+ # Specific Google Maps Service error
3
+ module Error
4
+ # Base error, capable of wrapping another
5
+ class BaseError < StandardError
6
+ # HTTP response object
7
+ # @return [Faraday::Response]
8
+ attr_reader :response
9
+
10
+ # Initialize error
11
+ #
12
+ # @param [Faraday::Response] response HTTP response.
13
+ def initialize(response = nil)
14
+ @response = response
15
+ end
16
+ end
17
+
18
+ # The response redirects to another URL.
19
+ class RedirectError < BaseError
20
+ ERRORS_3XX = {
21
+ '300' => 'Multiple Choices',
22
+ '301' => 'Moved Permanently',
23
+ '302' => 'Found',
24
+ '303' => 'See Other',
25
+ '304' => 'Not Modified',
26
+ '305' => 'Use Proxy',
27
+ '306' => 'Switch Proxy',
28
+ '307' => 'Temporary Redirect',
29
+ '308' => 'Permanent Redirect'
30
+ }
31
+ end
32
+
33
+ # A 4xx class HTTP error occurred.
34
+ # The request is invalid and should not be retried without modification.
35
+ class ClientError < BaseError
36
+ ERRORS_4XX = {
37
+ '400' => 'Bad Request',
38
+ '401' => 'Unauthorized',
39
+ '402' => 'Payment Required',
40
+ '403' => 'Forbidden',
41
+ '404' => 'Not Found',
42
+ '405' => 'Method Not Allowed',
43
+ '406' => 'Not Acceptable',
44
+ '407' => 'Proxy Authentication Required',
45
+ '408' => 'Request Timeout',
46
+ '409' => 'Conflict',
47
+ '415' => 'Unsupported Media Type',
48
+ '422' => 'Unprocessable Entity',
49
+ '429' => 'Too Many Requests'
50
+ }
51
+ end
52
+
53
+ # A 5xx class HTTP error occurred.
54
+ # An error occurred on the server and the request can be retried.
55
+ class ServerError < BaseError
56
+ end
57
+
58
+ # An unknown error occured.
59
+ class UnknownError < BaseError
60
+ end
61
+
62
+ # General Google Maps Web Service API error occured.
63
+ class ApiError < BaseError
64
+ end
65
+
66
+ # Requiered query is missing
67
+ class InvalidRequestError < ApiError
68
+ end
69
+
70
+ # The quota for the credential is over limit.
71
+ class RateLimitError < ApiError
72
+ end
73
+
74
+ # An unathorized error occurred. It might be caused by invalid key/secret or invalid access.
75
+ class RequestDeniedError < ApiError
76
+ end
77
+
78
+ # When an Address is not found. i.e. An address string could not be geocoded
79
+ class NotFoundError < ApiError
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,90 @@
1
+ require 'google_maps_apis/convert'
2
+
3
+ module GoogleMapsApis
4
+
5
+ # Encoder/decoder for [Google Encoded Polyline](https://developers.google.com/maps/documentation/utilities/polylinealgorithm).
6
+ module Polyline
7
+ module_function
8
+
9
+ # Decodes a Polyline string into a list of lat/lng hash.
10
+ #
11
+ # See the developer docs for a detailed description of this encoding:
12
+ # https://developers.google.com/maps/documentation/utilities/polylinealgorithm
13
+ #
14
+ # @example
15
+ # encoded_path = '_p~iF~ps|U_ulLnnqC_mqNvxq`@'
16
+ # path = GoogleMapsApis::Polyline.decode(encoded_path)
17
+ # #=> [{:lat=>38.5, :lng=>-120.2}, {:lat=>40.7, :lng=>-120.95}, {:lat=>43.252, :lng=>-126.45300000000002}]
18
+ #
19
+ # @param [String] polyline An encoded polyline
20
+ #
21
+ # @return [Array] Array of hash with lat/lng keys
22
+ def decode(polyline)
23
+ points = []
24
+ index = lat = lng = 0
25
+
26
+ while index < polyline.length
27
+ result = 1
28
+ shift = 0
29
+ while true
30
+ b = polyline[index].ord - 63 - 1
31
+ index += 1
32
+ result += b << shift
33
+ shift += 5
34
+ break if b < 0x1f
35
+ end
36
+ lat += (result & 1) != 0 ? (~result >> 1) : (result >> 1)
37
+
38
+ result = 1
39
+ shift = 0
40
+ while true
41
+ b = polyline[index].ord - 63 - 1
42
+ index += 1
43
+ result += b << shift
44
+ shift += 5
45
+ break if b < 0x1f
46
+ end
47
+ lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1)
48
+
49
+ points << {lat: lat * 1e-5, lng: lng * 1e-5}
50
+ end
51
+
52
+ points
53
+ end
54
+
55
+ # Encodes a list of points into a polyline string.
56
+ #
57
+ # See the developer docs for a detailed description of this encoding:
58
+ # https://developers.google.com/maps/documentation/utilities/polylinealgorithm
59
+ #
60
+ # @param [Array<Hash>, Array<Array>] points A list of lat/lng pairs.
61
+ #
62
+ # @return [String]
63
+ def encode(points)
64
+ last_lat = last_lng = 0
65
+ result = ""
66
+
67
+ points.each do |point|
68
+ ll = GoogleMapsApis::Convert.normalize_latlng(point)
69
+ lat = (ll[0] * 1e5).round.to_i
70
+ lng = (ll[1] * 1e5).round.to_i
71
+ d_lat = lat - last_lat
72
+ d_lng = lng - last_lng
73
+
74
+ [d_lat, d_lng].each do |v|
75
+ v = (v < 0) ? ~(v << 1) : (v << 1)
76
+ while v >= 0x20
77
+ result += ((0x20 | (v & 0x1f)) + 63).chr
78
+ v >>= 5
79
+ end
80
+ result += (v + 63).chr
81
+ end
82
+
83
+ last_lat = lat
84
+ last_lng = lng
85
+ end
86
+
87
+ result
88
+ end
89
+ end
90
+ end