Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add default lat/lon precision to base Geometry class #131

Merged
merged 10 commits into from
Jul 18, 2019
Merged
16 changes: 16 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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