From 6f645e86197315c60414566a89ba27e9ace5a4f3 Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Thu, 28 Aug 2014 19:36:29 +0200 Subject: [PATCH 1/2] Added MultiPoint, MultiLine and MultiPolygon fields --- mongoengine/base/fields.py | 67 ++++++++++++++++++++- mongoengine/fields.py | 68 ++++++++++++++++++++++ tests/fields/geo.py | 115 ++++++++++++++++++++++++++++++++++++- 3 files changed, 246 insertions(+), 4 deletions(-) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index c163e6e7..5bb9c7ac 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -457,7 +457,7 @@ class GeoJsonBaseField(BaseField): if error: self.error(error) - def _validate_polygon(self, value): + def _validate_polygon(self, value, top_level=True): if not isinstance(value, (list, tuple)): return 'Polygons must contain list of linestrings' @@ -475,7 +475,10 @@ class GeoJsonBaseField(BaseField): if error and error not in errors: errors.append(error) if errors: - return "Invalid Polygon:\n%s" % ", ".join(errors) + if top_level: + return "Invalid Polygon:\n%s" % ", ".join(errors) + else: + return "%s" % ", ".join(errors) def _validate_linestring(self, value, top_level=True): """Validates a linestring""" @@ -509,6 +512,66 @@ class GeoJsonBaseField(BaseField): not isinstance(value[1], (float, int))): return "Both values (%s) in point must be float or int" % repr(value) + def _validate_multipoint(self, value): + if not isinstance(value, (list, tuple)): + return 'MultiPoint must be a list of Point' + + # Quick and dirty validator + try: + value[0][0] + except: + return "Invalid MultiPoint must contain at least one valid point" + + errors = [] + for point in value: + error = self._validate_point(point) + if error and error not in errors: + errors.append(error) + + if errors: + return "%s" % ", ".join(errors) + + def _validate_multilinestring(self, value, top_level=True): + if not isinstance(value, (list, tuple)): + return 'MultiLineString must be a list of LineString' + + # Quick and dirty validator + try: + value[0][0][0] + except: + return "Invalid MultiLineString must contain at least one valid linestring" + + errors = [] + for linestring in value: + error = self._validate_linestring(linestring, False) + if error and error not in errors: + errors.append(error) + + if errors: + if top_level: + return "Invalid MultiLineString:\n%s" % ", ".join(errors) + else: + return "%s" % ", ".join(errors) + + def _validate_multipolygon(self, value): + if not isinstance(value, (list, tuple)): + return 'MultiPolygon must be a list of Polygon' + + # Quick and dirty validator + try: + value[0][0][0][0] + except: + return "Invalid MultiPolygon must contain at least one valid Polygon" + + errors = [] + for polygon in value: + error = self._validate_polygon(polygon, False) + if error and error not in errors: + errors.append(error) + + if errors: + return "Invalid MultiPolygon:\n%s" % ", ".join(errors) + def to_mongo(self, value): if isinstance(value, dict): return value diff --git a/mongoengine/fields.py b/mongoengine/fields.py index e2a6dd1d..2441a754 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -44,6 +44,7 @@ __all__ = [ 'GridFSError', 'GridFSProxy', 'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField', 'LineStringField', 'PolygonField', 'SequenceField', 'UUIDField', + 'MultiPointField', 'MultiLineStringField', 'MultiPolygonField', 'GeoJsonBaseField'] @@ -1903,3 +1904,70 @@ class PolygonField(GeoJsonBaseField): .. versionadded:: 0.8 """ _type = "Polygon" + + +class MultiPointField(GeoJsonBaseField): + + """A GeoJSON field storing a list of Points. + + The data is represented as: + + .. code-block:: js + + { "type" : "MultiPoint" , + "coordinates" : [[x1, y1], [x2, y2]]} + + You can either pass a dict with the full information or a list + to set the value. + + Requires mongodb >= 2.6 + .. versionadded:: 0.9 + """ + _type = "MultiPoint" + + +class MultiLineStringField(GeoJsonBaseField): + + """A GeoJSON field storing a list of LineStrings. + + The data is represented as: + + .. code-block:: js + + { "type" : "MultiLineString" , + "coordinates" : [[[x1, y1], [x1, y1] ... [xn, yn]], + [[x1, y1], [x1, y1] ... [xn, yn]]]} + + You can either pass a dict with the full information or a list of points. + + Requires mongodb >= 2.6 + .. versionadded:: 0.9 + """ + _type = "MultiLineString" + + +class MultiPolygonField(GeoJsonBaseField): + + """A GeoJSON field storing list of Polygons. + + The data is represented as: + + .. code-block:: js + + { "type" : "Polygon" , + "coordinates" : [[ + [[x1, y1], [x1, y1] ... [xn, yn]], + [[x1, y1], [x1, y1] ... [xn, yn]] + ], [ + [[x1, y1], [x1, y1] ... [xn, yn]], + [[x1, y1], [x1, y1] ... [xn, yn]] + ] + } + + You can either pass a dict with the full information or a list + of Polygons. + + Requires mongodb >= 2.6 + .. versionadded:: 0.9 + """ + _type = "MultiPolygon" diff --git a/tests/fields/geo.py b/tests/fields/geo.py index be29a0b2..8193d87e 100644 --- a/tests/fields/geo.py +++ b/tests/fields/geo.py @@ -19,8 +19,8 @@ class GeoFieldTest(unittest.TestCase): def _test_for_expected_error(self, Cls, loc, expected): try: Cls(loc=loc).validate() - self.fail() - except ValidationError, e: + self.fail('Should not validate the location {0}'.format(loc)) + except ValidationError as e: self.assertEqual(expected, e.to_dict()['loc']) def test_geopoint_validation(self): @@ -155,6 +155,117 @@ class GeoFieldTest(unittest.TestCase): Location(loc=[[[1, 2], [3, 4], [5, 6], [1, 2]]]).validate() + def test_multipoint_validation(self): + class Location(Document): + loc = MultiPointField() + + invalid_coords = {"x": 1, "y": 2} + expected = 'MultiPointField can only accept a valid GeoJson dictionary or lists of (x, y)' + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = {"type": "MadeUp", "coordinates": [[]]} + expected = 'MultiPointField type must be "MultiPoint"' + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = {"type": "MultiPoint", "coordinates": [[1, 2, 3]]} + expected = "Value ([1, 2, 3]) must be a two-dimensional point" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[]] + expected = "Invalid MultiPoint must contain at least one valid point" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[1]], [[1, 2, 3]]] + for coord in invalid_coords: + expected = "Value (%s) must be a two-dimensional point" % repr(coord[0]) + self._test_for_expected_error(Location, coord, expected) + + invalid_coords = [[[{}, {}]], [("a", "b")]] + for coord in invalid_coords: + expected = "Both values (%s) in point must be float or int" % repr(coord[0]) + self._test_for_expected_error(Location, coord, expected) + + Location(loc=[[1, 2]]).validate() + Location(loc={ + "type": "MultiPoint", + "coordinates": [ + [1, 2], + [81.4471435546875, 23.61432859499169] + ]}).validate() + + def test_multilinestring_validation(self): + class Location(Document): + loc = MultiLineStringField() + + invalid_coords = {"x": 1, "y": 2} + expected = 'MultiLineStringField can only accept a valid GeoJson dictionary or lists of (x, y)' + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = {"type": "MadeUp", "coordinates": [[]]} + expected = 'MultiLineStringField type must be "MultiLineString"' + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = {"type": "MultiLineString", "coordinates": [[[1, 2, 3]]]} + expected = "Invalid MultiLineString:\nValue ([1, 2, 3]) must be a two-dimensional point" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [5, "a"] + expected = "Invalid MultiLineString must contain at least one valid linestring" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[1]]] + expected = "Invalid MultiLineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0][0]) + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[1, 2, 3]]] + expected = "Invalid MultiLineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0][0]) + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[[{}, {}]]], [[("a", "b")]]] + for coord in invalid_coords: + expected = "Invalid MultiLineString:\nBoth values (%s) in point must be float or int" % repr(coord[0][0]) + self._test_for_expected_error(Location, coord, expected) + + Location(loc=[[[1, 2], [3, 4], [5, 6], [1,2]]]).validate() + + def test_multipolygon_validation(self): + class Location(Document): + loc = MultiPolygonField() + + invalid_coords = {"x": 1, "y": 2} + expected = 'MultiPolygonField can only accept a valid GeoJson dictionary or lists of (x, y)' + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = {"type": "MadeUp", "coordinates": [[]]} + expected = 'MultiPolygonField type must be "MultiPolygon"' + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = {"type": "MultiPolygon", "coordinates": [[[[1, 2, 3]]]]} + expected = "Invalid MultiPolygon:\nValue ([1, 2, 3]) must be a two-dimensional point" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[[5, "a"]]]] + expected = "Invalid MultiPolygon:\nBoth values ([5, 'a']) in point must be float or int" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[[]]]] + expected = "Invalid MultiPolygon must contain at least one valid Polygon" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[[1, 2, 3]]]] + expected = "Invalid MultiPolygon:\nValue ([1, 2, 3]) must be a two-dimensional point" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[[{}, {}]]], [[("a", "b")]]] + expected = "Invalid MultiPolygon:\nBoth values ([{}, {}]) in point must be float or int, Both values (('a', 'b')) in point must be float or int" + self._test_for_expected_error(Location, invalid_coords, expected) + + invalid_coords = [[[[1, 2], [3, 4]]]] + expected = "Invalid MultiPolygon:\nLineStrings must start and end at the same point" + self._test_for_expected_error(Location, invalid_coords, expected) + + Location(loc=[[[[1, 2], [3, 4], [5, 6], [1, 2]]]]).validate() + def test_indexes_geopoint(self): """Ensure that indexes are created automatically for GeoPointFields. """ From 708c3f1e2a416523a55128dbe1bcc5a374b9a910 Mon Sep 17 00:00:00 2001 From: Axel Haustant Date: Thu, 28 Aug 2014 19:42:09 +0200 Subject: [PATCH 2/2] Added new geospatial fields to th documentation --- docs/apireference.rst | 3 +++ docs/changelog.rst | 1 + docs/guide/defining-documents.rst | 9 +++++++++ 3 files changed, 13 insertions(+) diff --git a/docs/apireference.rst b/docs/apireference.rst index 9b4f2c66..857a14b0 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -95,6 +95,9 @@ Fields .. autoclass:: mongoengine.fields.PointField .. autoclass:: mongoengine.fields.LineStringField .. autoclass:: mongoengine.fields.PolygonField +.. autoclass:: mongoengine.fields.MultiPointField +.. autoclass:: mongoengine.fields.MultiLineStringField +.. autoclass:: mongoengine.fields.MultiPolygonField .. autoclass:: mongoengine.fields.GridFSError .. autoclass:: mongoengine.fields.GridFSProxy .. autoclass:: mongoengine.fields.ImageGridFsProxy diff --git a/docs/changelog.rst b/docs/changelog.rst index 94dbcf3c..4bc01801 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -52,6 +52,7 @@ Changes in 0.9.X - DEV - index_cls is ignored when deciding to set _cls as index prefix #733 - Make 'db' argument to connection optional #737 - Allow atomic update for the entire `DictField` #742 +- Added MultiPointField, MultiLineField, MultiPolygonField Changes in 0.8.7 ================ diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index ddc37eae..8db991c1 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -91,6 +91,12 @@ are as follows: * :class:`~mongoengine.fields.StringField` * :class:`~mongoengine.fields.URLField` * :class:`~mongoengine.fields.UUIDField` +* :class:`~mongoengine.fields.PointField` +* :class:`~mongoengine.fields.LineStringField` +* :class:`~mongoengine.fields.PolygonField` +* :class:`~mongoengine.fields.MultiPointField` +* :class:`~mongoengine.fields.MultiLineStringField` +* :class:`~mongoengine.fields.MultiPolygonField` Field arguments --------------- @@ -544,6 +550,9 @@ The following fields will explicitly add a "2dsphere" index: - :class:`~mongoengine.fields.PointField` - :class:`~mongoengine.fields.LineStringField` - :class:`~mongoengine.fields.PolygonField` + - :class:`~mongoengine.fields.MultiPointField` + - :class:`~mongoengine.fields.MultiLineStringField` + - :class:`~mongoengine.fields.MultiPolygonField` As "2dsphere" indexes can be part of a compound index, you may not want the automatic index but would prefer a compound index. In this example we turn off