From 8d42902f1908c2fd5a894e082d3a8aead75d1c28 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Thu, 2 Apr 2009 16:50:44 +0000 Subject: [PATCH] Fixed #9745 -- Added the `GeoQuerySet` methods `snap_to_grid` and `geojson`. git-svn-id: http://code.djangoproject.com/svn/django/trunk@10369 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- .../gis/db/backend/postgis/__init__.py | 2 + .../contrib/gis/db/backend/postgis/query.py | 9 ++- django/contrib/gis/db/models/manager.py | 6 ++ django/contrib/gis/db/models/query.py | 62 +++++++++++++++ django/contrib/gis/tests/geoapp/tests.py | 76 ++++++++++++++++++- 5 files changed, 152 insertions(+), 3 deletions(-) diff --git a/django/contrib/gis/db/backend/postgis/__init__.py b/django/contrib/gis/db/backend/postgis/__init__.py index 1c45363136b..7833376d1e1 100644 --- a/django/contrib/gis/db/backend/postgis/__init__.py +++ b/django/contrib/gis/db/backend/postgis/__init__.py @@ -19,6 +19,7 @@ SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True, envelope=ENVELOPE, extent=EXTENT, gis_terms=POSTGIS_TERMS, + geojson=ASGEOJSON, gml=ASGML, intersection=INTERSECTION, kml=ASKML, @@ -32,6 +33,7 @@ SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True, point_on_surface=POINT_ON_SURFACE, scale=SCALE, select=GEOM_SELECT, + snap_to_grid=SNAP_TO_GRID, svg=ASSVG, sym_difference=SYM_DIFFERENCE, transform=TRANSFORM, diff --git a/django/contrib/gis/db/backend/postgis/query.py b/django/contrib/gis/db/backend/postgis/query.py index 37671c50a28..aca6e0d3ec8 100644 --- a/django/contrib/gis/db/backend/postgis/query.py +++ b/django/contrib/gis/db/backend/postgis/query.py @@ -38,6 +38,7 @@ if MAJOR_VERSION >= 1: # Functions used by the GeoManager & GeoQuerySet AREA = get_func('Area') + ASGEOJSON = get_func('AsGeoJson') ASKML = get_func('AsKML') ASGML = get_func('AsGML') ASSVG = get_func('AsSVG') @@ -61,11 +62,12 @@ if MAJOR_VERSION >= 1: PERIMETER = get_func('Perimeter') POINT_ON_SURFACE = get_func('PointOnSurface') SCALE = get_func('Scale') + SNAP_TO_GRID = get_func('SnapToGrid') SYM_DIFFERENCE = get_func('SymDifference') TRANSFORM = get_func('Transform') TRANSLATE = get_func('Translate') - # Special cases for union and KML methods. + # Special cases for union, KML, and GeoJSON methods. if MINOR_VERSION1 < 3: UNIONAGG = 'GeomUnion' UNION = 'Union' @@ -75,6 +77,11 @@ if MAJOR_VERSION >= 1: if MINOR_VERSION1 == 1: ASKML = False + + # Only 1.3.4+ have AsGeoJson. + if (MINOR_VERSION1 < 3 or + (MINOR_VERSION1 == 3 and MINOR_VERSION2 < 4)): + ASGEOJSON = False else: raise NotImplementedError('PostGIS versions < 1.0 are not supported.') diff --git a/django/contrib/gis/db/models/manager.py b/django/contrib/gis/db/models/manager.py index 602d11251ab..b4cc14b5b65 100644 --- a/django/contrib/gis/db/models/manager.py +++ b/django/contrib/gis/db/models/manager.py @@ -30,6 +30,9 @@ class GeoManager(Manager): def extent(self, *args, **kwargs): return self.get_query_set().extent(*args, **kwargs) + def geojson(self, *args, **kwargs): + return self.get_query_set().geojson(*args, **kwargs) + def gml(self, *args, **kwargs): return self.get_query_set().gml(*args, **kwargs) @@ -63,6 +66,9 @@ class GeoManager(Manager): def scale(self, *args, **kwargs): return self.get_query_set().scale(*args, **kwargs) + def snap_to_grid(self, *args, **kwargs): + return self.get_query_set().snap_to_grid(*args, **kwargs) + def svg(self, *args, **kwargs): return self.get_query_set().svg(*args, **kwargs) diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index a043afcd9a6..13e15721a85 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -103,6 +103,32 @@ class GeoQuerySet(QuerySet): """ return self._spatial_aggregate(aggregates.Extent, **kwargs) + def geojson(self, precision=8, crs=False, bbox=False, **kwargs): + """ + Returns a GeoJSON representation of the geomtry field in a `geojson` + attribute on each element of the GeoQuerySet. + + The `crs` and `bbox` keywords may be set to True if the users wants + the coordinate reference system and the bounding box to be included + in the GeoJSON representation of the geometry. + """ + if not SpatialBackend.postgis or not SpatialBackend.geojson: + raise NotImplementedError('Only PostGIS 1.3.4+ supports GeoJSON serialization.') + + if not isinstance(precision, (int, long)): + raise TypeError('Precision keyword must be set with an integer.') + + # Setting the options flag + options = 0 + if crs and bbox: options = 3 + elif crs: options = 1 + elif bbox: options = 2 + s = {'desc' : 'GeoJSON', + 'procedure_args' : {'precision' : precision, 'options' : options}, + 'procedure_fmt' : '%(geo_col)s,%(precision)s,%(options)s', + } + return self._spatial_attribute('geojson', s, **kwargs) + def gml(self, precision=8, version=2, **kwargs): """ Returns GML representation of the given field in a `gml` attribute @@ -213,6 +239,42 @@ class GeoQuerySet(QuerySet): } return self._spatial_attribute('scale', s, **kwargs) + def snap_to_grid(self, *args, **kwargs): + """ + Snap all points of the input geometry to the grid. How the + geometry is snapped to the grid depends on how many arguments + were given: + - 1 argument : A single size to snap both the X and Y grids to. + - 2 arguments: X and Y sizes to snap the grid to. + - 4 arguments: X, Y sizes and the X, Y origins. + """ + if False in [isinstance(arg, (float, int, long)) for arg in args]: + raise TypeError('Size argument(s) for the grid must be a float or integer values.') + + nargs = len(args) + if nargs == 1: + size = args[0] + procedure_fmt = '%(geo_col)s,%(size)s' + procedure_args = {'size' : size} + elif nargs == 2: + xsize, ysize = args + procedure_fmt = '%(geo_col)s,%(xsize)s,%(ysize)s' + procedure_args = {'xsize' : xsize, 'ysize' : ysize} + elif nargs == 4: + xsize, ysize, xorigin, yorigin = args + procedure_fmt = '%(geo_col)s,%(xorigin)s,%(yorigin)s,%(xsize)s,%(ysize)s' + procedure_args = {'xsize' : xsize, 'ysize' : ysize, + 'xorigin' : xorigin, 'yorigin' : yorigin} + else: + raise ValueError('Must provide 1, 2, or 4 arguments to `snap_to_grid`.') + + s = {'procedure_fmt' : procedure_fmt, + 'procedure_args' : procedure_args, + 'select_field' : GeomField(), + } + + return self._spatial_attribute('snap_to_grid', s, **kwargs) + def svg(self, **kwargs): """ Returns SVG representation of the geographic field in a `svg` diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 475a1f7618d..53986ad7aac 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -119,7 +119,7 @@ class GeoModelTest(unittest.TestCase): @no_oracle # Oracle does not support KML. @no_spatialite # SpatiaLite does not support KML. def test03a_kml(self): - "Testing KML output from the database using GeoManager.kml()." + "Testing KML output from the database using GeoQuerySet.kml()." if DISABLE: return # Should throw a TypeError when trying to obtain KML from a # non-geometry field. @@ -145,7 +145,7 @@ class GeoModelTest(unittest.TestCase): @no_spatialite # SpatiaLite does not support GML. def test03b_gml(self): - "Testing GML output from the database using GeoManager.gml()." + "Testing GML output from the database using GeoQuerySet.gml()." if DISABLE: return # Should throw a TypeError when tyring to obtain GML from a # non-geometry field. @@ -164,6 +164,38 @@ class GeoModelTest(unittest.TestCase): for ptown in [ptown1, ptown2]: self.assertEqual('-104.609252,38.255001', ptown.gml) + @no_spatialite + @no_oracle + def test03c_geojson(self): + "Testing GeoJSON output from the database using GeoQuerySet.geojson()." + if DISABLE: return + # PostGIS only supports GeoJSON on 1.3.4+ + if not SpatialBackend.geojson: + return + + # Precision argument should only be an integer + self.assertRaises(TypeError, City.objects.geojson, precision='foo') + + # Reference queries and values. + # SELECT ST_AsGeoJson("geoapp_city"."point", 8, 0) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Pueblo'; + json = '{"type":"Point","coordinates":[-104.60925200,38.25500100]}' + self.assertEqual(City.objects.geojson().get(name='Pueblo').geojson, json) + + # SELECT ST_AsGeoJson("geoapp_city"."point", 8, 1) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Houston'; + json = '{"type":"Point","crs":{"type":"EPSG","properties":{"EPSG":4326}},"coordinates":[-95.36315100,29.76337400]}' + # This time we want to include the CRS by using the `crs` keyword. + self.assertEqual(City.objects.geojson(crs=True, model_att='json').get(name='Houston').json, json) + + # SELECT ST_AsGeoJson("geoapp_city"."point", 8, 2) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Victoria'; + json = '{"type":"Point","bbox":[-123.30519600,48.46261100,-123.30519600,48.46261100],"coordinates":[-123.30519600,48.46261100]}' + # This time we include the bounding box by using the `bbox` keyword. + self.assertEqual(City.objects.geojson(bbox=True).get(name='Victoria').geojson, json) + + # SELECT ST_AsGeoJson("geoapp_city"."point", 5, 3) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Chicago'; + json = '{"type":"Point","crs":{"type":"EPSG","properties":{"EPSG":4326}},"bbox":[-87.65018,41.85039,-87.65018,41.85039],"coordinates":[-87.65018,41.85039]}' + # Finally, we set every available keyword. + self.assertEqual(City.objects.geojson(bbox=True, crs=True, precision=5).get(name='Chicago').geojson, json) + def test04_transform(self): "Testing the transform() GeoManager method." if DISABLE: return @@ -620,6 +652,46 @@ class GeoModelTest(unittest.TestCase): self.assertEqual(1, qs.count()) for pc in qs: self.assertEqual(32128, pc.point.srid) + + @no_spatialite + @no_oracle + def test27_snap_to_grid(self): + "Testing GeoQuerySet.snap_to_grid()." + if DISABLE: return + + # Let's try and break snap_to_grid() with bad combinations of arguments. + for bad_args in ((), range(3), range(5)): + self.assertRaises(ValueError, Country.objects.snap_to_grid, *bad_args) + for bad_args in (('1.0',), (1.0, None), tuple(map(unicode, range(4)))): + self.assertRaises(TypeError, Country.objects.snap_to_grid, *bad_args) + + # Boundary for San Marino, courtesy of Bjorn Sandvik of thematicmapping.org + # from the world borders dataset he provides. + wkt = ('MULTIPOLYGON(((12.41580 43.95795,12.45055 43.97972,12.45389 43.98167,' + '12.46250 43.98472,12.47167 43.98694,12.49278 43.98917,' + '12.50555 43.98861,12.51000 43.98694,12.51028 43.98277,' + '12.51167 43.94333,12.51056 43.93916,12.49639 43.92333,' + '12.49500 43.91472,12.48778 43.90583,12.47444 43.89722,' + '12.46472 43.89555,12.45917 43.89611,12.41639 43.90472,' + '12.41222 43.90610,12.40782 43.91366,12.40389 43.92667,' + '12.40500 43.94833,12.40889 43.95499,12.41580 43.95795)))') + sm = Country.objects.create(name='San Marino', mpoly=fromstr(wkt)) + + # Because floating-point arithmitic isn't exact, we set a tolerance + # to pass into GEOS `equals_exact`. + tol = 0.000000001 + + # SELECT AsText(ST_SnapToGrid("geoapp_country"."mpoly", 0.1)) FROM "geoapp_country" WHERE "geoapp_country"."name" = 'San Marino'; + ref = fromstr('MULTIPOLYGON(((12.4 44,12.5 44,12.5 43.9,12.4 43.9,12.4 44)))') + self.failUnless(ref.equals_exact(Country.objects.snap_to_grid(0.1).get(name='San Marino').snap_to_grid, tol)) + + # SELECT AsText(ST_SnapToGrid("geoapp_country"."mpoly", 0.05, 0.23)) FROM "geoapp_country" WHERE "geoapp_country"."name" = 'San Marino'; + ref = fromstr('MULTIPOLYGON(((12.4 43.93,12.45 43.93,12.5 43.93,12.45 43.93,12.4 43.93)))') + self.failUnless(ref.equals_exact(Country.objects.snap_to_grid(0.05, 0.23).get(name='San Marino').snap_to_grid, tol)) + + # SELECT AsText(ST_SnapToGrid("geoapp_country"."mpoly", 0.5, 0.17, 0.05, 0.23)) FROM "geoapp_country" WHERE "geoapp_country"."name" = 'San Marino'; + ref = fromstr('MULTIPOLYGON(((12.4 43.87,12.45 43.87,12.45 44.1,12.5 44.1,12.5 43.87,12.45 43.87,12.4 43.87)))') + self.failUnless(ref.equals_exact(Country.objects.snap_to_grid(0.05, 0.23, 0.5, 0.17).get(name='San Marino').snap_to_grid, tol)) from test_feeds import GeoFeedTest from test_regress import GeoRegressionTests