plus_codes 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +13 -0
- data/README.md +70 -0
- data/Rakefile +22 -0
- data/lib/plus_codes.rb +62 -0
- data/lib/plus_codes/code_area.rb +43 -0
- data/lib/plus_codes/open_location_code.rb +351 -0
- data/lib/plus_codes/version.rb +3 -0
- data/test/plus_codes_test.rb +72 -0
- data/test/test_data/encodingTests.csv +23 -0
- data/test/test_data/shortCodeTests.csv +15 -0
- data/test/test_data/validityTests.csv +23 -0
- data/test/test_helper.rb +16 -0
- metadata +132 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7430998fc47b7df06e1254f74ca980b1c33fbd12
|
4
|
+
data.tar.gz: f503a83dd1b483d57d2dc917b3abe5d95dc22ee0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 114f48473a81ead5f78179d0293892ff4c7f43b891b5c4cb9f4d4524806a80bacaf8242e042ee1f866c665f025f7381fc2a568b33b83bbe544b605e81a7519ab
|
7
|
+
data.tar.gz: c289c5db96763ff688bc8d6ed77d26b9442e1f248a29b0bd92099ecb659192239f4ec825a9780e240cb08b6a8cf019a08a6c186dfb005a1217d0122079e7843d
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2015 Wei-Ming Wu
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# PlusCodes
|
2
|
+
|
3
|
+
Ruby implementation of Google Open Location Code(Plus+Codes)
|
4
|
+
|
5
|
+
[Open Location Code project](https://github.com/google/open-location-code)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'plus_codes'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install plus_codes
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
require 'plus_codes/open_location_code'
|
27
|
+
|
28
|
+
olc = PlusCodes::OpenLocationCode.new
|
29
|
+
|
30
|
+
# Encodes the latitude and longitude into a Plus+Codes
|
31
|
+
code = olc.encode(47.0000625,8.0000625)
|
32
|
+
# => "8FVC2222+22"
|
33
|
+
|
34
|
+
# Encodes any latitude and longitude into a Plus+Codes with preferred length
|
35
|
+
code = olc.encode(47.0000625,8.0000625, 16)
|
36
|
+
# => "8FVC2222+22GCCCCC"
|
37
|
+
|
38
|
+
# Decodes a Plus+Codes back into coordinates
|
39
|
+
code_area = olc.decode(code)
|
40
|
+
puts code_area
|
41
|
+
# => lat_lo: 47.000062496 long_lo: 8.0000625 lat_hi: 47.000062504 long_hi: 8.000062530517578 code_len: 16
|
42
|
+
|
43
|
+
# Checks if a Plus+Codes is valid or not
|
44
|
+
olc.valid?(code)
|
45
|
+
# => true
|
46
|
+
|
47
|
+
# Checks if a Plus+Codes is full or not
|
48
|
+
olc.full?(code)
|
49
|
+
# => true
|
50
|
+
|
51
|
+
# Checks if a Plus+Codes is short or not
|
52
|
+
olc.short?(code)
|
53
|
+
# => false
|
54
|
+
|
55
|
+
# Shorten a Plus+Codes as possible by given reference latitude and longitude
|
56
|
+
olc.shorten('9C3W9QCJ+2VX', 51.3708675, -1.217765625)
|
57
|
+
# => "CJ+2VX"
|
58
|
+
|
59
|
+
# Extends a Plus+Codes by given reference latitude and longitude
|
60
|
+
olc.recover_nearest('CJ+2VX', 51.3708675, -1.217765625)
|
61
|
+
# => "9C3W9QCJ+2VX"
|
62
|
+
```
|
63
|
+
|
64
|
+
## Contributing
|
65
|
+
|
66
|
+
1. Fork it ( https://github.com/wnameless/plus_codes-ruby/fork )
|
67
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
68
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
69
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
70
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'yard'
|
9
|
+
YARD::Rake::YardocTask.new
|
10
|
+
|
11
|
+
Bundler::GemHelper.install_tasks
|
12
|
+
|
13
|
+
require 'rake/testtask'
|
14
|
+
|
15
|
+
Rake::TestTask.new(:test) do |t|
|
16
|
+
t.libs << 'lib'
|
17
|
+
t.libs << 'test'
|
18
|
+
t.pattern = 'test/**/*_test.rb'
|
19
|
+
t.verbose = false
|
20
|
+
end
|
21
|
+
|
22
|
+
task :default => :test
|
data/lib/plus_codes.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# Plus+Codes is a Ruby implementation of Google Open Location Code(Plus+Codes).
|
2
|
+
# @author We-Ming Wu
|
3
|
+
module PlusCodes
|
4
|
+
|
5
|
+
# A separator used to break the code into two parts to aid memorability.
|
6
|
+
SEPARATOR = '+'.freeze
|
7
|
+
|
8
|
+
# The number of characters to place before the separator.
|
9
|
+
SEPARATOR_POSITION = 8
|
10
|
+
|
11
|
+
# The character used to pad codes.
|
12
|
+
PADDING = '0'.freeze
|
13
|
+
|
14
|
+
# The character set used to encode the values.
|
15
|
+
CODE_ALPHABET = '23456789CFGHJMPQRVWX'.freeze
|
16
|
+
|
17
|
+
# The base to use to convert numbers to/from.
|
18
|
+
ENCODING_BASE = CODE_ALPHABET.length
|
19
|
+
|
20
|
+
# The maximum value for latitude in degrees.
|
21
|
+
LATITUDE_MAX = 90
|
22
|
+
|
23
|
+
# The maximum value for longitude in degrees.
|
24
|
+
LONGITUDE_MAX = 180
|
25
|
+
|
26
|
+
# Maximum code length using lat/lng pair encoding. The area of such a
|
27
|
+
# code is approximately 13x13 meters (at the equator), and should be suitable
|
28
|
+
# for identifying buildings. This excludes prefix and separator characters.
|
29
|
+
PAIR_CODE_LENGTH = 10
|
30
|
+
|
31
|
+
# The resolution values in degrees for each position in the lat/lng pair
|
32
|
+
# encoding. These give the place value of each position, and therefore the
|
33
|
+
# dimensions of the resulting area.
|
34
|
+
PAIR_RESOLUTIONS = [20.0, 1.0, 0.05, 0.0025, 0.000125].freeze
|
35
|
+
|
36
|
+
# Number of columns in the grid refinement method.
|
37
|
+
GRID_COLUMNS = 4
|
38
|
+
|
39
|
+
# Number of rows in the grid refinement method.
|
40
|
+
GRID_ROWS = 5
|
41
|
+
|
42
|
+
# Size of the initial grid in degrees.
|
43
|
+
GRID_SIZE_DEGREES = 0.000125
|
44
|
+
|
45
|
+
# Minimum length of a code that can be shortened.
|
46
|
+
MIN_TRIMMABLE_CODE_LEN = 6
|
47
|
+
|
48
|
+
# Decoder lookup table.
|
49
|
+
# -2: illegal.
|
50
|
+
# -1: Padding or Separator
|
51
|
+
# >= 0: index in the alphabet.
|
52
|
+
DECODE = [
|
53
|
+
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
|
54
|
+
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
|
55
|
+
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -2, -2, -2, -2,
|
56
|
+
-1, -2, 0, 1, 2, 3, 4, 5, 6, 7, -2, -2, -2, -2, -2, -2,
|
57
|
+
-2, -2, -2, 8, -2, -2, 9, 10, 11, -2, 12, -2, -2, 13, -2, -2,
|
58
|
+
14, 15, 16, -2, -2, -2, 17, 18, 19, -2, -2, -2, -2, -2, -2, -2,
|
59
|
+
-2, -2, -2, 8, -2, -2, 9, 10, 11, -2, 12, -2, -2, 13, -2, -2,
|
60
|
+
14, 15, 16, -2, -2, -2, 17, 18, 19, -2, -2, -2, -2, -2, -2, -2,].freeze
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module PlusCodes
|
2
|
+
|
3
|
+
# [CodeArea] contains coordinates of a decoded Open Location Code(Plus+Codes).
|
4
|
+
# The coordinates include the latitude and longitude of the lower left and
|
5
|
+
# upper right corners and the center of the bounding box for the area the
|
6
|
+
# code represents.
|
7
|
+
# Attributes:
|
8
|
+
# latitude_lo: The latitude of the SW corner in degrees.
|
9
|
+
# longitude_lo: The longitude of the SW corner in degrees.
|
10
|
+
# latitude_hi: The latitude of the NE corner in degrees.
|
11
|
+
# longitude_hi: The longitude of the NE corner in degrees.
|
12
|
+
# latitude_center: The latitude of the center in degrees.
|
13
|
+
# longitude_center: The longitude of the center in degrees.
|
14
|
+
# code_length: The number of significant characters that were in the code.
|
15
|
+
# @author We-Ming Wu
|
16
|
+
class CodeArea
|
17
|
+
attr_accessor :latitude_lo, :longitude_lo, :latitude_hi, :longitude_hi,
|
18
|
+
:code_length, :latitude_center, :longitude_center
|
19
|
+
|
20
|
+
# Creates a [CodeArea].
|
21
|
+
#
|
22
|
+
# @param latitude_lo [Numeric] the latitude of the SW corner in degrees
|
23
|
+
# @param longitude_lo [Numeric] the longitude of the SW corner in degrees
|
24
|
+
# @param latitude_hi [Numeric] the latitude of the NE corner in degrees
|
25
|
+
# @param longitude_hi [Numeric] the longitude of the NE corner in degrees
|
26
|
+
# @param code_length [Integer] the number of characters in the code, this excludes the separator
|
27
|
+
# @return [CodeArea] a code area which contains the coordinates
|
28
|
+
def initialize(latitude_lo, longitude_lo, latitude_hi, longitude_hi, code_length)
|
29
|
+
@latitude_lo = latitude_lo
|
30
|
+
@longitude_lo = longitude_lo
|
31
|
+
@latitude_hi = latitude_hi
|
32
|
+
@longitude_hi = longitude_hi
|
33
|
+
@code_length = code_length
|
34
|
+
@latitude_center = [@latitude_lo + (@latitude_hi - @latitude_lo) / 2, LATITUDE_MAX].min
|
35
|
+
@longitude_center = [@longitude_lo + (@longitude_hi - @longitude_lo) / 2, LONGITUDE_MAX].min
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
"lat_lo: #{@latitude_lo} long_lo: #{@longitude_lo} lat_hi: #{@latitude_hi} long_hi: #{@longitude_hi} code_len: #{@code_length}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,351 @@
|
|
1
|
+
require 'plus_codes'
|
2
|
+
require 'plus_codes/code_area'
|
3
|
+
|
4
|
+
module PlusCodes
|
5
|
+
|
6
|
+
# [OpenLocationCode] implements the Google Open Location Code(Plus+Codes) algorithm.
|
7
|
+
# @author We-Ming Wu
|
8
|
+
class OpenLocationCode
|
9
|
+
|
10
|
+
# Validates the given plus+codes.
|
11
|
+
#
|
12
|
+
# @param code [String] a plus+codes
|
13
|
+
# @return [TrueClass, FalseClass] true if the code is valid, false otherwise
|
14
|
+
def valid?(code)
|
15
|
+
return false if code.nil? || code.length <= 1
|
16
|
+
|
17
|
+
separator_index = code.index(SEPARATOR)
|
18
|
+
# There must be a single separator at an even index and position should be < SEPARATOR_POSITION.
|
19
|
+
return false if separator_index.nil? ||
|
20
|
+
separator_index != code.rindex(SEPARATOR) ||
|
21
|
+
separator_index > SEPARATOR_POSITION ||
|
22
|
+
separator_index % 2 == 1
|
23
|
+
|
24
|
+
# We can have an even number of padding characters before the separator,
|
25
|
+
# but then it must be the final character.
|
26
|
+
if code.include?(PADDING)
|
27
|
+
# Not allowed to start with them!
|
28
|
+
return false if code.index(PADDING) == 0
|
29
|
+
|
30
|
+
# There can only be one group and it must have even length.
|
31
|
+
pad_match = /(#{PADDING}+)/.match(code).to_a
|
32
|
+
return false if pad_match.length != 2
|
33
|
+
match = pad_match[1]
|
34
|
+
return false if match.length % 2 == 1 || match.length > SEPARATOR_POSITION - 2
|
35
|
+
|
36
|
+
# If the code is long enough to end with a separator, make sure it does.
|
37
|
+
return false if code[code.length - 1] != SEPARATOR
|
38
|
+
end
|
39
|
+
|
40
|
+
# If there are characters after the separator, make sure there isn't just
|
41
|
+
# one of them (not legal).
|
42
|
+
return false if code.length - separator_index - 1 == 1
|
43
|
+
|
44
|
+
# Check code contains only valid characters.
|
45
|
+
code.chars.each do |ch|
|
46
|
+
return false if ch.ord > DECODE.length || DECODE[ch.ord] < -1
|
47
|
+
end
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
# Checks if the given plus+codes is in short format.
|
52
|
+
#
|
53
|
+
# @param code [String] a plus+codes
|
54
|
+
# @return [TrueClass, FalseClass] true if the code is short, false otherwise
|
55
|
+
def short?(code)
|
56
|
+
return false unless valid?(code)
|
57
|
+
# If there are less characters than expected before the SEPARATOR.
|
58
|
+
code.index(SEPARATOR) >= 0 && code.index(SEPARATOR) < SEPARATOR_POSITION
|
59
|
+
end
|
60
|
+
|
61
|
+
# Checks if the given plus+codes is in full format.
|
62
|
+
#
|
63
|
+
# @param code [String] a plus+codes
|
64
|
+
# @return [TrueClass, FalseClass] true if the code is full, false otherwise
|
65
|
+
def full?(code)
|
66
|
+
return false unless valid?(code)
|
67
|
+
# If it's short, it's not full.
|
68
|
+
return false if short?(code)
|
69
|
+
|
70
|
+
# Work out what the first latitude character indicates for latitude.
|
71
|
+
first_lat_value = DECODE[code[0].ord] * ENCODING_BASE
|
72
|
+
# The code would decode to a latitude of >= 90 degrees.
|
73
|
+
return false if first_lat_value >= LATITUDE_MAX * 2
|
74
|
+
if code.length > 1
|
75
|
+
# Work out what the first longitude character indicates for longitude.
|
76
|
+
first_lng_value = DECODE[code[1].ord] * ENCODING_BASE
|
77
|
+
# The code would decode to a longitude of >= 180 degrees.
|
78
|
+
return false if first_lng_value >= LONGITUDE_MAX * 2
|
79
|
+
end
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
# Encodes given latitude and longitude with the optionally provided code length.
|
84
|
+
#
|
85
|
+
# @param latitude [Numeric] a latitude in degrees
|
86
|
+
# @param longitude [Numeric] a longitude in degrees
|
87
|
+
# @param code_length [Integer] the number of characters in the code, this excludes the separator
|
88
|
+
# @return [String] a plus+codes
|
89
|
+
def encode(latitude, longitude, code_length = PAIR_CODE_LENGTH)
|
90
|
+
if code_length < 2 ||
|
91
|
+
(code_length < SEPARATOR_POSITION && code_length % 2 == 1)
|
92
|
+
raise ArgumentError, "Invalid Open Location Code length: #{code_length}"
|
93
|
+
end
|
94
|
+
|
95
|
+
latitude = clip_latitude(latitude)
|
96
|
+
longitude = normalize_longitude(longitude)
|
97
|
+
if latitude == 90
|
98
|
+
latitude = latitude - compute_latitude_precision(code_length).to_f
|
99
|
+
p latitude
|
100
|
+
end
|
101
|
+
code = encode_pairs(latitude, longitude, [code_length, PAIR_CODE_LENGTH].min)
|
102
|
+
# If the requested length indicates we want grid refined codes.
|
103
|
+
code += encode_grid(latitude, longitude, code_length - PAIR_CODE_LENGTH) if code_length > PAIR_CODE_LENGTH
|
104
|
+
code
|
105
|
+
end
|
106
|
+
|
107
|
+
# Decodes the given plus+codes in to a [CodeArea].
|
108
|
+
#
|
109
|
+
# @param code [String] a plus+codes
|
110
|
+
# @return [CodeArea] a code area which contains the coordinates
|
111
|
+
def decode(code)
|
112
|
+
raise ArgumentError,
|
113
|
+
"Passed Open Location Code is not a valid full code: #{code}" unless full?(code)
|
114
|
+
|
115
|
+
# Strip out separator character (we've already established the code is
|
116
|
+
# valid so the maximum is one), padding characters and convert to upper
|
117
|
+
# case.
|
118
|
+
code = code.gsub(SEPARATOR, '')
|
119
|
+
code = code.gsub(/#{PADDING}+/, '')
|
120
|
+
code = code.upcase
|
121
|
+
# Decode the lat/lng pair component.
|
122
|
+
code_area = decode_pairs(code[0...[code.length, PAIR_CODE_LENGTH].min])
|
123
|
+
# If there is a grid refinement component, decode that.
|
124
|
+
return code_area if code.length <= PAIR_CODE_LENGTH
|
125
|
+
|
126
|
+
grid_area = decode_grid(code[PAIR_CODE_LENGTH..-1])
|
127
|
+
CodeArea.new(code_area.latitude_lo + grid_area.latitude_lo,
|
128
|
+
code_area.longitude_lo + grid_area.longitude_lo,
|
129
|
+
code_area.latitude_lo + grid_area.latitude_hi,
|
130
|
+
code_area.longitude_lo + grid_area.longitude_hi,
|
131
|
+
code_area.code_length + grid_area.code_length)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Finds the full plus+codes from given short plus+codes, reference latitude and longitude.
|
135
|
+
#
|
136
|
+
# @param code [String] a plus+codes
|
137
|
+
# @param reference_latitude [Numeric] a reference latitude in degrees
|
138
|
+
# @param reference_longitude [Numeric] a reference longitude in degrees
|
139
|
+
# @return [String] a plus+codes
|
140
|
+
def recover_nearest(short_code, reference_latitude, reference_longitude)
|
141
|
+
unless short?(short_code)
|
142
|
+
if full?(short_code)
|
143
|
+
return short_code
|
144
|
+
else
|
145
|
+
raise ArgumentError, 'ValueError: Passed short code is not valid: ' + short_code
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Ensure that latitude and longitude are valid.
|
150
|
+
reference_latitude = clip_latitude(reference_latitude)
|
151
|
+
reference_longitude = normalize_longitude(reference_longitude)
|
152
|
+
|
153
|
+
# Clean up the passed code.
|
154
|
+
short_code = short_code.upcase
|
155
|
+
# Compute the number of digits we need to recover.
|
156
|
+
padding_length = SEPARATOR_POSITION - short_code.index(SEPARATOR)
|
157
|
+
# The resolution (height and width) of the padded area in degrees.
|
158
|
+
resolution = 20 ** (2 - (padding_length / 2))
|
159
|
+
# Distance from the center to an edge (in degrees).
|
160
|
+
area_to_edge = resolution / 2.0
|
161
|
+
|
162
|
+
# Now round down the reference latitude and longitude to the resolution.
|
163
|
+
rounded_latitude = (reference_latitude / resolution).floor * resolution
|
164
|
+
rounded_longitude = (reference_longitude / resolution).floor * resolution
|
165
|
+
|
166
|
+
# Use the reference location to pad the supplied short code and decode it.
|
167
|
+
code_area = decode(
|
168
|
+
encode(rounded_latitude, rounded_longitude).slice(0, padding_length) +
|
169
|
+
short_code)
|
170
|
+
# How many degrees latitude is the code from the reference? If it is more
|
171
|
+
# than half the resolution, we need to move it east or west.
|
172
|
+
degrees_difference = code_area.latitude_center - reference_latitude
|
173
|
+
if degrees_difference > area_to_edge
|
174
|
+
# If the center of the short code is more than half a cell east,
|
175
|
+
# then the best match will be one position west.
|
176
|
+
code_area.latitude_center -= resolution
|
177
|
+
elsif degrees_difference < -area_to_edge
|
178
|
+
# If the center of the short code is more than half a cell west,
|
179
|
+
# then the best match will be one position east.
|
180
|
+
code_area.latitude_center += resolution
|
181
|
+
end
|
182
|
+
|
183
|
+
# How many degrees longitude is the code from the reference?
|
184
|
+
degrees_difference = code_area.longitude_center - reference_longitude
|
185
|
+
if degrees_difference > area_to_edge
|
186
|
+
code_area.longitude_center -= resolution
|
187
|
+
elsif degrees_difference < -area_to_edge
|
188
|
+
code_area.longitude_center += resolution
|
189
|
+
end
|
190
|
+
encode(code_area.latitude_center, code_area.longitude_center, code_area.code_length)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Shortens the given full plus+codes by provided reference latitude and longitude.
|
194
|
+
#
|
195
|
+
# @param code [String] a plus+codes
|
196
|
+
# @param latitude [Numeric] a latitude in degrees
|
197
|
+
# @param longitude [Numeric] a longitude in degrees
|
198
|
+
# @return [String] a short plus+codes
|
199
|
+
def shorten(code, latitude, longitude)
|
200
|
+
raise ArgumentError,
|
201
|
+
"ValueError: Passed code is not valid and full: #{code}" unless full?(code)
|
202
|
+
raise ArgumentError,
|
203
|
+
"ValueError: Cannot shorten padded codes: #{code}" unless code.index(PADDING).nil?
|
204
|
+
|
205
|
+
code = code.upcase
|
206
|
+
code_area = decode(code)
|
207
|
+
if code_area.code_length < MIN_TRIMMABLE_CODE_LEN
|
208
|
+
raise RangeError,
|
209
|
+
"ValueError: Code length must be at least #{MIN_TRIMMABLE_CODE_LEN}"
|
210
|
+
end
|
211
|
+
# Ensure that latitude and longitude are valid.
|
212
|
+
latitude = clip_latitude(latitude)
|
213
|
+
longitude = normalize_longitude(longitude)
|
214
|
+
# How close are the latitude and longitude to the code center.
|
215
|
+
range = [(code_area.latitude_center - latitude).abs,
|
216
|
+
(code_area.longitude_center - longitude).abs].max
|
217
|
+
i = PAIR_RESOLUTIONS.length - 2
|
218
|
+
while i >= 1 do
|
219
|
+
# Check if we're close enough to shorten. The range must be less than 1/2
|
220
|
+
# the resolution to shorten at all, and we want to allow some safety, so
|
221
|
+
# use 0.3 instead of 0.5 as a multiplier.
|
222
|
+
return code[(i + 1) * 2..-1] if range < (PAIR_RESOLUTIONS[i] * 0.3)
|
223
|
+
# Trim it.
|
224
|
+
i -= 1
|
225
|
+
end
|
226
|
+
code
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
|
231
|
+
def encode_pairs(latitude, longitude, code_length)
|
232
|
+
code = ''
|
233
|
+
# Adjust latitude and longitude so they fall into positive ranges.
|
234
|
+
adjusted_latitude = latitude + LATITUDE_MAX
|
235
|
+
adjusted_longitude = longitude + LONGITUDE_MAX
|
236
|
+
# Count digits - can't use string length because it may include a separator
|
237
|
+
# character.
|
238
|
+
digit_count = 0
|
239
|
+
while (digit_count < code_length) do
|
240
|
+
# Provides the value of digits in this place in decimal degrees.
|
241
|
+
place_value = PAIR_RESOLUTIONS[(digit_count / 2).to_i]
|
242
|
+
# Do the latitude - gets the digit for this place and subtracts that for
|
243
|
+
# the next digit.
|
244
|
+
digit_value = (adjusted_latitude / place_value).to_i
|
245
|
+
adjusted_latitude -= digit_value * place_value
|
246
|
+
code += CODE_ALPHABET[digit_value]
|
247
|
+
digit_count += 1
|
248
|
+
# And do the longitude - gets the digit for this place and subtracts that
|
249
|
+
# for the next digit.
|
250
|
+
digit_value = (adjusted_longitude / place_value).to_i
|
251
|
+
adjusted_longitude -= digit_value * place_value
|
252
|
+
code += CODE_ALPHABET[digit_value]
|
253
|
+
digit_count +=1
|
254
|
+
# Should we add a separator here?
|
255
|
+
code += SEPARATOR if digit_count == SEPARATOR_POSITION && digit_count < code_length
|
256
|
+
end
|
257
|
+
# If necessary, Add padding.
|
258
|
+
if code.length < SEPARATOR_POSITION
|
259
|
+
code = code + (PADDING * (SEPARATOR_POSITION - code.length))
|
260
|
+
end
|
261
|
+
code = code + SEPARATOR if code.length == SEPARATOR_POSITION
|
262
|
+
code
|
263
|
+
end
|
264
|
+
|
265
|
+
def encode_grid(latitude, longitude, code_length)
|
266
|
+
code = ''
|
267
|
+
lat_place_value = GRID_SIZE_DEGREES
|
268
|
+
lng_place_value = GRID_SIZE_DEGREES
|
269
|
+
# Adjust latitude and longitude so they fall into positive ranges and
|
270
|
+
# get the offset for the required places.
|
271
|
+
adjusted_latitude = (latitude + LATITUDE_MAX) % lat_place_value
|
272
|
+
adjusted_longitude = (longitude + LONGITUDE_MAX) % lng_place_value
|
273
|
+
(1..code_length).each do
|
274
|
+
# Work out the row and column.
|
275
|
+
row = (adjusted_latitude / (lat_place_value / GRID_ROWS)).floor
|
276
|
+
col = (adjusted_longitude / (lng_place_value / GRID_COLUMNS)).floor
|
277
|
+
lat_place_value /= GRID_ROWS
|
278
|
+
lng_place_value /= GRID_COLUMNS
|
279
|
+
adjusted_latitude -= row * lat_place_value
|
280
|
+
adjusted_longitude -= col * lng_place_value
|
281
|
+
code += CODE_ALPHABET[row * GRID_COLUMNS + col]
|
282
|
+
end
|
283
|
+
code
|
284
|
+
end
|
285
|
+
|
286
|
+
def decode_pairs(code)
|
287
|
+
# Get the latitude and longitude values. These will need correcting from
|
288
|
+
# positive ranges.
|
289
|
+
latitude = decode_pairs_sequence(code, 0.0)
|
290
|
+
longitude = decode_pairs_sequence(code, 1.0)
|
291
|
+
# Correct the values and set them into the CodeArea object.
|
292
|
+
CodeArea.new(latitude[0] - LATITUDE_MAX,
|
293
|
+
longitude[0] - LONGITUDE_MAX, latitude[1] - LATITUDE_MAX,
|
294
|
+
longitude[1] - LONGITUDE_MAX, code.length)
|
295
|
+
end
|
296
|
+
|
297
|
+
def decode_pairs_sequence(code, offset)
|
298
|
+
i = 0
|
299
|
+
value = 0
|
300
|
+
while i * 2 + offset < code.length do
|
301
|
+
value += DECODE[code[i * 2 + offset.floor].ord] * PAIR_RESOLUTIONS[i]
|
302
|
+
i += 1
|
303
|
+
end
|
304
|
+
[value, value + PAIR_RESOLUTIONS[i - 1]]
|
305
|
+
end
|
306
|
+
|
307
|
+
def decode_grid(code)
|
308
|
+
latitude_lo = 0.0
|
309
|
+
longitude_lo = 0.0
|
310
|
+
lat_place_value = GRID_SIZE_DEGREES
|
311
|
+
lng_place_value = GRID_SIZE_DEGREES
|
312
|
+
(0...code.length).each do |i|
|
313
|
+
code_index = DECODE[code[i].ord]
|
314
|
+
row = (code_index / GRID_COLUMNS).floor()
|
315
|
+
col = code_index % GRID_COLUMNS
|
316
|
+
|
317
|
+
lat_place_value /= GRID_ROWS
|
318
|
+
lng_place_value /= GRID_COLUMNS
|
319
|
+
|
320
|
+
latitude_lo += row * lat_place_value
|
321
|
+
longitude_lo += col * lng_place_value
|
322
|
+
end
|
323
|
+
CodeArea.new(latitude_lo, longitude_lo, latitude_lo + lat_place_value,
|
324
|
+
longitude_lo + lng_place_value, code.length)
|
325
|
+
end
|
326
|
+
|
327
|
+
def clip_latitude(latitude)
|
328
|
+
[90.0, [-90.0, latitude].max].min
|
329
|
+
end
|
330
|
+
|
331
|
+
def compute_latitude_precision(code_length)
|
332
|
+
if code_length <= 10
|
333
|
+
20 ** ((code_length / -2).to_i + 2)
|
334
|
+
else
|
335
|
+
(20 ** -3) / (GRID_ROWS ** (code_length - 10))
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def normalize_longitude(longitude)
|
340
|
+
begin
|
341
|
+
longitude += 360
|
342
|
+
end while longitude < -180
|
343
|
+
begin
|
344
|
+
longitude -= 360
|
345
|
+
end while longitude >= 180
|
346
|
+
longitude
|
347
|
+
end
|
348
|
+
|
349
|
+
end
|
350
|
+
|
351
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'plus_codes/open_location_code'
|
3
|
+
|
4
|
+
class PlusCodesTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@test_data_folder_path = File.join(File.dirname(__FILE__), 'test_data')
|
8
|
+
@olc = PlusCodes::OpenLocationCode.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_validity
|
12
|
+
read_csv_lines('validityTests.csv').each do |line|
|
13
|
+
cols = line.split(',')
|
14
|
+
code = cols[0]
|
15
|
+
is_valid = cols[1] == 'true'
|
16
|
+
is_short = cols[2] == 'true'
|
17
|
+
is_full = cols[3] == 'true'
|
18
|
+
is_valid_olc = @olc.valid?(code)
|
19
|
+
is_short_olc = @olc.short?(code)
|
20
|
+
is_full_olc = @olc.full?(code)
|
21
|
+
result = is_full == is_full_olc && is_short_olc == is_short && is_valid_olc == is_valid
|
22
|
+
assert_true(result)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_encode_decode
|
27
|
+
read_csv_lines('encodingTests.csv').each do |line|
|
28
|
+
cols = line.split(',')
|
29
|
+
code_area = @olc.decode(cols[0])
|
30
|
+
code = @olc.encode(cols[1].to_f, cols[2].to_f, code_area.code_length)
|
31
|
+
assert_equal(cols[0], code)
|
32
|
+
assert_true((code_area.latitude_lo - cols[3].to_f).abs < 0.001)
|
33
|
+
assert_true((code_area.longitude_lo - cols[4].to_f).abs < 0.001)
|
34
|
+
assert_true((code_area.latitude_hi - cols[5].to_f).abs < 0.001)
|
35
|
+
assert_true((code_area.longitude_hi - cols[6].to_f).abs < 0.001)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_shorten
|
40
|
+
read_csv_lines('shortCodeTests.csv').each do |line|
|
41
|
+
cols = line.split(',')
|
42
|
+
code = cols[0]
|
43
|
+
lat = cols[1].to_f
|
44
|
+
lng = cols[2].to_f
|
45
|
+
short_code = cols[3]
|
46
|
+
short = @olc.shorten(code, lat, lng)
|
47
|
+
assert_equal(short_code, short)
|
48
|
+
expanded = @olc.recover_nearest(short, lat, lng)
|
49
|
+
assert_equal(code, expanded)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_longer_encoding_with_speacial_case
|
54
|
+
assert_equal('CFX3X2X2+X2XXXXQ', @olc.encode(90.0, 1.0, 15));
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_code_area_to_s
|
58
|
+
read_csv_lines('encodingTests.csv').each do |line|
|
59
|
+
cols = line.split(',')
|
60
|
+
code_area = @olc.decode(cols[0])
|
61
|
+
assert_equal("lat_lo: #{code_area.latitude_lo} long_lo: #{code_area.longitude_lo} " <<
|
62
|
+
"lat_hi: #{code_area.latitude_hi} long_hi: #{code_area.longitude_hi} " <<
|
63
|
+
"code_len: #{code_area.code_length}", code_area.to_s)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def read_csv_lines(csv_file)
|
68
|
+
f = File.open(File.join(@test_data_folder_path, csv_file), 'r')
|
69
|
+
f.each_line.lazy.select { |line| line !~ /^\s*#/ }.map { |line| line.chop }
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Test encoding and decoding Open Location Codes.
|
2
|
+
#
|
3
|
+
# Provides test cases for encoding latitude and longitude to codes and expected
|
4
|
+
# values for decoding.
|
5
|
+
#
|
6
|
+
# Format:
|
7
|
+
# code,lat,lng,latLo,lngLo,latHi,lngHi
|
8
|
+
7FG49Q00+,20.375,2.775,20.35,2.75,20.4,2.8
|
9
|
+
7FG49QCJ+2V,20.3700625,2.7821875,20.37,2.782125,20.370125,2.78225
|
10
|
+
7FG49QCJ+2VX,20.3701125,2.782234375,20.3701,2.78221875,20.370125,2.78225
|
11
|
+
7FG49QCJ+2VXGJ,20.3701135,2.78223535156,20.370113,2.782234375,20.370114,2.78223632813
|
12
|
+
8FVC2222+22,47.0000625,8.0000625,47.0,8.0,47.000125,8.000125
|
13
|
+
4VCPPQGP+Q9,-41.2730625,174.7859375,-41.273125,174.785875,-41.273,174.786
|
14
|
+
62G20000+,0.5,-179.5,0.0,-180.0,1,-179
|
15
|
+
22220000+,-89.5,-179.5,-90,-180,-89,-179
|
16
|
+
7FG40000+,20.5,2.5,20.0,2.0,21.0,3.0
|
17
|
+
22222222+22,-89.9999375,-179.9999375,-90.0,-180.0,-89.999875,-179.999875
|
18
|
+
6VGX0000+,0.5,179.5,0,179,1,180
|
19
|
+
# Special cases over 90 latitude and 180 longitude
|
20
|
+
CFX30000+,90,1,89,1,90,2
|
21
|
+
CFX30000+,92,1,89,1,90,2
|
22
|
+
62H20000+,1,180,1,-180,2,-179
|
23
|
+
62H30000+,1,181,1,-179,2,-178
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Test shortening and extending codes.
|
2
|
+
#
|
3
|
+
# Format:
|
4
|
+
# full code,lat,lng,shortcode
|
5
|
+
9C3W9QCJ+2VX,51.3701125,-1.217765625,+2VX
|
6
|
+
# Adjust so we can't trim by 8 (+/- .000755)
|
7
|
+
9C3W9QCJ+2VX,51.3708675,-1.217765625,CJ+2VX
|
8
|
+
9C3W9QCJ+2VX,51.3693575,-1.217765625,CJ+2VX
|
9
|
+
9C3W9QCJ+2VX,51.3701125,-1.218520625,CJ+2VX
|
10
|
+
9C3W9QCJ+2VX,51.3701125,-1.217010625,CJ+2VX
|
11
|
+
# Adjust so we can't trim by 6 (+/- .0151)
|
12
|
+
9C3W9QCJ+2VX,51.3852125,-1.217765625,9QCJ+2VX
|
13
|
+
9C3W9QCJ+2VX,51.3550125,-1.217765625,9QCJ+2VX
|
14
|
+
9C3W9QCJ+2VX,51.3701125,-1.232865625,9QCJ+2VX
|
15
|
+
9C3W9QCJ+2VX,51.3701125,-1.202665625,9QCJ+2VX
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Test data for validity tests.
|
2
|
+
# Format of each line is:
|
3
|
+
# code,isValid,isShort,isFull
|
4
|
+
# Valid full codes:
|
5
|
+
8FWC2345+G6,true,false,true
|
6
|
+
8FWC2345+G6G,true,false,true
|
7
|
+
8fwc2345+,true,false,true
|
8
|
+
8FWCX400+,true,false,true
|
9
|
+
# Valid short codes:
|
10
|
+
WC2345+G6g,true,true,false
|
11
|
+
2345+G6,true,true,false
|
12
|
+
45+G6,true,true,false
|
13
|
+
+G6,true,true,false
|
14
|
+
# Invalid codes
|
15
|
+
G+,false,false,false
|
16
|
+
+,false,false,false
|
17
|
+
8FWC2345+G,false,false,false
|
18
|
+
8FWC2_45+G6,false,false,false
|
19
|
+
8FWC2η45+G6,false,false,false
|
20
|
+
8FWC2345+G6+,false,false,false
|
21
|
+
8FWC2300+G6,false,false,false
|
22
|
+
WC2300+G6g,false,false,false
|
23
|
+
WC2345+G,false,false,false
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
require 'simplecov'
|
4
|
+
SimpleCov.start
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'test/unit'
|
13
|
+
|
14
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
15
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
16
|
+
require 'plus_codes'
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: plus_codes
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Wei-Ming Wu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-09-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: test-unit
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: simplecov
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: yard
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Ruby implementation of Google Open Location Code(Plus+Codes)
|
84
|
+
email:
|
85
|
+
- wnameless@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- LICENSE
|
91
|
+
- README.md
|
92
|
+
- Rakefile
|
93
|
+
- lib/plus_codes.rb
|
94
|
+
- lib/plus_codes/code_area.rb
|
95
|
+
- lib/plus_codes/open_location_code.rb
|
96
|
+
- lib/plus_codes/version.rb
|
97
|
+
- test/plus_codes_test.rb
|
98
|
+
- test/test_data/encodingTests.csv
|
99
|
+
- test/test_data/shortCodeTests.csv
|
100
|
+
- test/test_data/validityTests.csv
|
101
|
+
- test/test_helper.rb
|
102
|
+
homepage: https://github.com/wnameless/plus_codes-ruby
|
103
|
+
licenses:
|
104
|
+
- Apache License, Version 2.0
|
105
|
+
metadata: {}
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
requirements: []
|
121
|
+
rubyforge_project:
|
122
|
+
rubygems_version: 2.4.5
|
123
|
+
signing_key:
|
124
|
+
specification_version: 4
|
125
|
+
summary: Ruby implementation of Google Open Location Code(Plus+Codes)
|
126
|
+
test_files:
|
127
|
+
- test/plus_codes_test.rb
|
128
|
+
- test/test_data/encodingTests.csv
|
129
|
+
- test/test_data/shortCodeTests.csv
|
130
|
+
- test/test_data/validityTests.csv
|
131
|
+
- test/test_helper.rb
|
132
|
+
has_rdoc:
|