Skip to content

Commit 5461af7

Browse files
authored
Merge pull request #74 from joh12041/master
Follow-up on PR #31 - automate python testing and update recoverNearest method
2 parents ee7425e + 8315940 commit 5461af7

File tree

6 files changed

+178
-70
lines changed

6 files changed

+178
-70
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ env:
3030
- TEST_DIR=js
3131
- TEST_DIR=go
3232
- TEST_DIR=ruby
33+
- TEST_DIR=python
3334
- TEST_DIR=rust
3435

3536
# Test script to run. This is called once for each TEST_DIR value above.

python/BUILD

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
py_library(
2+
name = 'openlocationcode',
3+
srcs = ['openlocationcode.py'],
4+
)
5+
6+
py_test(
7+
name = 'openlocationcode_test',
8+
srcs = ['openlocationcode_test.py'],
9+
size = 'small',
10+
deps = [':openlocationcode'],
11+
data = ['//test_data:test_data'],
12+
)

python/pluscodes.py renamed to python/openlocationcode.py

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,62 @@
1+
# -*- coding: utf-8 -*-
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.
14+
# ==============================================================================
15+
#
16+
#
17+
# Convert locations to and from short codes.
18+
#
19+
# Open Location Codes are short, 10-11 character codes that can be used instead
20+
# of street addresses. The codes can be generated and decoded offline, and use
21+
# a reduced character set that minimises the chance of codes including words.
22+
#
23+
# Codes are able to be shortened relative to a nearby location. This means that
24+
# in many cases, only four to seven characters of the code are needed.
25+
# To recover the original code, the same location is not required, as long as
26+
# a nearby location is provided.
27+
#
28+
# Codes represent rectangular areas rather than points, and the longer the
29+
# code, the smaller the area. A 10 character code represents a 13.5x13.5
30+
# meter area (at the equator. An 11 character code represents approximately
31+
# a 2.8x3.5 meter area.
32+
#
33+
# Two encoding algorithms are used. The first 10 characters are pairs of
34+
# characters, one for latitude and one for latitude, using base 20. Each pair
35+
# reduces the area of the code by a factor of 400. Only even code lengths are
36+
# sensible, since an odd-numbered length would have sides in a ratio of 20:1.
37+
#
38+
# At position 11, the algorithm changes so that each character selects one
39+
# position from a 4x5 grid. This allows single-character refinements.
40+
#
41+
# Examples:
42+
#
43+
# Encode a location, default accuracy:
44+
# var code = olc.encode(47.365590, 8.524997);
45+
#
46+
# Encode a location using one stage of additional refinement:
47+
# var code = olc.encode(47.365590, 8.524997, 11);
48+
#
49+
# Decode a full code:
50+
# var coord = olc.decode(code);
51+
# var msg = 'Center is ' + coord.latitudeCenter + ',' + coord.longitudeCenter;
52+
#
53+
# Attempt to trim the first characters from a code:
54+
# var shortCode = olc.shorten('8FVC9G8F+6X', 47.5, 8.5);
55+
#
56+
# Recover the full code from a short code:
57+
# var code = olc.recoverNearest('9G8F+6X', 47.4, 8.6);
58+
# var code = olc.recoverNearest('8F+6X', 47.4, 8.6);
59+
160
import re
261
import math
362

@@ -218,11 +277,8 @@ def decode(code):
218277
to find the nearest matching full code.
219278
Returns:
220279
The nearest full Open Location Code to the reference location that matches
221-
the short code. Note that the returned code may not have the same
222-
computed characters as the reference location. This is because it returns
223-
the nearest match, not necessarily the match within the same cell. If the
224-
passed code was not a valid short code, but was a valid full code, it is
225-
returned unchanged.
280+
the short code. If the passed code was not a valid short code, but was a
281+
valid full code, it is returned unchanged.
226282
"""
227283
def recoverNearest(shortcode, referenceLatitude, referenceLongitude):
228284
if not isShort(shortcode):
@@ -238,11 +294,8 @@ def recoverNearest(shortcode, referenceLatitude, referenceLongitude):
238294
resolution = pow(20, 2 - (paddingLength / 2))
239295
# Distance from the center to an edge (in degrees).
240296
areaToEdge = resolution / 2.0
241-
# Now round down the reference latitude and longitude to the resolution.
242-
roundedLatitude = math.floor(referenceLatitude / resolution) * resolution
243-
roundedLongitude = math.floor(referenceLongitude / resolution) * resolution
244297
# Use the reference location to pad the supplied short code and decode it.
245-
codeArea = decode(encode(roundedLatitude, roundedLongitude)[0:paddingLength] + shortcode)
298+
codeArea = decode(encode(referenceLatitude, referenceLongitude)[0:paddingLength] + shortcode)
246299
# How many degrees latitude is the code from the reference? If it is more
247300
# than half the resolution, we need to move it east or west.
248301
degreesDifference = codeArea.latitudeCenter - referenceLatitude
@@ -313,7 +366,7 @@ def shorten(code,latitude,longitude):
313366
latitude: A latitude in signed decimal degrees.
314367
"""
315368
def clipLatitude(latitude):
316-
return min(90,max(-90,latitude))
369+
return min(90, max(-90, latitude))
317370

