Skip to content

Commit

Permalink
Add default lat/lon precision to base Geometry class (#131)
Browse files Browse the repository at this point in the history
* Add default lat/lon precision to base Geometry class

* use floats in tests for 2.7 `round()` compatibility

* Add test case for coord rounding

* Add Default Precision section to docs
  • Loading branch information
rayrrr committed Jul 18, 2019
1 parent 7a1b814 commit d3e7bcb
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 23 deletions.
16 changes: 16 additions & 0 deletions README.rst
Expand Up @@ -266,6 +266,22 @@ This encoding/decoding functionality shown in the previous can be extended to cu
>>> geojson.dumps(point_instance, sort_keys=True) # doctest: +ELLIPSIS
'{"coordinates": [52.23..., -19.23...], "type": "Point"}'
Default Precision
~~~~~~~~~~~~~~~~~

GeoJSON Object-based classes in this package have an additional `precision` attribute which rounds off
coordinates to 6 decimal places (roughly 0.1 meters) by default and can be customized per object instance.

.. code:: python
>>> from geojson import Point
>>> Point((-115.123412341234, 37.123412341234)) # rounded to 6 decimal places by default
{"coordinates": [-115.123412, 37.123412], "type": "Point"}
>>> Point((-115.12341234, 37.12341234), precision=8) # rounded to 8 decimal places
{"coordinates": [-115.12341234, 37.12341234], "type": "Point"}
Helpful utilities
-----------------

Expand Down
16 changes: 10 additions & 6 deletions geojson/geometry.py
Expand Up @@ -16,24 +16,28 @@ class Geometry(GeoJSON):
Represents an abstract base class for a WGS84 geometry.
"""

def __init__(self, coordinates=None, validate=False, **extra):
def __init__(self, coordinates=None, validate=False, precision=6, **extra):
"""
Initialises a Geometry object.
:param coordinates: Coordinates of the Geometry object.
:type coordinates: tuple or list of tuple
:param validate: Raise exception if validation errors are present?
:type validate: boolean
:param precision: Number of decimal places for lat/lon coords.
:type precision: integer
"""

super(Geometry, self).__init__(**extra)
self["coordinates"] = self.clean_coordinates(coordinates or [])
self["coordinates"] = self.clean_coordinates(
coordinates or [], precision)

if validate:
errors = self.errors()
if errors:
raise ValueError('{}: {}'.format(errors, coordinates))

@classmethod
def clean_coordinates(cls, coords):
def clean_coordinates(cls, coords, precision):
if isinstance(coords, cls):
return coords['coordinates']

Expand All @@ -42,11 +46,11 @@ def clean_coordinates(cls, coords):
coords = [coords]
for coord in coords:
if isinstance(coord, (list, tuple)):
new_coords.append(cls.clean_coordinates(coord))
new_coords.append(cls.clean_coordinates(coord, precision))
elif isinstance(coord, Geometry):
new_coords.append(coord['coordinates'])
elif isinstance(coord, _JSON_compliant_types):
new_coords.append(coord)
new_coords.append(round(coord, precision))
else:
raise ValueError("%r is not a JSON compliant number" % coord)
return new_coords
Expand Down
6 changes: 6 additions & 0 deletions tests/test_coords.py
Expand Up @@ -3,12 +3,18 @@
import geojson
from geojson.utils import coords, map_coords

TOO_PRECISE = (1.12341234, -2.12341234)


class CoordsTestCase(unittest.TestCase):
def test_point(self):
itr = coords(geojson.Point((-115.81, 37.24)))
self.assertEqual(next(itr), (-115.81, 37.24))

def test_point_rounding(self):
itr = coords(geojson.Point(TOO_PRECISE))
self.assertEqual(next(itr), tuple([round(c, 6) for c in TOO_PRECISE]))

def test_dict(self):
itr = coords({'type': 'Point', 'coordinates': [-115.81, 37.24]})
self.assertEqual(next(itr), (-115.81, 37.24))
Expand Down
22 changes: 11 additions & 11 deletions tests/test_features.py
Expand Up @@ -15,13 +15,13 @@ def test_protocol(self):
f = {
'type': 'Feature',
'id': '1',
'geometry': {'type': 'Point', 'coordinates': [53, -4]},
'geometry': {'type': 'Point', 'coordinates': [53.0, -4.0]},
'properties': {'title': 'Dict 1'},
}

json = geojson.dumps(f, sort_keys=True)
self.assertEqual(json, '{"geometry":'
' {"coordinates": [53, -4],'
' {"coordinates": [53.0, -4.0],'
' "type": "Point"},'
' "id": "1",'
' "properties": {"title": "Dict 1"},'
Expand All @@ -30,7 +30,7 @@ def test_protocol(self):
o = geojson.loads(json)
output = geojson.dumps(o, sort_keys=True)
self.assertEqual(output, '{"geometry":'
' {"coordinates": [53, -4],'
' {"coordinates": [53.0, -4.0],'
' "type": "Point"},'
' "id": "1",'
' "properties": {"title": "Dict 1"},'
Expand All @@ -49,7 +49,7 @@ def test_feature_class(self):
from geojson.examples import SimpleWebFeature
feature = SimpleWebFeature(
id='1',
geometry={'type': 'Point', 'coordinates': [53, -4]},
geometry={'type': 'Point', 'coordinates': [53.0, -4.0]},
title='Feature 1', summary='The first feature',
link='http://example.org/features/1'
)
Expand All @@ -61,10 +61,10 @@ def test_feature_class(self):
self.assertEqual(feature.properties['link'],
'http://example.org/features/1')
self.assertEqual(geojson.dumps(feature.geometry, sort_keys=True),
'{"coordinates": [53, -4], "type": "Point"}')
'{"coordinates": [53.0, -4.0], "type": "Point"}')

# Encoding
json = ('{"geometry": {"coordinates": [53, -4],'
json = ('{"geometry": {"coordinates": [53.0, -4.0],'
' "type": "Point"},'
' "id": "1",'
' "properties":'
Expand All @@ -77,7 +77,7 @@ def test_feature_class(self):
# Decoding
factory = geojson.examples.create_simple_web_feature
json = ('{"geometry": {"type": "Point",'
' "coordinates": [53, -4]},'
' "coordinates": [53.0, -4.0]},'
' "id": "1",'
' "properties": {"summary": "The first feature",'
' "link": "http://example.org/features/1",'
Expand All @@ -91,7 +91,7 @@ def test_feature_class(self):
self.assertEqual(feature.properties['link'],
'http://example.org/features/1')
self.assertEqual(geojson.dumps(feature.geometry, sort_keys=True),
'{"coordinates": [53, -4], "type": "Point"}')
'{"coordinates": [53.0, -4.0], "type": "Point"}')

def test_geo_interface(self):
class Thingy(object):
Expand All @@ -108,12 +108,12 @@ def __geo_interface__(self):
"geometry": {"type": "Point",
"coordinates": (self.x, self.y)}})

ob = Thingy('1', 'thingy one', -106, 40)
ob = Thingy('1', 'thingy one', -106.0, 40.0)
self.assertEqual(geojson.dumps(ob.__geo_interface__['geometry'],
sort_keys=True),
'{"coordinates": [-106, 40], "type": "Point"}')
'{"coordinates": [-106.0, 40.0], "type": "Point"}')
self.assertEqual(geojson.dumps(ob, sort_keys=True),
('{"geometry": {"coordinates": [-106, 40],'
('{"geometry": {"coordinates": [-106.0, 40.0],'
' "type": "Point"},'
' "id": "1",'
' "properties": {"title": "thingy one"}}'))
12 changes: 6 additions & 6 deletions tests/test_geo_interface.py
Expand Up @@ -65,21 +65,21 @@ def __geo_interface__(self):
properties={'name': self.name})

self.name = "In N Out Burger"
self.latlng = [-54, 4]
self.latlng = [-54.0, 4.0]

self.restaurant_nogeo = Restaurant(self.name, self.latlng)

self.restaurant1 = Restaurant1(self.name, self.latlng)
self.restaurant2 = Restaurant2(self.name, self.latlng)

self.restaurant_str = ('{"coordinates": [-54, 4],'
self.restaurant_str = ('{"coordinates": [-54.0, 4.0],'
' "type": "Point"}')

self.restaurant_feature1 = RestaurantFeature1(self.name, self.latlng)
self.restaurant_feature2 = RestaurantFeature2(self.name, self.latlng)

self.restaurant_feature_str = ('{"geometry":'
' {"coordinates": [-54, 4],'
' {"coordinates": [-54.0, 4.0],'
' "type": "Point"},'
' "properties":'
' {"name": "In N Out Burger"},'
Expand Down Expand Up @@ -133,13 +133,13 @@ def test_decode_nested(self):

def test_invalid(self):
with self.assertRaises(ValueError) as cm:
geojson.loads('{"type":"Point", "coordinates":[[-Infinity, 4]]}')
geojson.loads('{"type":"Point", "coordinates":[[-Infinity, 4.0]]}')

self.assertIn('is not JSON compliant', str(cm.exception))

def test_mapping(self):
self.assertEqual(to_mapping(geojson.Point([1, 2])),
{"coordinates": [1, 2], "type": "Point"})
self.assertEqual(to_mapping(geojson.Point([1.0, 2.0])),
{"coordinates": [1.0, 2.0], "type": "Point"})

def test_GeoJSON(self):
self.assertEqual(None, geojson.GeoJSON().__geo_interface__)
Expand Down

0 comments on commit d3e7bcb

Please sign in to comment.