plus_codes 0.1.2 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 69d6043d4051f33ae155cb1659af7e8e533054ae
4
- data.tar.gz: 2eddbf67c2fd1e8ae3b230150868a074d173d41a
3
+ metadata.gz: f6d973ca48c7cbdf04ae78ceae2e41e0788ead86
4
+ data.tar.gz: 09713b9cbfcbad05b8d24fe8740104f14d8bc031
5
5
  SHA512:
6
- metadata.gz: 3a1d9f99221ae07169b7dcd1145b7a0a2a863636043fa5f6d602abd919c992e614cd9a2c2f332abd7feb5abb2267406816f4632273f6c93948055c3bb76c632c
7
- data.tar.gz: d81015cd0361126dc140b86590bdd28bee3a6f6f26caff6a78a9ef77154c6ced5ebf5992b2b9366a74be16cf56a638b014daa287524feee91fd7c3c30ff4f752
6
+ metadata.gz: 3e30d6a3c449e5e1c597abd7531a80e567f98eace3f9430414e19c50866c3004b1e70263f281895255e99164b8e0ec37daa18204fec16960aea5bb6558fe4995
7
+ data.tar.gz: b6e3a2028c1413b280c3dc032c76c00ce773fc7fc6afdf4f2fe74011b0e944d9eaf6671f07d02b3358d3f7909062f0ea6f7cf7c5f9202d03313972408adf4d05
data/Rakefile CHANGED
@@ -5,13 +5,12 @@ rescue LoadError
5
5
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
6
  end
7
7
 
8
+ Bundler::GemHelper.install_tasks
9
+
8
10
  require 'yard'
9
11
  YARD::Rake::YardocTask.new
10
12
 
11
- Bundler::GemHelper.install_tasks
12
-
13
13
  require 'rake/testtask'
14
-
15
14
  Rake::TestTask.new(:test) do |t|
16
15
  t.libs << 'lib'
17
16
  t.libs << 'test'
@@ -4,40 +4,34 @@ module PlusCodes
4
4
  # The coordinates include the latitude and longitude of the lower left and
5
5
  # upper right corners and the center of the bounding box for the area the
6
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
- #
7
+ #
16
8
  # @author We-Ming Wu
17
9
  class CodeArea
18
- attr_accessor :latitude_lo, :longitude_lo, :latitude_hi, :longitude_hi,
19
- :code_length, :latitude_center, :longitude_center
10
+ attr_accessor :south_latitude, :west_longitude, :latitude_height,
11
+ :longitude_width, :latitude_center, :longitude_center
20
12
 
21
13
  # Creates a [CodeArea].
22
14
  #
23
- # @param latitude_lo [Numeric] the latitude of the SW corner in degrees
24
- # @param longitude_lo [Numeric] the longitude of the SW corner in degrees
25
- # @param latitude_hi [Numeric] the latitude of the NE corner in degrees
26
- # @param longitude_hi [Numeric] the longitude of the NE corner in degrees
27
- # @param code_length [Integer] the number of characters in the code, this excludes the separator
15
+ # @param south_latitude [Numeric] the latitude of the SW corner in degrees
16
+ # @param west_longitude [Numeric] the longitude of the SW corner in degrees
17
+ # @param latitude_height [Numeric] the height from the SW corner in degrees
18
+ # @param longitude_width [Numeric] the width from the SW corner in degrees
28
19
  # @return [CodeArea] a code area which contains the coordinates
29
- def initialize(latitude_lo, longitude_lo, latitude_hi, longitude_hi, code_length)
30
- @latitude_lo = latitude_lo
31
- @longitude_lo = longitude_lo
32
- @latitude_hi = latitude_hi
33
- @longitude_hi = longitude_hi
34
- @code_length = code_length
35
- @latitude_center = [@latitude_lo + (@latitude_hi - @latitude_lo) / 2, LATITUDE_MAX].min
36
- @longitude_center = [@longitude_lo + (@longitude_hi - @longitude_lo) / 2, LONGITUDE_MAX].min
20
+ def initialize(south_latitude, west_longitude, latitude_height, longitude_width)
21
+ @south_latitude = south_latitude
22
+ @west_longitude = west_longitude
23
+ @latitude_height = latitude_height
24
+ @longitude_width = longitude_width
25
+ @latitude_center = south_latitude + latitude_height / 2.0
26
+ @longitude_center = west_longitude + longitude_width / 2.0
37
27
  end