318371
"""
319372
Compute the latitude precision value for a given code length. Lengths <=
@@ -323,7 +376,7 @@ def clipLatitude(latitude):
323376
"""
324377
def computeLatitutePrecision(codeLength):
325378
if codeLength <= 10:
326-
return pow(20,math.floor((codeLength / -2) + 2))
379+
return pow(20, math.floor((codeLength / -2) + 2))
327380
return pow(20, -3) / pow(GRID_ROWS_, codeLength - 10)
328381

329382
"""
@@ -359,16 +412,16 @@ def encodePairs(latitude, longitude, codeLength):
359412
digitCount = 0
360413
while digitCount < codeLength:
361414
# Provides the value of digits in this place in decimal degrees.
362-
placeValue = PAIR_RESOLUTIONS_[math.floor(digitCount / 2)]
415+
placeValue = PAIR_RESOLUTIONS_[int(math.floor(digitCount / 2))]
363416
# Do the latitude - gets the digit for this place and subtracts that for
364417
# the next digit.
365-
digitValue = math.floor(adjustedLatitude / placeValue)
418+
digitValue = int(math.floor(adjustedLatitude / placeValue))
366419
adjustedLatitude -= digitValue * placeValue
367420
code += CODE_ALPHABET_[digitValue]
368421
digitCount += 1
369422
# And do the longitude - gets the digit for this place and subtracts that
370423
# for the next digit.
371-
digitValue = math.floor(adjustedLongitude / placeValue)
424+
digitValue = int(math.floor(adjustedLongitude / placeValue))
372425
adjustedLongitude -= digitValue * placeValue
373426
code += CODE_ALPHABET_[digitValue]
374427
digitCount += 1
@@ -401,8 +454,8 @@ def encodeGrid(latitude, longitude, codeLength):
401454
adjustedLongitude = (longitude + LONGITUDE_MAX_) % lngPlaceValue
402455
for i in range(codeLength):
403456
# Work out the row and column.
404-
row = math.floor(adjustedLatitude / (latPlaceValue / GRID_ROWS_))
405-
col = math.floor(adjustedLongitude / (lngPlaceValue / GRID_COLUMNS_))
457+
row = int(math.floor(adjustedLatitude / (latPlaceValue / GRID_ROWS_)))
458+
col = int(math.floor(adjustedLongitude / (lngPlaceValue / GRID_COLUMNS_)))
406459
latPlaceValue /= GRID_ROWS_
407460
lngPlaceValue /= GRID_COLUMNS_
408461
adjustedLatitude -= row * latPlaceValue
@@ -492,7 +545,7 @@ def decodeGrid(code):
492545
code_length: The number of significant characters that were in the code.
493546
This excludes the separator.
494547
"""
495-
class CodeArea:
548+
class CodeArea(object):
496549
def __init__(self,latitudeLo, longitudeLo, latitudeHi, longitudeHi, codeLength):
497550
self.latitudeLo = latitudeLo
498551
self.longitudeLo = longitudeLo

