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 +4 -4
- data/Rakefile +2 -3
- data/lib/plus_codes/code_area.rb +20 -26
- data/lib/plus_codes/open_location_code.rb +161 -246
- data/lib/plus_codes/version.rb +1 -1
- data/lib/plus_codes.rb +12 -49
- data/test/plus_codes_test.rb +21 -17
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6d973ca48c7cbdf04ae78ceae2e41e0788ead86
|
4
|
+
data.tar.gz: 09713b9cbfcbad05b8d24fe8740104f14d8bc031
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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'
|
data/lib/plus_codes/code_area.rb
CHANGED
@@ -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
|
-
#
|
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 :
|
19
|
-
|
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
|
24
|
-
# @param
|
25
|
-
# @param
|
26
|
-
# @param
|
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(
|
30
|
-
@
|
31
|
-
@
|
32
|
-
@
|
33
|
-
@
|
34
|
-
@
|
35
|
-
@
|
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
|
40
|
-
|
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
|
-
#
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
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 =
|
88
|
-
|
89
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
62
|
+
|
63
|
+
digit < SEPARATOR_POSITION ? padded(code) : code
|
103
64
|
end
|
104
65
|
|
105
|
-
# Decodes
|
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
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
#
|
106
|
+
# Recovers a full Open Location Code(Plus+Codes) from a short code and a reference location.
|
133
107
|
#
|
134
|
-
# @param
|
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
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
182
|
-
|
183
|
-
if
|
184
|
-
|
185
|
-
elsif
|
186
|
-
|
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
|
-
|
142
|
+
|
143
|
+
encode(latitude, longitude, code.length - SEPARATOR.length)
|
189
144
|
end
|
190
145
|
|
191
|
-
#
|
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
|
-
|
154
|
+
"Open Location Code(Plus+Codes) is a valid full code: #{code}" unless full?(code)
|
200
155
|
raise ArgumentError,
|
201
|
-
|
156
|
+
"Cannot shorten padded codes: #{code}" unless code.index(PADDING).nil?
|
202
157
|
|
203
|
-
code = code.upcase
|
204
158
|
code_area = decode(code)
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
-
|
166
|
+
|
167
|
+
code.upcase
|
225
168
|
end
|
226
169
|
|
227
170
|
private
|
228
171
|
|
229
|
-
def
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
-
|
260
|
-
code
|
190
|
+
[latitude, longitude]
|
261
191
|
end
|
262
192
|
|
263
|
-
def
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
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
|
285
|
-
|
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
|
296
|
-
|
297
|
-
|
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
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
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
|
-
|
322
|
-
longitude_lo + lng_place_value, code.length)
|
224
|
+
true
|
323
225
|
end
|
324
226
|
|
325
|
-
def
|
326
|
-
|
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
|
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) / (
|
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
|
-
|
339
|
-
longitude += 360
|
340
|
-
end while longitude < -180
|
341
|
-
begin
|
254
|
+
until longitude < 180
|
342
255
|
longitude -= 360
|
343
|
-
end
|
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
|
data/lib/plus_codes/version.rb
CHANGED
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
|
6
|
+
# A separator used to separate the code into two parts.
|
7
7
|
SEPARATOR = '+'.freeze
|
8
8
|
|
9
|
-
# The number of characters
|
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
|
12
|
+
# The character used to pad a code
|
13
13
|
PADDING = '0'.freeze
|
14
14
|
|
15
|
-
# The character set used to encode
|
15
|
+
# The character set used to encode coordinates.
|
16
16
|
CODE_ALPHABET = '23456789CFGHJMPQRVWX'.freeze
|
17
17
|
|
18
|
-
#
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
data/test/plus_codes_test.rb
CHANGED
@@ -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
|
-
|
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.
|
33
|
-
assert_true((code_area.
|
34
|
-
assert_true((code_area.
|
35
|
-
assert_true((code_area.
|
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+
|
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
|
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.
|
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-
|
11
|
+
date: 2015-09-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|