plus_codes 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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