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 +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
|