google_maps_apis 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,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