python/openlocationcode_test.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
import unittest
5+
import openlocationcode as olc
6+
7+
class TestValidity(unittest.TestCase):
8+
def setUp(self):
9+
self.testdata = []
10+
headermap = {0: 'code', 1: 'isValid', 2: 'isShort', 3: 'isFull'}
11+
tests_fn = 'test_data/validityTests.csv'
12+
with open(tests_fn, "r") as fin:
13+
for line in fin:
14+
if line.startswith('#'):
15+
continue
16+
td = line.strip().split(',')
17+
assert len(td) == len(headermap), 'Wrong format of testing data: {0}'.format(line)
18+
# all values should be booleans except the code
19+
for i in range(1, len(headermap)):
20+
td[i] = (td[i] == 'true')
21+
self.testdata.append({headermap[i]: v for i, v in enumerate(td)})
22+
23+
def test_validcodes(self):
24+
for td in self.testdata:
25+
self.assertEqual(olc.isValid(td['code']), td['isValid'], td)
26+
27+
def test_fullcodes(self):
28+
for td in self.testdata:
29+
self.assertEqual(olc.isFull(td['code']), td['isFull'], td)
30+
31+
def test_shortcodes(self):
32+
for td in self.testdata:
33+
self.assertEqual(olc.isShort(td['code']), td['isShort'], td)
34+
35+
36+
class TestShorten(unittest.TestCase):
37+
def setUp(self):
38+
self.testdata = []
39+
headermap = {0: 'fullcode', 1: 'lat', 2: 'lng', 3: 'shortcode'}
40+
tests_fn = 'test_data/shortCodeTests.csv'
41+
with open(tests_fn, "r") as fin:
42+
for line in fin:
43+
if line.startswith('#'):
44+
continue
45+
td = line.strip().split(',')
46+
assert len(td) == len(headermap), 'Wrong format of testing data: {0}'.format(line)
47+
td[1] = float(td[1])
48+
td[2] = float(td[2])
49+
self.testdata.append({headermap[i]: v for i, v in enumerate(td)})
50+
51+
def test_full2short(self):
52+
for td in self.testdata:
53+
self.assertEqual(td['shortcode'], olc.shorten(td['fullcode'], td['lat'], td['lng']), td)
54+
55+
56+
class TestEncoding(unittest.TestCase):
57+
def setUp(self):
58+
self.testdata = []
59+
headermap = {0: 'code', 1: 'lat', 2: 'lng', 3: 'latLo', 4: 'lngLo', 5: 'latHi', 6: 'longHi'}
60+
tests_fn = 'test_data/encodingTests.csv'
61+
with open(tests_fn, "r") as fin:
62+
for line in fin:
63+
if line.startswith('#'):
64+
continue
65+
td = line.strip().split(',')
66+
assert len(td) == len(headermap), 'Wrong format of testing data: {0}'.format(line)
67+
# all values should be numbers except the code
68+
for i in range(1, len(headermap)):
69+
td[i] = float(td[i])
70+
self.testdata.append({headermap[i]: v for i, v in enumerate(td)})
71+
72+
def test_encoding(self):
73+
for td in self.testdata:
74+
codelength = len(td['code']) - 1
75+
if '0' in td['code']:
76+
codelength = td['code'].index("0")
77+
self.assertEqual(td['code'], olc.encode(td['lat'], td['lng'], codelength), td)
78+
79+
def test_decoding(self):
80+
precision = 10
81+
for td in self.testdata:
82+
decoded = olc.decode(td['code'])
83+
self.assertEqual(round(decoded.latitudeLo, precision), round(td['latLo'], precision), td)
84+
self.assertEqual(round(decoded.longitudeLo, precision), round(td['lngLo'], precision), td)
85+
self.assertEqual(round(decoded.latitudeHi, precision), round(td['latHi'], precision), td)
86+
self.assertEqual(round(decoded.longitudeHi, precision), round(td['longHi'], precision), td)
87+
88+
89+
if __name__ == '__main__':
90+
unittest.main()

python/testpluscodes.py

Lines changed: 0 additions & 53 deletions
This file was deleted.

run_tests.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ if [ "$TEST_DIR" == "ruby" ]; then
3131
cd ruby && ruby test/plus_codes_test.rb
3232
exit
3333
fi
34+
# Python?
35+
if [ "$TEST_DIR" == "python" ]; then
36+
python python/openlocationcode_test.py
37+
exit
38+
fi
3439

3540
if [ "$TEST_DIR" == "rust" ]; then
3641
curl https://sh.rustup.rs -sSf | sh -s -- -y

0 commit comments

Comments
 (0)