38
28
 
39
- def to_s
40
- "lat_lo: #{@latitude_lo} long_lo: #{@longitude_lo} lat_hi: #{@latitude_hi} long_hi: #{@longitude_hi} code_len: #{@code_length}"
29
+ def north_latitude
30
+ @south_latitude + @latitude_height
31
+ end
32
+
33
+ def east_longitude
34
+ @west_longitude + @longitude_width
41
35
  end
42
36
  end
43
37
 
@@ -8,187 +8,142 @@ module PlusCodes
8
8
  # @author We-Ming Wu
9
9
  class OpenLocationCode
10
10
 
11
- # Validates the given plus+codes.
11
+ # Determines if a string is a valid sequence of Open Location Code(Plus+Codes) characters.
12
12
  #
13
13
  # @param code [String] a plus+codes
14
14
  # @return [TrueClass, FalseClass] true if the code is valid, false otherwise
15
15
  def valid?(code)
16
- return false if code.nil? || code.length <= 1
17
-
18
- separator_index = code.index(SEPARATOR)
19
- # There must be a single separator at an even index and position should be < SEPARATOR_POSITION.
20
- return false if separator_index.nil? ||
21
- separator_index != code.rindex(SEPARATOR) ||
22
- separator_index > SEPARATOR_POSITION ||
23
- separator_index.odd?
24
-
25
- # We can have an even number of padding characters before the separator,
26
- # but then it must be the final character.
27
- if code.include?(PADDING)
28
- return false if code.start_with?(PADDING)
29
- pad_match = code.scan(/#{PADDING}+/)
30
- return false unless pad_match.one?
31
- padding = pad_match[0]
32
- return false if padding.length.odd?
33
- return false if padding.length > SEPARATOR_POSITION - 2
34
- return false if code[-2] != PADDING
35
- return false if code[-1] != SEPARATOR
36
- end
37
-
38
- # If there are characters after the separator, make sure there isn't just
39
- # one of them (not legal).
40
- return false if code.length - separator_index - 1 == 1
41
-
42
- # Check code contains only valid characters.
43
- code.chars.each do |ch|
44
- return false if ch.ord >= DECODE.length || DECODE[ch.ord] < -1
45
- end
46
- true
16
+ valid_length?(code) &&
17
+ valid_separator?(code) &&
18
+ valid_padding?(code) &&
19
+ valid_character?(code)
47
20
  end
48
21
 
49
- # Checks if the given plus+codes is in short format.
22
+ # Determines if a string is a valid short Open Location Code(Plus+Codes).
50
23
  #
51
24
  # @param code [String] a plus+codes
52
25
  # @return [TrueClass, FalseClass] true if the code is short, false otherwise
53
26
  def short?(code)
54
- return false unless valid?(code)
55
- # If there are less characters than expected before the SEPARATOR.
56
- code.index(SEPARATOR) >= 0 && code.index(SEPARATOR) < SEPARATOR_POSITION
27
+ valid?(code) && code.index(SEPARATOR) < SEPARATOR_POSITION
57
28
  end
58
29
 
59
- # Checks if the given plus+codes is in full format.
30
+ # Determines if a string is a valid full Open Location Code(Plus+Codes).
60
31
  #
61
32
  # @param code [String] a plus+codes
62
33
  # @return [TrueClass, FalseClass] true if the code is full, false otherwise
63
34
  def full?(code)
64
- return false unless valid?(code)
65
- # If it's short, it's not full.
66
- return false if short?(code)
67
-
68
- # Work out what the first latitude character indicates for latitude.
69
- first_lat_value = DECODE[code[0].ord] * ENCODING_BASE
70
- # The code would decode to a latitude of >= 90 degrees.
71
- return false if first_lat_value >= LATITUDE_MAX * 2
72
- if code.length > 1
73
- # Work out what the first longitude character indicates for longitude.
74
- first_lng_value = DECODE[code[1].ord] * ENCODING_BASE
75
- # The code would decode to a longitude of >= 180 degrees.
76
- return false if first_lng_value >= LONGITUDE_MAX * 2
77
- end
78
- true
35
+ valid?(code) && !short?(code)
79
36
  end
80
37
 
81
- # Encodes given latitude and longitude with the optionally provided code length.
38
+ # Converts a latitude and longitude into a Open Location Code(Plus+Codes).
82
39
  #
83
40
  # @param latitude [Numeric] a latitude in degrees
84
41
  # @param longitude [Numeric] a longitude in degrees
85
42
  # @param code_length [Integer] the number of characters in the code, this excludes the separator
86
43
  # @return [String] a plus+codes
87
- def encode(latitude, longitude, code_length = PAIR_CODE_LENGTH)
88
- if code_length < 2 || (code_length < SEPARATOR_POSITION && code_length.odd?)
89
- raise ArgumentError, "Invalid Open Location Code length: #{code_length}"
90
- end
44
+ def encode(latitude, longitude, code_length = 10)
45
+ raise ArgumentError,
46
+ "Invalid Open Location Code(Plus+Codes) length: #{code_length}" if invalid_length?(code_length)
91
47
 
92
48
  latitude = clip_latitude(latitude)
93
49
  longitude = normalize_longitude(longitude)
94
- if latitude == 90
95
- latitude = latitude - compute_latitude_precision(code_length).to_f
96
- end
97
- code = encode_pairs(latitude, longitude, [code_length, PAIR_CODE_LENGTH].min)
98
- # If the requested length indicates we want grid refined codes.
99
- if code_length > PAIR_CODE_LENGTH
100
- code += encode_grid(latitude, longitude, code_length - PAIR_CODE_LENGTH)
50
+ latitude -= precision_by_length(code_length) if latitude == 90
51
+
52
+ lat = (latitude + 90).to_r
53
+ lng = (longitude + 180).to_r
54
+
55
+ digit = 0
56
+ code = ''
57
+ while digit < code_length
58
+ lat, lng = narrow_region(digit, lat, lng)
59
+ digit, lat, lng = build_code(digit, code, lat, lng)
60
+ code << SEPARATOR if (digit == SEPARATOR_POSITION)
101
61
  end
102
- code
62
+
63
+ digit < SEPARATOR_POSITION ? padded(code) : code
103
64
  end
104
65
 
105
- # Decodes the given plus+codes in to a [CodeArea].
66
+ # Decodes an Open Location Code(Plus+Codes) into a [CodeArea].
106
67
  #
107
68
  # @param code [String] a plus+codes
108
69
  # @return [CodeArea] a code area which contains the coordinates
109
70
  def decode(code)
110
71
  raise ArgumentError,
111
- "Passed Open Location Code is not a valid full code: #{code}" unless full?(code)
72
+ "Open Location Code(Plus+Codes) is not a valid full code: #{code}" unless full?(code)
112
73
 
113
- # Strip out separator character (we've already established the code is
114
- # valid so the maximum is one), padding characters and convert to upper
115
- # case.
116
74
  code = code.gsub(SEPARATOR, '')
117
75
  code = code.gsub(/#{PADDING}+/, '')
118
76
  code = code.upcase
119
- # Decode the lat/lng pair component.
120
- code_area = decode_pairs(code[0...[code.length, PAIR_CODE_LENGTH].min])
121
- # If there is a grid refinement component, decode that.
122
- return code_area if code.length <= PAIR_CODE_LENGTH
123
-
124
- grid_area = decode_grid(code[PAIR_CODE_LENGTH..-1])
125
- CodeArea.new(code_area.latitude_lo + grid_area.latitude_lo,
126
- code_area.longitude_lo + grid_area.longitude_lo,
127
- code_area.latitude_lo + grid_area.latitude_hi,
128
- code_area.longitude_lo + grid_area.longitude_hi,
129
- code_area.code_length + grid_area.code_length)
77
+
78
+ south_latitude = -90.0
79
+ west_longitude = -180.0
80
+
81
+ lat_resolution = 400.to_r
82
+ lng_resolution = 400.to_r
83
+
84
+ digit = 0
85
+ while digit < code.length
86
+ if digit < 10
87
+ lat_resolution /= 20
88
+ lng_resolution /= 20
89
+ south_latitude += lat_resolution * DECODE[code[digit].ord]
90
+ west_longitude += lng_resolution * DECODE[code[digit + 1].ord]
91
+ digit += 2
92
+ else
93
+ lat_resolution /= 5
94
+ lng_resolution /= 4
95
+ row = DECODE[code[digit].ord] / 4
96
+ column = DECODE[code[digit].ord] % 4
97
+ south_latitude += lat_resolution * row
98
+ west_longitude += lng_resolution * column
99
+ digit += 1
100
+ end
101
+ end
102
+
103
+ CodeArea.new(south_latitude, west_longitude, lat_resolution, lng_resolution)
130
104
  end
131
105
 
132
- # Finds the full plus+codes from given short plus+codes, reference latitude and longitude.
106
+ # Recovers a full Open Location Code(Plus+Codes) from a short code and a reference location.
133
107
  #
134
- # @param code [String] a plus+codes
108
+ # @param short_code [String] a plus+codes
135
109
  # @param reference_latitude [Numeric] a reference latitude in degrees
136
110
  # @param reference_longitude [Numeric] a reference longitude in degrees
137
111
  # @return [String] a plus+codes
138
112
  def recover_nearest(short_code, reference_latitude, reference_longitude)
139
- unless short?(short_code)
140
- if full?(short_code)
141
- return short_code
142
- else
143
- raise ArgumentError, "ValueError: Passed short code is not valid: #{short_code}"
144
- end
145
- end
113
+ return short_code if full?(short_code)
114
+ raise ArgumentError,
115
+ "Open Location Code(Plus+Codes) is not valid: #{short_code}" unless short?(short_code)
116
+
117
+ ref_lat = clip_latitude(reference_latitude)
118
+ ref_lng = normalize_longitude(reference_longitude)
119
+
120
+ prefix_len = SEPARATOR_POSITION - short_code.index(SEPARATOR)
121
+ code = prefix_by_reference(ref_lat, ref_lng, prefix_len) << short_code
122
+ code_area = decode(code)
123
+
124
+ area_range = precision_by_length(prefix_len)
125
+ area_edge = area_range / 2
146
126
 
147
- # Ensure that latitude and longitude are valid.
148
- reference_latitude = clip_latitude(reference_latitude)
149
- reference_longitude = normalize_longitude(reference_longitude)
150
-
151
- # Clean up the passed code.
152
- short_code = short_code.upcase
153
- # Compute the number of digits we need to recover.
154
- padding_length = SEPARATOR_POSITION - short_code.index(SEPARATOR)
155
- # The resolution (height and width) of the padded area in degrees.
156
- resolution = 20 ** (2 - (padding_length / 2))
157
- # Distance from the center to an edge (in degrees).
158
- area_to_edge = resolution / 2.0
159
-
160
- # Now round down the reference latitude and longitude to the resolution.
161
- rounded_latitude = (reference_latitude / resolution).floor * resolution
162
- rounded_longitude = (reference_longitude / resolution).floor * resolution
163
-
164
- # Use the reference location to pad the supplied short code and decode it.
165
- code_area = decode(
166
- encode(rounded_latitude, rounded_longitude).slice(0, padding_length) +
167
- short_code)
168
- # How many degrees latitude is the code from the reference? If it is more
169
- # than half the resolution, we need to move it east or west.
170
- degrees_difference = code_area.latitude_center - reference_latitude
171
- if degrees_difference > area_to_edge
172
- # If the center of the short code is more than half a cell east,
173
- # then the best match will be one position west.
174
- code_area.latitude_center -= resolution
175
- elsif degrees_difference < -area_to_edge
176
- # If the center of the short code is more than half a cell west,
177
- # then the best match will be one position east.
178
- code_area.latitude_center += resolution
127
+ latitude = code_area.latitude_center
128
+ latitude_diff = latitude - ref_lat
129
+ if (latitude_diff > area_edge)
130
+ latitude -= area_range
131
+ elsif (latitude_diff < -area_edge)
132
+ latitude += area_range
179
133
  end
180
134
 
181
- # How many degrees longitude is the code from the reference?
182
- degrees_difference = code_area.longitude_center - reference_longitude
183
- if degrees_difference > area_to_edge
184
- code_area.longitude_center -= resolution
185
- elsif degrees_difference < -area_to_edge
186
- code_area.longitude_center += resolution
135
+ longitude = code_area.longitude_center
136
+ longitude_diff = longitude - ref_lng
137
+ if (longitude_diff > area_edge)
138
+ longitude -= area_range
139
+ elsif (longitude_diff < -area_edge)
140
+ longitude += area_range
187
141
  end
188
- encode(code_area.latitude_center, code_area.longitude_center, code_area.code_length)
142
+
143
+ encode(latitude, longitude, code.length - SEPARATOR.length)
189
144
  end
190
145
 
191
- # Shortens the given full plus+codes by provided reference latitude and longitude.
146
+ # Removes four, six or eight digits from the front of an Open Location Code(Plus+Codes) given a reference location.
192
147
  #
193
148
  # @param code [String] a plus+codes
194
149
  # @param latitude [Numeric] a latitude in degrees
@@ -196,154 +151,114 @@ module PlusCodes
196
151
  # @return [String] a short plus+codes
197
152
  def shorten(code, latitude, longitude)
198
153
  raise ArgumentError,
199
- "ValueError: Passed code is not valid and full: #{code}" unless full?(code)
154
+ "Open Location Code(Plus+Codes) is a valid full code: #{code}" unless full?(code)
200
155
  raise ArgumentError,
201
- "ValueError: Cannot shorten padded codes: #{code}" unless code.index(PADDING).nil?
156
+ "Cannot shorten padded codes: #{code}" unless code.index(PADDING).nil?
202
157
 
203
- code = code.upcase
204
158
  code_area = decode(code)
205
- if code_area.code_length < MIN_TRIMMABLE_CODE_LEN
206
- raise RangeError,
207
- "ValueError: Code length must be at least #{MIN_TRIMMABLE_CODE_LEN}"
208
- end
209
- # Ensure that latitude and longitude are valid.
210
- latitude = clip_latitude(latitude)
211
- longitude = normalize_longitude(longitude)
212
- # How close are the latitude and longitude to the code center.
213
- range = [(code_area.latitude_center - latitude).abs,
214
- (code_area.longitude_center - longitude).abs].max
215
- i = PAIR_RESOLUTIONS.length - 2
216
- while i >= 1 do
217
- # Check if we're close enough to shorten. The range must be less than 1/2
218
- # the resolution to shorten at all, and we want to allow some safety, so
219
- # use 0.3 instead of 0.5 as a multiplier.
220
- return code[(i + 1) * 2..-1] if range < (PAIR_RESOLUTIONS[i] * 0.3)
221
- # Trim it.
222
- i -= 1
159
+ lat_diff = (latitude - code_area.latitude_center).abs
160
+ lng_diff = (longitude - code_area.longitude_center).abs
161
+ max_diff = [lat_diff, lng_diff].max
162
+ [8, 6, 4].each do |removal_len|
163
+ area_edge = precision_by_length(removal_len + 2) / 2
164
+ return code[removal_len..-1] if max_diff < area_edge
223
165
  end
224
- code
166
+
167
+ code.upcase
225
168
  end
226
169
 
227
170
  private
228
171
 
229
- def encode_pairs(latitude, longitude, code_length)
230
- code = ''
231
- # Adjust latitude and longitude so they fall into positive ranges.
232
- adjusted_latitude = latitude + LATITUDE_MAX
233
- adjusted_longitude = longitude + LONGITUDE_MAX
234
- # Count digits - can't use string length because it may include a separator
235
- # character.
236
- digit_count = 0
237
- while (digit_count < code_length) do
238
- # Provides the value of digits in this place in decimal degrees.
239
- place_value = PAIR_RESOLUTIONS[(digit_count / 2).to_i]
240
- # Do the latitude - gets the digit for this place and subtracts that for
241
- # the next digit.
242
- digit_value = (adjusted_latitude / place_value).to_i
243
- adjusted_latitude -= digit_value * place_value
244
- code += CODE_ALPHABET[digit_value]
245
- digit_count += 1
246
- # And do the longitude - gets the digit for this place and subtracts that
247
- # for the next digit.
248
- digit_value = (adjusted_longitude / place_value).to_i
249
- adjusted_longitude -= digit_value * place_value
250
- code += CODE_ALPHABET[digit_value]
251
- digit_count +=1
252
- # Should we add a separator here?
253
- code += SEPARATOR if digit_count == SEPARATOR_POSITION && digit_count < code_length
254
- end
255
- # If necessary, Add padding.
256
- if code.length < SEPARATOR_POSITION
257
- code = code + (PADDING * (SEPARATOR_POSITION - code.length))
172
+ def prefix_by_reference(latitude, longitude, prefix_len)
173
+ precision = precision_by_length(prefix_len)
174
+ rounded_latitude = (latitude / precision).floor * precision
175
+ rounded_longitude = (longitude / precision).floor * precision
176
+ encode(rounded_latitude, rounded_longitude)[0...prefix_len]
177
+ end
178
+
179
+ def narrow_region(digit, latitude, longitude)
180
+ if digit == 0
181
+ latitude /= 20
182
+ longitude /= 20
183
+ elsif digit < 10
184
+ latitude *= 20
185
+ longitude *= 20
186
+ else
187
+ latitude *= 5
188
+ longitude *= 4
258
189
  end
259
- code = code + SEPARATOR if code.length == SEPARATOR_POSITION
260
- code
190
+ [latitude, longitude]
261
191
  end
262
192
 
263
- def encode_grid(latitude, longitude, code_length)
264
- code = ''
265
- lat_place_value = GRID_SIZE_DEGREES
266
- lng_place_value = GRID_SIZE_DEGREES
267
- # Adjust latitude and longitude so they fall into positive ranges and
268
- # get the offset for the required places.
269
- adjusted_latitude = (latitude + LATITUDE_MAX) % lat_place_value
270
- adjusted_longitude = (longitude + LONGITUDE_MAX) % lng_place_value
271
- (1..code_length).each do
272
- # Work out the row and column.
273
- row = (adjusted_latitude / (lat_place_value / GRID_ROWS)).floor
274
- col = (adjusted_longitude / (lng_place_value / GRID_COLUMNS)).floor
275
- lat_place_value /= GRID_ROWS
276
- lng_place_value /= GRID_COLUMNS
277
- adjusted_latitude -= row * lat_place_value
278
- adjusted_longitude -= col * lng_place_value
279
- code += CODE_ALPHABET[row * GRID_COLUMNS + col]
193
+ def build_code(digit_count, code, latitude, longitude)
194
+ lat_digit = latitude.to_i
195
+ lng_digit = longitude.to_i
196
+ if digit_count < 10
197
+ code << CODE_ALPHABET[lat_digit]
198
+ code << CODE_ALPHABET[lng_digit]
199
+ [digit_count + 2, latitude - lat_digit, longitude - lng_digit]
200
+ else
201
+ code << CODE_ALPHABET[4 * lat_digit + lng_digit]
202
+ [digit_count + 1, latitude - lat_digit, longitude - lng_digit]
280
203
  end
281
- code
282
204
  end
283
205
 
284
- def decode_pairs(code)
285
- # Get the latitude and longitude values. These will need correcting from
286
- # positive ranges.
287
- latitude = decode_pairs_sequence(code, 0.0)
288
- longitude = decode_pairs_sequence(code, 1.0)
289
- # Correct the values and set them into the CodeArea object.
290
- CodeArea.new(latitude[0] - LATITUDE_MAX,
291
- longitude[0] - LONGITUDE_MAX, latitude[1] - LATITUDE_MAX,
292
- longitude[1] - LONGITUDE_MAX, code.length)
206
+ def valid_length?(code)
207
+ !code.nil? && code.length >= 2 + SEPARATOR.length && code.split(SEPARATOR).last.length != 1
293
208
  end
294
209
 
295
- def decode_pairs_sequence(code, offset)
296
- i = 0
297
- value = 0
298
- while i * 2 + offset < code.length do
299
- value += DECODE[code[i * 2 + offset.floor].ord] * PAIR_RESOLUTIONS[i]
300
- i += 1
301
- end
302
- [value, value + PAIR_RESOLUTIONS[i - 1]]
210
+ def valid_separator?(code)
211
+ separator_idx = code.index(SEPARATOR)
212
+ code.count(SEPARATOR) == 1 && separator_idx <= SEPARATOR_POSITION && separator_idx.even?
303
213
  end
304
214
 
305
- def decode_grid(code)
306
- latitude_lo = 0.0
307
- longitude_lo = 0.0
308
- lat_place_value = GRID_SIZE_DEGREES
309
- lng_place_value = GRID_SIZE_DEGREES
310
- (0...code.length).each do |i|
311
- code_index = DECODE[code[i].ord]
312
- row = (code_index / GRID_COLUMNS).floor
313
- col = code_index % GRID_COLUMNS
314
-
315
- lat_place_value /= GRID_ROWS
316
- lng_place_value /= GRID_COLUMNS
317
-
318
- latitude_lo += row * lat_place_value
319
- longitude_lo += col * lng_place_value
215
+ def valid_padding?(code)
216
+ if code.include?(PADDING)
217
+ return false if code.start_with?(PADDING)
218
+ return false if code[-2..-1] != PADDING + SEPARATOR
219
+
220
+ paddings = code.scan(/#{PADDING}+/)
221
+ return false if !paddings.one? || paddings[0].length.odd?
222
+ return false if paddings[0].length > SEPARATOR_POSITION - 2
320
223
  end
321
- CodeArea.new(latitude_lo, longitude_lo, latitude_lo + lat_place_value,
322
- longitude_lo + lng_place_value, code.length)
224
+ true
323
225
  end
324
226
 
325
- def clip_latitude(latitude)
326
- [90.0, [-90.0, latitude].max].min
227
+ def valid_character?(code)
228
+ code.chars.each { |ch| return false if DECODE[ch.ord].nil? }
229
+ true
327
230
  end
328
231
 
329
- def compute_latitude_precision(code_length)
232
+ def invalid_length?(code_length)
233
+ code_length < 2 || (code_length < SEPARATOR_POSITION && code_length.odd?)
234
+ end
235
+
236
+ def padded(code)
237
+ code << PADDING * (SEPARATOR_POSITION - code.length) << SEPARATOR
238
+ end
239
+
240
+ def precision_by_length(code_length)
330
241
  if code_length <= 10
331
- 20 ** ((code_length / -2).to_i + 2)
242
+ precision = 20 ** ((code_length / -2).to_i + 2)
332
243
  else
333
- (20 ** -3) / (GRID_ROWS ** (code_length - 10))
244
+ precision = (20 ** -3) / (5 ** (code_length - 10))
334
245
  end
246
+ precision.to_r
247
+ end
248
+
249
+ def clip_latitude(latitude)
250
+ [90.0, [-90.0, latitude].max].min
335
251
  end
336
252
 
337
253
  def normalize_longitude(longitude)
338
- begin
339
- longitude += 360
340
- end while longitude < -180
341
- begin
254
+ until longitude < 180
342
255
  longitude -= 360
343
- end while longitude >= 180
256
+ end
257
+ until longitude >= -180
258
+ longitude += 360
259
+ end
344
260
  longitude
345
261
  end
346
-
347
262
  end
348
263
 
349
264
  end
@@ -1,3 +1,3 @@
1
1
  module PlusCodes
2
- VERSION = '0.1.2'
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/plus_codes.rb CHANGED
@@ -1,63 +1,26 @@
1
1
  # Plus+Codes is a Ruby implementation of Google Open Location Code(Plus+Codes).
2
- #
2
+ #
3
3
  # @author We-Ming Wu
4
4
  module PlusCodes
5
5
 
6
- # A separator used to break the code into two parts to aid memorability.
6
+ # A separator used to separate the code into two parts.
7
7
  SEPARATOR = '+'.freeze
8
8
 
9
- # The number of characters to place before the separator.
9
+ # The max number of characters can be placed before the separator.
10
10
  SEPARATOR_POSITION = 8
11
11
 
12
- # The character used to pad codes.
12
+ # The character used to pad a code
13
13
  PADDING = '0'.freeze
14
14
 
15
- # The character set used to encode the values.
15
+ # The character set used to encode coordinates.
16
16
  CODE_ALPHABET = '23456789CFGHJMPQRVWX'.freeze
17
17
 
18
- # The base to use to convert numbers to/from.
19
- ENCODING_BASE = CODE_ALPHABET.length
20
-
21
- # The maximum value for latitude in degrees.
22
- LATITUDE_MAX = 90
23
-
24
- # The maximum value for longitude in degrees.
25
- LONGITUDE_MAX = 180
26
-
27
- # Maximum code length using lat/lng pair encoding. The area of such a
28
- # code is approximately 13x13 meters (at the equator), and should be suitable
29
- # for identifying buildings. This excludes prefix and separator characters.
30
- PAIR_CODE_LENGTH = 10
31
-
32
- # The resolution values in degrees for each position in the lat/lng pair
33
- # encoding. These give the place value of each position, and therefore the
34
- # dimensions of the resulting area.
35
- PAIR_RESOLUTIONS = [20.0, 1.0, 0.05, 0.0025, 0.000125].freeze
36
-
37
- # Number of columns in the grid refinement method.
38
- GRID_COLUMNS = 4
39
-
40
- # Number of rows in the grid refinement method.
41
- GRID_ROWS = 5
42
-
43
- # Size of the initial grid in degrees.
44
- GRID_SIZE_DEGREES = 0.000125
45
-
46
- # Minimum length of a code that can be shortened.
47
- MIN_TRIMMABLE_CODE_LEN = 6
48
-
49
- # Decoder lookup table.
50
- # -2: illegal.
51
- # -1: Padding or Separator
52
- # >= 0: index in the alphabet.
53
- DECODE = [
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, -2, -2, -2, -2, -2,
56
- -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -2, -2, -2, -2,
57
- -1, -2, 0, 1, 2, 3, 4, 5, 6, 7, -2, -2, -2, -2, -2, -2,
58
- -2, -2, -2, 8, -2, -2, 9, 10, 11, -2, 12, -2, -2, 13, -2, -2,
59
- 14, 15, 16, -2, -2, -2, 17, 18, 19, -2, -2, -2, -2, -2, -2, -2,
60
- -2, -2, -2, 8, -2, -2, 9, 10, 11, -2, 12, -2, -2, 13, -2, -2,
61
- 14, 15, 16, -2, -2, -2, 17, 18, 19, -2, -2, -2, -2, -2, -2, -2,].freeze
18
+ # ASCII lookup table.
19
+ DECODE = (CODE_ALPHABET.chars + [PADDING, SEPARATOR]).reduce([]) do |ary, c|
20
+ ary[c.ord] = CODE_ALPHABET.index(c)
21
+ ary[c.downcase.ord] = CODE_ALPHABET.index(c)
22
+ ary[c.ord] ||= -1
23
+ ary
24
+ end.freeze
62
25
 
63
26
  end
@@ -27,12 +27,16 @@ class PlusCodesTest < Test::Unit::TestCase
27
27
  read_csv_lines('encodingTests.csv').each do |line|
28
28
  cols = line.split(',')
29
29
  code_area = @olc.decode(cols[0])
30
- code = @olc.encode(cols[1].to_f, cols[2].to_f, code_area.code_length)
30
+ if cols[0].index('0')
31
+ code = @olc.encode(cols[1].to_f, cols[2].to_f, cols[0].index('0'))
32
+ else
33
+ code = @olc.encode(cols[1].to_f, cols[2].to_f, cols[0].length - 1)
34
+ end
31
35
  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
+ assert_true((code_area.south_latitude - cols[3].to_f).abs < 0.001)
37
+ assert_true((code_area.west_longitude - cols[4].to_f).abs < 0.001)
38
+ assert_true((code_area.north_latitude - cols[5].to_f).abs < 0.001)
39
+ assert_true((code_area.east_longitude - cols[6].to_f).abs < 0.001)
36
40
  end
37
41
  end
38
42
 
@@ -48,20 +52,11 @@ class PlusCodesTest < Test::Unit::TestCase
48
52
  expanded = @olc.recover_nearest(short, lat, lng)
49
53
  assert_equal(code, expanded)
50
54
  end
55
+ @olc.shorten('9C3W9QCJ+2VX', 60.3701125, 10.202665625)
51
56
  end
52
57
 
53
58
  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
59
+ assert_equal('CFX3X2X2+X2RRRRJ', @olc.encode(90.0, 1.0, 15));
65
60
  end
66
61
 
67
62
  def test_exceptions
@@ -72,9 +67,18 @@ class PlusCodesTest < Test::Unit::TestCase
72
67
  @olc.recover_nearest('9C3W9QCJ-2VX', 51.3708675, -1.217765625)
73
68
  end
74
69
  @olc.recover_nearest('9C3W9QCJ+2VX', 51.3708675, -1.217765625)
70
+ assert_raise ArgumentError do
71
+ @olc.decode('sfdg')
72
+ end
73
+ assert_raise ArgumentError do
74
+ @olc.shorten('9C3W9Q+', 1, 2)
75
+ end
76
+ assert_raise ArgumentError do
77
+ @olc.shorten('9C3W9Q00+', 1, 2)
78
+ end
75
79
  end
76
80
 
77
- def test_valid?_with_speacial_case
81
+ def test_valid_with_speacial_case
78
82
  assert_false(@olc.valid?('3W00CJJJ+'))
79
83
  end
80
84
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plus_codes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wei-Ming Wu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-16 00:00:00.000000000 Z
11
+ date: 2015-09-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler