plus_codes 0.1.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.
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: