plus_codes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7430998fc47b7df06e1254f74ca980b1c33fbd12
4
+ data.tar.gz: f503a83dd1b483d57d2dc917b3abe5d95dc22ee0
5
+ SHA512:
6
+ metadata.gz: 114f48473a81ead5f78179d0293892ff4c7f43b891b5c4cb9f4d4524806a80bacaf8242e042ee1f866c665f025f7381fc2a568b33b83bbe544b605e81a7519ab
7
+ data.tar.gz: c289c5db96763ff688bc8d6ed77d26b9442e1f248a29b0bd92099ecb659192239f4ec825a9780e240cb08b6a8cf019a08a6c186dfb005a1217d0122079e7843d
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2015 Wei-Ming Wu
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # PlusCodes
2
+
3
+ Ruby implementation of Google Open Location Code(Plus+Codes)
4
+
5
+ [Open Location Code project](https://github.com/google/open-location-code)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'plus_codes'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install plus_codes
22
+
23
+ ## Usage
24
+
25
+ ```ruby
26
+ require 'plus_codes/open_location_code'
27
+
28
+ olc = PlusCodes::OpenLocationCode.new
29
+
30
+ # Encodes the latitude and longitude into a Plus+Codes
31
+ code = olc.encode(47.0000625,8.0000625)
32
+ # => "8FVC2222+22"
33
+
34
+ # Encodes any latitude and longitude into a Plus+Codes with preferred length
35
+ code = olc.encode(47.0000625,8.0000625, 16)
36
+ # => "8FVC2222+22GCCCCC"
37
+
38
+ # Decodes a Plus+Codes back into coordinates
39
+ code_area = olc.decode(code)
40
+ puts code_area
41
+ # => lat_lo: 47.000062496 long_lo: 8.0000625 lat_hi: 47.000062504 long_hi: 8.000062530517578 code_len: 16
42
+
43
+ # Checks if a Plus+Codes is valid or not
44
+ olc.valid?(code)
45
+ # => true
46
+
47
+ # Checks if a Plus+Codes is full or not
48
+ olc.full?(code)
49
+ # => true
50
+
51
+ # Checks if a Plus+Codes is short or not
52
+ olc.short?(code)
53
+ # => false
54
+
55
+ # Shorten a Plus+Codes as possible by given reference latitude and longitude
56
+ olc.shorten('9C3W9QCJ+2VX', 51.3708675, -1.217765625)
57
+ # => "CJ+2VX"
58
+
59
+ # Extends a Plus+Codes by given reference latitude and longitude
60
+ olc.recover_nearest('CJ+2VX', 51.3708675, -1.217765625)
61
+ # => "9C3W9QCJ+2VX"
62
+ ```
63
+
64
+ ## Contributing
65
+
66
+ 1. Fork it ( https://github.com/wnameless/plus_codes-ruby/fork )
67
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
68
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
69
+ 4. Push to the branch (`git push origin my-new-feature`)
70
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ require 'yard'
9
+ YARD::Rake::YardocTask.new
10
+
11
+ Bundler::GemHelper.install_tasks
12
+
13
+ require 'rake/testtask'
14
+
15
+ Rake::TestTask.new(:test) do |t|
16
+ t.libs << 'lib'
17
+ t.libs << 'test'
18
+ t.pattern = 'test/**/*_test.rb'
19
+ t.verbose = false
20
+ end
21
+
22
+ task :default => :test
data/lib/plus_codes.rb ADDED
@@ -0,0 +1,62 @@
1
+ # Plus+Codes is a Ruby implementation of Google Open Location Code(Plus+Codes).
2
+ # @author We-Ming Wu
3
+ module PlusCodes
4
+
5
+ # A separator used to break the code into two parts to aid memorability.
6
+ SEPARATOR = '+'.freeze
7
+
8
+ # The number of characters to place before the separator.
9
+ SEPARATOR_POSITION = 8
10
+
11
+ # The character used to pad codes.
12
+ PADDING = '0'.freeze
13
+
14
+ # The character set used to encode the values.
15
+ CODE_ALPHABET = '23456789CFGHJMPQRVWX'.freeze
16
+
17
+ # The base to use to convert numbers to/from.
18
+ ENCODING_BASE = CODE_ALPHABET.length
19
+
20
+ # The maximum value for latitude in degrees.
21
+ LATITUDE_MAX = 90
22
+
23
+ # The maximum value for longitude in degrees.
24
+ LONGITUDE_MAX = 180
25
+
26
+ # Maximum code length using lat/lng pair encoding. The area of such a
27
+ # code is approximately 13x13 meters (at the equator), and should be suitable
28
+ # for identifying buildings. This excludes prefix and separator characters.
29
+ PAIR_CODE_LENGTH = 10
30
+
31
+ # The resolution values in degrees for each position in the lat/lng pair
32
+ # encoding. These give the place value of each position, and therefore the
33
+ # dimensions of the resulting area.
34
+ PAIR_RESOLUTIONS = [20.0, 1.0, 0.05, 0.0025, 0.000125].freeze
35
+
36
+ # Number of columns in the grid refinement method.
37
+ GRID_COLUMNS = 4
38
+
39
+ # Number of rows in the grid refinement method.
40
+ GRID_ROWS = 5
41
+
42
+ # Size of the initial grid in degrees.
43
+ GRID_SIZE_DEGREES = 0.000125
44
+
45
+ # Minimum length of a code that can be shortened.
46
+ MIN_TRIMMABLE_CODE_LEN = 6
47
+
48
+ # Decoder lookup table.
49
+ # -2: illegal.
50
+ # -1: Padding or Separator
51
+ # >= 0: index in the alphabet.
52
+ DECODE = [
53
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
54
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
55
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -2, -2, -2, -2,
56
+ -1, -2, 0, 1, 2, 3, 4, 5, 6, 7, -2, -2, -2, -2, -2, -2,
57
+ -2, -2, -2, 8, -2, -2, 9, 10, 11, -2, 12, -2, -2, 13, -2, -2,
58
+ 14, 15, 16, -2, -2, -2, 17, 18, 19, -2, -2, -2, -2, -2, -2, -2,
59
+ -2, -2, -2, 8, -2, -2, 9, 10, 11, -2, 12, -2, -2, 13, -2, -2,
60
+ 14, 15, 16, -2, -2, -2, 17, 18, 19, -2, -2, -2, -2, -2, -2, -2,].freeze
61
+
62
+ end
@@ -0,0 +1,43 @@
1
+ module PlusCodes
2
+
3
+ # [CodeArea] contains coordinates of a decoded Open Location Code(Plus+Codes).
4
+ # The coordinates include the latitude and longitude of the lower left and
5
+ # upper right corners and the center of the bounding box for the area the
6
+ # code represents.
7
+ # Attributes:
8
+ # latitude_lo: The latitude of the SW corner in degrees.
9
+ # longitude_lo: The longitude of the SW corner in degrees.
10
+ # latitude_hi: The latitude of the NE corner in degrees.
11
+ # longitude_hi: The longitude of the NE corner in degrees.
12
+ # latitude_center: The latitude of the center in degrees.
13
+ # longitude_center: The longitude of the center in degrees.
14
+ # code_length: The number of significant characters that were in the code.
15
+ # @author We-Ming Wu
16
+ class CodeArea
17
+ attr_accessor :latitude_lo, :longitude_lo, :latitude_hi, :longitude_hi,
18
+ :code_length, :latitude_center, :longitude_center
19
+
20
+ # Creates a [CodeArea].
21
+ #
22
+ # @param latitude_lo [Numeric] the latitude of the SW corner in degrees
23
+ # @param longitude_lo [Numeric] the longitude of the SW corner in degrees
24
+ # @param latitude_hi [Numeric] the latitude of the NE corner in degrees
25
+ # @param longitude_hi [Numeric] the longitude of the NE corner in degrees
26
+ # @param code_length [Integer] the number of characters in the code, this excludes the separator
27
+ # @return [CodeArea] a code area which contains the coordinates
28
+ def initialize(latitude_lo, longitude_lo, latitude_hi, longitude_hi, code_length)
29
+ @latitude_lo = latitude_lo
30
+ @longitude_lo = longitude_lo
31
+ @latitude_hi = latitude_hi
32
+ @longitude_hi = longitude_hi
33
+ @code_length = code_length
34
+ @latitude_center = [@latitude_lo + (@latitude_hi - @latitude_lo) / 2, LATITUDE_MAX].min
35
+ @longitude_center = [@longitude_lo + (@longitude_hi - @longitude_lo) / 2, LONGITUDE_MAX].min
36
+ end
37
+
38
+ def to_s
39
+ "lat_lo: #{@latitude_lo} long_lo: #{@longitude_lo} lat_hi: #{@latitude_hi} long_hi: #{@longitude_hi} code_len: #{@code_length}"
40
+ end
41
+ end
42
+
43
+ end
@@ -0,0 +1,351 @@
1
+ require 'plus_codes'
2
+ require 'plus_codes/code_area'
3
+
4
+ module PlusCodes
5
+
6
+ # [OpenLocationCode] implements the Google Open Location Code(Plus+Codes) algorithm.
7
+ # @author We-Ming Wu
8
+ class OpenLocationCode
9
+
10
+ # Validates the given plus+codes.
11
+ #
12
+ # @param code [String] a plus+codes
13
+ # @return [TrueClass, FalseClass] true if the code is valid, false otherwise
14
+ def valid?(code)
15
+ return false if code.nil? || code.length <= 1
16
+
17
+ separator_index = code.index(SEPARATOR)
18
+ # There must be a single separator at an even index and position should be < SEPARATOR_POSITION.
19
+ return false if separator_index.nil? ||
20
+ separator_index != code.rindex(SEPARATOR) ||
21
+ separator_index > SEPARATOR_POSITION ||
22
+ separator_index % 2 == 1
23
+
24
+ # We can have an even number of padding characters before the separator,
25
+ # but then it must be the final character.
26
+ if code.include?(PADDING)
27
+ # Not allowed to start with them!
28
+ return false if code.index(PADDING) == 0
29
+
30
+ # There can only be one group and it must have even length.
31
+ pad_match = /(#{PADDING}+)/.match(code).to_a
32
+ return false if pad_match.length != 2
33
+ match = pad_match[1]
34
+ return false if match.length % 2 == 1 || match.length > SEPARATOR_POSITION - 2
35
+
36
+ # If the code is long enough to end with a separator, make sure it does.
37
+ return false if code[code.length - 1] != SEPARATOR
38
+ end
39
+
40
+ # If there are characters after the separator, make sure there isn't just
41
+ # one of them (not legal).
42
+ return false if code.length - separator_index - 1 == 1
43
+
44
+ # Check code contains only valid characters.
45
+ code.chars.each do |ch|
46
+ return false if ch.ord > DECODE.length || DECODE[ch.ord] < -1
47
+ end
48
+ true
49
+ end
50
+
51
+ # Checks if the given plus+codes is in short format.
52
+ #
53
+ # @param code [String] a plus+codes
54
+ # @return [TrueClass, FalseClass] true if the code is short, false otherwise
55
+ def short?(code)
56
+ return false unless valid?(code)
57
+ # If there are less characters than expected before the SEPARATOR.
58
+ code.index(SEPARATOR) >= 0 && code.index(SEPARATOR) < SEPARATOR_POSITION
59
+ end
60
+
61
+ # Checks if the given plus+codes is in full format.
62
+ #
63
+ # @param code [String] a plus+codes
64
+ # @return [TrueClass, FalseClass] true if the code is full, false otherwise
65
+ def full?(code)
66
+ return false unless valid?(code)
67
+ # If it's short, it's not full.
68
+ return false if short?(code)
69
+
70
+ # Work out what the first latitude character indicates for latitude.
71
+ first_lat_value = DECODE[code[0].ord] * ENCODING_BASE
72
+ # The code would decode to a latitude of >= 90 degrees.
73
+ return false if first_lat_value >= LATITUDE_MAX * 2
74
+ if code.length > 1
75
+ # Work out what the first longitude character indicates for longitude.
76
+ first_lng_value = DECODE[code[1].ord] * ENCODING_BASE
77
+ # The code would decode to a longitude of >= 180 degrees.
78
+ return false if first_lng_value >= LONGITUDE_MAX * 2
79
+ end
80
+ true
81
+ end
82
+
83
+ # Encodes given latitude and longitude with the optionally provided code length.
84
+ #
85
+ # @param latitude [Numeric] a latitude in degrees
86
+ # @param longitude [Numeric] a longitude in degrees
87
+ # @param code_length [Integer] the number of characters in the code, this excludes the separator
88
+ # @return [String] a plus+codes
89
+ def encode(latitude, longitude, code_length = PAIR_CODE_LENGTH)
90
+ if code_length < 2 ||
91
+ (code_length < SEPARATOR_POSITION && code_length % 2 == 1)
92
+ raise ArgumentError, "Invalid Open Location Code length: #{code_length}"
93
+ end
94
+
95
+ latitude = clip_latitude(latitude)
96
+ longitude = normalize_longitude(longitude)
97
+ if latitude == 90
98
+ latitude = latitude - compute_latitude_precision(code_length).to_f
99
+ p latitude
100
+ end
101
+ code = encode_pairs(latitude, longitude, [code_length, PAIR_CODE_LENGTH].min)
102
+ # If the requested length indicates we want grid refined codes.
103
+ code += encode_grid(latitude, longitude, code_length - PAIR_CODE_LENGTH) if code_length > PAIR_CODE_LENGTH
104
+ code
105
+ end
106
+
107
+ # Decodes the given plus+codes in to a [CodeArea].
108
+ #
109
+ # @param code [String] a plus+codes
110
+ # @return [CodeArea] a code area which contains the coordinates
111
+ def decode(code)
112
+ raise ArgumentError,
113
+ "Passed Open Location Code is not a valid full code: #{code}" unless full?(code)
114
+
115
+ # Strip out separator character (we've already established the code is
116
+ # valid so the maximum is one), padding characters and convert to upper
117
+ # case.
118
+ code = code.gsub(SEPARATOR, '')
119
+ code = code.gsub(/#{PADDING}+/, '')
120
+ code = code.upcase
121
+ # Decode the lat/lng pair component.
122
+ code_area = decode_pairs(code[0...[code.length, PAIR_CODE_LENGTH].min])
123
+ # If there is a grid refinement component, decode that.
124
+ return code_area if code.length <= PAIR_CODE_LENGTH
125
+
126
+ grid_area = decode_grid(code[PAIR_CODE_LENGTH..-1])
127
+ CodeArea.new(code_area.latitude_lo + grid_area.latitude_lo,
128
+ code_area.longitude_lo + grid_area.longitude_lo,
129
+ code_area.latitude_lo + grid_area.latitude_hi,
130
+ code_area.longitude_lo + grid_area.longitude_hi,
131
+ code_area.code_length + grid_area.code_length)
132
+ end
133
+
134
+ # Finds the full plus+codes from given short plus+codes, reference latitude and longitude.
135
+ #
136
+ # @param code [String] a plus+codes
137
+ # @param reference_latitude [Numeric] a reference latitude in degrees
138
+ # @param reference_longitude [Numeric] a reference longitude in degrees
139
+ # @return [String] a plus+codes
140
+ def recover_nearest(short_code, reference_latitude, reference_longitude)
141
+ unless short?(short_code)
142
+ if full?(short_code)
143
+ return short_code
144
+ else
145
+ raise ArgumentError, 'ValueError: Passed short code is not valid: ' + short_code
146
+ end
147
+ end
148
+
149
+ # Ensure that latitude and longitude are valid.
150
+ reference_latitude = clip_latitude(reference_latitude)
151
+ reference_longitude = normalize_longitude(reference_longitude)
152
+
153
+ # Clean up the passed code.
154
+ short_code = short_code.upcase
155
+ # Compute the number of digits we need to recover.
156
+ padding_length = SEPARATOR_POSITION - short_code.index(SEPARATOR)
157
+ # The resolution (height and width) of the padded area in degrees.
158
+ resolution = 20 ** (2 - (padding_length / 2))
159
+ # Distance from the center to an edge (in degrees).
160
+ area_to_edge = resolution / 2.0
161
+
162
+ # Now round down the reference latitude and longitude to the resolution.
163
+ rounded_latitude = (reference_latitude / resolution).floor * resolution
164
+ rounded_longitude = (reference_longitude / resolution).floor * resolution
165
+
166
+ # Use the reference location to pad the supplied short code and decode it.
167
+ code_area = decode(
168
+ encode(rounded_latitude, rounded_longitude).slice(0, padding_length) +
169
+ short_code)
170
+ # How many degrees latitude is the code from the reference? If it is more
171
+ # than half the resolution, we need to move it east or west.
172
+ degrees_difference = code_area.latitude_center - reference_latitude
173
+ if degrees_difference > area_to_edge
174
+ # If the center of the short code is more than half a cell east,
175
+ # then the best match will be one position west.
176
+ code_area.latitude_center -= resolution
177
+ elsif degrees_difference < -area_to_edge
178
+ # If the center of the short code is more than half a cell west,
179
+ # then the best match will be one position east.
180
+ code_area.latitude_center += resolution
181
+ end
182
+
183
+ # How many degrees longitude is the code from the reference?
184
+ degrees_difference = code_area.longitude_center - reference_longitude
185
+ if degrees_difference > area_to_edge
186
+ code_area.longitude_center -= resolution
187
+ elsif degrees_difference < -area_to_edge
188
+ code_area.longitude_center += resolution
189
+ end
190
+ encode(code_area.latitude_center, code_area.longitude_center, code_area.code_length)
191
+ end
192
+
193
+ # Shortens the given full plus+codes by provided reference latitude and longitude.
194
+ #
195
+ # @param code [String] a plus+codes
196
+ # @param latitude [Numeric] a latitude in degrees
197
+ # @param longitude [Numeric] a longitude in degrees
198
+ # @return [String] a short plus+codes
199
+ def shorten(code, latitude, longitude)
200
+ raise ArgumentError,
201
+ "ValueError: Passed code is not valid and full: #{code}" unless full?(code)
202
+ raise ArgumentError,
203
+ "ValueError: Cannot shorten padded codes: #{code}" unless code.index(PADDING).nil?
204
+
205
+ code = code.upcase
206
+ code_area = decode(code)
207
+ if code_area.code_length < MIN_TRIMMABLE_CODE_LEN
208
+ raise RangeError,
209
+ "ValueError: Code length must be at least #{MIN_TRIMMABLE_CODE_LEN}"
210
+ end
211
+ # Ensure that latitude and longitude are valid.
212
+ latitude = clip_latitude(latitude)
213
+ longitude = normalize_longitude(longitude)
214
+ # How close are the latitude and longitude to the code center.
215
+ range = [(code_area.latitude_center - latitude).abs,
216
+ (code_area.longitude_center - longitude).abs].max
217
+ i = PAIR_RESOLUTIONS.length - 2
218
+ while i >= 1 do
219
+ # Check if we're close enough to shorten. The range must be less than 1/2
220
+ # the resolution to shorten at all, and we want to allow some safety, so
221
+ # use 0.3 instead of 0.5 as a multiplier.
222
+ return code[(i + 1) * 2..-1] if range < (PAIR_RESOLUTIONS[i] * 0.3)
223
+ # Trim it.
224
+ i -= 1
225
+ end
226
+ code
227
+ end
228
+
229
+ private
230
+
231
+ def encode_pairs(latitude, longitude, code_length)
232
+ code = ''
233
+ # Adjust latitude and longitude so they fall into positive ranges.
234
+ adjusted_latitude = latitude + LATITUDE_MAX
235
+ adjusted_longitude = longitude + LONGITUDE_MAX
236
+ # Count digits - can't use string length because it may include a separator
237
+ # character.
238
+ digit_count = 0
239
+ while (digit_count < code_length) do
240
+ # Provides the value of digits in this place in decimal degrees.
241
+ place_value = PAIR_RESOLUTIONS[(digit_count / 2).to_i]
242
+ # Do the latitude - gets the digit for this place and subtracts that for
243
+ # the next digit.
244
+ digit_value = (adjusted_latitude / place_value).to_i
245
+ adjusted_latitude -= digit_value * place_value
246
+ code += CODE_ALPHABET[digit_value]
247
+ digit_count += 1
248
+ # And do the longitude - gets the digit for this place and subtracts that
249
+ # for the next digit.
250
+ digit_value = (adjusted_longitude / place_value).to_i
251
+ adjusted_longitude -= digit_value * place_value
252
+ code += CODE_ALPHABET[digit_value]
253
+ digit_count +=1
254
+ # Should we add a separator here?
255
+ code += SEPARATOR if digit_count == SEPARATOR_POSITION && digit_count < code_length
256
+ end
257
+ # If necessary, Add padding.
258
+ if code.length < SEPARATOR_POSITION
259
+ code = code + (PADDING * (SEPARATOR_POSITION - code.length))
260
+ end
261
+ code = code + SEPARATOR if code.length == SEPARATOR_POSITION
262
+ code
263
+ end
264
+
265
+ def encode_grid(latitude, longitude, code_length)
266
+ code = ''
267
+ lat_place_value = GRID_SIZE_DEGREES
268
+ lng_place_value = GRID_SIZE_DEGREES
269
+ # Adjust latitude and longitude so they fall into positive ranges and
270
+ # get the offset for the required places.
271
+ adjusted_latitude = (latitude + LATITUDE_MAX) % lat_place_value
272
+ adjusted_longitude = (longitude + LONGITUDE_MAX) % lng_place_value
273
+ (1..code_length).each do
274
+ # Work out the row and column.
275
+ row = (adjusted_latitude / (lat_place_value / GRID_ROWS)).floor
276
+ col = (adjusted_longitude / (lng_place_value / GRID_COLUMNS)).floor
277
+ lat_place_value /= GRID_ROWS
278
+ lng_place_value /= GRID_COLUMNS
279
+ adjusted_latitude -= row * lat_place_value
280
+ adjusted_longitude -= col * lng_place_value
281
+ code += CODE_ALPHABET[row * GRID_COLUMNS + col]
282
+ end
283
+ code
284
+ end
285
+
286
+ def decode_pairs(code)
287
+ # Get the latitude and longitude values. These will need correcting from
288
+ # positive ranges.
289
+ latitude = decode_pairs_sequence(code, 0.0)
290
+ longitude = decode_pairs_sequence(code, 1.0)
291
+ # Correct the values and set them into the CodeArea object.
292
+ CodeArea.new(latitude[0] - LATITUDE_MAX,
293
+ longitude[0] - LONGITUDE_MAX, latitude[1] - LATITUDE_MAX,
294
+ longitude[1] - LONGITUDE_MAX, code.length)
295
+ end
296
+
297
+ def decode_pairs_sequence(code, offset)
298
+ i = 0
299
+ value = 0
300
+ while i * 2 + offset < code.length do
301
+ value += DECODE[code[i * 2 + offset.floor].ord] * PAIR_RESOLUTIONS[i]
302
+ i += 1
303
+ end
304
+ [value, value + PAIR_RESOLUTIONS[i - 1]]
305
+ end
306
+
307
+ def decode_grid(code)
308
+ latitude_lo = 0.0
309
+ longitude_lo = 0.0
310
+ lat_place_value = GRID_SIZE_DEGREES
311
+ lng_place_value = GRID_SIZE_DEGREES
312
+ (0...code.length).each do |i|
313
+ code_index = DECODE[code[i].ord]
314
+ row = (code_index / GRID_COLUMNS).floor()
315
+ col = code_index % GRID_COLUMNS
316
+
317
+ lat_place_value /= GRID_ROWS
318
+ lng_place_value /= GRID_COLUMNS
319
+
320
+ latitude_lo += row * lat_place_value
321
+ longitude_lo += col * lng_place_value
322
+ end
323
+ CodeArea.new(latitude_lo, longitude_lo, latitude_lo + lat_place_value,
324
+ longitude_lo + lng_place_value, code.length)
325
+ end
326
+
327
+ def clip_latitude(latitude)
328
+ [90.0, [-90.0, latitude].max].min
329
+ end
330
+
331
+ def compute_latitude_precision(code_length)
332
+ if code_length <= 10
333
+ 20 ** ((code_length / -2).to_i + 2)
334
+ else
335
+ (20 ** -3) / (GRID_ROWS ** (code_length - 10))
336
+ end
337
+ end
338
+
339
+ def normalize_longitude(longitude)
340
+ begin
341
+ longitude += 360
342
+ end while longitude < -180
343
+ begin
344
+ longitude -= 360
345
+ end while longitude >= 180
346
+ longitude
347
+ end
348
+
349
+ end
350
+
351
+ end
@@ -0,0 +1,3 @@
1
+ module PlusCodes
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,72 @@
1
+ require 'test_helper'
2
+ require 'plus_codes/open_location_code'
3
+
4
+ class PlusCodesTest < Test::Unit::TestCase
5
+
6
+ def setup
7
+ @test_data_folder_path = File.join(File.dirname(__FILE__), 'test_data')
8
+ @olc = PlusCodes::OpenLocationCode.new
9
+ end
10
+
11
+ def test_validity
12
+ read_csv_lines('validityTests.csv').each do |line|
13
+ cols = line.split(',')
14
+ code = cols[0]
15
+ is_valid = cols[1] == 'true'
16
+ is_short = cols[2] == 'true'
17
+ is_full = cols[3] == 'true'
18
+ is_valid_olc = @olc.valid?(code)
19
+ is_short_olc = @olc.short?(code)
20
+ is_full_olc = @olc.full?(code)
21
+ result = is_full == is_full_olc && is_short_olc == is_short && is_valid_olc == is_valid
22
+ assert_true(result)
23
+ end
24
+ end
25
+
26
+ def test_encode_decode
27
+ read_csv_lines('encodingTests.csv').each do |line|
28
+ cols = line.split(',')
29
+ code_area = @olc.decode(cols[0])
30
+ code = @olc.encode(cols[1].to_f, cols[2].to_f, code_area.code_length)
31
+ assert_equal(cols[0], code)
32
+ assert_true((code_area.latitude_lo - cols[3].to_f).abs < 0.001)
33
+ assert_true((code_area.longitude_lo - cols[4].to_f).abs < 0.001)
34
+ assert_true((code_area.latitude_hi - cols[5].to_f).abs < 0.001)
35
+ assert_true((code_area.longitude_hi - cols[6].to_f).abs < 0.001)
36
+ end
37
+ end
38
+
39
+ def test_shorten
40
+ read_csv_lines('shortCodeTests.csv').each do |line|
41
+ cols = line.split(',')
42
+ code = cols[0]
43
+ lat = cols[1].to_f
44
+ lng = cols[2].to_f
45
+ short_code = cols[3]
46
+ short = @olc.shorten(code, lat, lng)
47
+ assert_equal(short_code, short)
48
+ expanded = @olc.recover_nearest(short, lat, lng)
49
+ assert_equal(code, expanded)
50
+ end
51
+ end
52
+
53
+ def test_longer_encoding_with_speacial_case
54
+ assert_equal('CFX3X2X2+X2XXXXQ', @olc.encode(90.0, 1.0, 15));
55
+ end
56
+
57
+ def test_code_area_to_s
58
+ read_csv_lines('encodingTests.csv').each do |line|
59
+ cols = line.split(',')
60
+ code_area = @olc.decode(cols[0])
61
+ assert_equal("lat_lo: #{code_area.latitude_lo} long_lo: #{code_area.longitude_lo} " <<
62
+ "lat_hi: #{code_area.latitude_hi} long_hi: #{code_area.longitude_hi} " <<
63
+ "code_len: #{code_area.code_length}", code_area.to_s)
64
+ end
65
+ end
66
+
67
+ def read_csv_lines(csv_file)
68
+ f = File.open(File.join(@test_data_folder_path, csv_file), 'r')
69
+ f.each_line.lazy.select { |line| line !~ /^\s*#/ }.map { |line| line.chop }
70
+ end
71
+
72
+ end
@@ -0,0 +1,23 @@
1
+ # Test encoding and decoding Open Location Codes.
2
+ #
3
+ # Provides test cases for encoding latitude and longitude to codes and expected
4
+ # values for decoding.
5
+ #
6
+ # Format:
7
+ # code,lat,lng,latLo,lngLo,latHi,lngHi
8
+ 7FG49Q00+,20.375,2.775,20.35,2.75,20.4,2.8
9
+ 7FG49QCJ+2V,20.3700625,2.7821875,20.37,2.782125,20.370125,2.78225
10
+ 7FG49QCJ+2VX,20.3701125,2.782234375,20.3701,2.78221875,20.370125,2.78225
11
+ 7FG49QCJ+2VXGJ,20.3701135,2.78223535156,20.370113,2.782234375,20.370114,2.78223632813
12
+ 8FVC2222+22,47.0000625,8.0000625,47.0,8.0,47.000125,8.000125
13
+ 4VCPPQGP+Q9,-41.2730625,174.7859375,-41.273125,174.785875,-41.273,174.786
14
+ 62G20000+,0.5,-179.5,0.0,-180.0,1,-179
15
+ 22220000+,-89.5,-179.5,-90,-180,-89,-179
16
+ 7FG40000+,20.5,2.5,20.0,2.0,21.0,3.0
17
+ 22222222+22,-89.9999375,-179.9999375,-90.0,-180.0,-89.999875,-179.999875
18
+ 6VGX0000+,0.5,179.5,0,179,1,180
19
+ # Special cases over 90 latitude and 180 longitude
20
+ CFX30000+,90,1,89,1,90,2
21
+ CFX30000+,92,1,89,1,90,2
22
+ 62H20000+,1,180,1,-180,2,-179
23
+ 62H30000+,1,181,1,-179,2,-178
@@ -0,0 +1,15 @@
1
+ # Test shortening and extending codes.
2
+ #
3
+ # Format:
4
+ # full code,lat,lng,shortcode
5
+ 9C3W9QCJ+2VX,51.3701125,-1.217765625,+2VX
6
+ # Adjust so we can't trim by 8 (+/- .000755)
7
+ 9C3W9QCJ+2VX,51.3708675,-1.217765625,CJ+2VX
8
+ 9C3W9QCJ+2VX,51.3693575,-1.217765625,CJ+2VX
9
+ 9C3W9QCJ+2VX,51.3701125,-1.218520625,CJ+2VX
10
+ 9C3W9QCJ+2VX,51.3701125,-1.217010625,CJ+2VX
11
+ # Adjust so we can't trim by 6 (+/- .0151)
12
+ 9C3W9QCJ+2VX,51.3852125,-1.217765625,9QCJ+2VX
13
+ 9C3W9QCJ+2VX,51.3550125,-1.217765625,9QCJ+2VX
14
+ 9C3W9QCJ+2VX,51.3701125,-1.232865625,9QCJ+2VX
15
+ 9C3W9QCJ+2VX,51.3701125,-1.202665625,9QCJ+2VX
@@ -0,0 +1,23 @@
1
+ # Test data for validity tests.
2
+ # Format of each line is:
3
+ # code,isValid,isShort,isFull
4
+ # Valid full codes:
5
+ 8FWC2345+G6,true,false,true
6
+ 8FWC2345+G6G,true,false,true
7
+ 8fwc2345+,true,false,true
8
+ 8FWCX400+,true,false,true
9
+ # Valid short codes:
10
+ WC2345+G6g,true,true,false
11
+ 2345+G6,true,true,false
12
+ 45+G6,true,true,false
13
+ +G6,true,true,false
14
+ # Invalid codes
15
+ G+,false,false,false
16
+ +,false,false,false
17
+ 8FWC2345+G,false,false,false
18
+ 8FWC2_45+G6,false,false,false
19
+ 8FWC2η45+G6,false,false,false
20
+ 8FWC2345+G6+,false,false,false
21
+ 8FWC2300+G6,false,false,false
22
+ WC2300+G6g,false,false,false
23
+ WC2345+G,false,false,false
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'simplecov'
4
+ SimpleCov.start
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'test/unit'
13
+
14
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
15
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
16
+ require 'plus_codes'
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: plus_codes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Wei-Ming Wu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: test-unit
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Ruby implementation of Google Open Location Code(Plus+Codes)
84
+ email:
85
+ - wnameless@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - LICENSE
91
+ - README.md
92
+ - Rakefile
93
+ - lib/plus_codes.rb
94
+ - lib/plus_codes/code_area.rb
95
+ - lib/plus_codes/open_location_code.rb
96
+ - lib/plus_codes/version.rb
97
+ - test/plus_codes_test.rb
98
+ - test/test_data/encodingTests.csv
99
+ - test/test_data/shortCodeTests.csv
100
+ - test/test_data/validityTests.csv
101
+ - test/test_helper.rb
102
+ homepage: https://github.com/wnameless/plus_codes-ruby
103
+ licenses:
104
+ - Apache License, Version 2.0
105
+ metadata: {}
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 2.4.5
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: Ruby implementation of Google Open Location Code(Plus+Codes)
126
+ test_files:
127
+ - test/plus_codes_test.rb
128
+ - test/test_data/encodingTests.csv
129
+ - test/test_data/shortCodeTests.csv
130
+ - test/test_data/validityTests.csv
131
+ - test/test_helper.rb
132
+ has_rdoc: