Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

gis: Added the `extent` method to `GeoQuerySet`; moved various spatia…

…l-backend settings into the `SpatialBackend` container class.

git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7028 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 398eca3fb2ac84304b179fabd2f0960228c59b09 1 parent feebe39
Justin Bronn authored January 22, 2008
29  django/contrib/gis/db/backend/__init__.py
@@ -5,17 +5,14 @@
5 5
  needed for GeoDjango.
6 6
  
7 7
  (1) GeoBackEndField, a base class needed for GeometryField.
8  
- (2) GeometryProxy, for lazy-instantiated geometries from the 
9  
-     database output.
10  
- (3) GIS_TERMS, a list of acceptable geographic lookup types for 
  8
+ (2) GIS_TERMS, a list of acceptable geographic lookup types for 
11 9
      the backend.
12  
- (4) The `parse_lookup` function, used for spatial SQL construction by
  10
+ (3) The `parse_lookup` function, used for spatial SQL construction by
13 11
      the GeoQuerySet.
14  
- (5) The `create_spatial_db`, and `get_geo_where_clause`
  12
+ (4) The `create_spatial_db`, and `get_geo_where_clause`
15 13
      routines (needed by `parse_lookup`).
16  
-
17  
- Currently only PostGIS is supported, but someday backends will be added for
18  
- additional spatial databases (e.g., Oracle, DB2).
  14
+ (5) The `SpatialBackend` object, which contains information specific
  15
+     to the spatial backend.
19 16
 """
20 17
 from types import StringType, UnicodeType
21 18
 from django.conf import settings
@@ -26,7 +23,7 @@
26 23
 from django.contrib.gis.geos import GEOSGeometry
27 24
 
28 25
 # These routines (needed by GeoManager), default to False.
29  
-ASGML, ASKML, DISTANCE, TRANSFORM, UNION, VERSION = (False, False, False, False, False, False)
  26
+ASGML, ASKML, DISTANCE, EXTENT, TRANSFORM, UNION, VERSION = (False, False, False, False, False, False, False)
30 27
 
31 28
 if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
32 29
     # PostGIS is the spatial database, getting the rquired modules, 
@@ -34,7 +31,7 @@
34 31
     from django.contrib.gis.db.backend.postgis import \
35 32
         PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
36 33
         create_spatial_db, get_geo_where_clause, \
37  
-        ASGML, ASKML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION, \
  34
+        ASGML, ASKML, DISTANCE, EXTENT, GEOM_SELECT, TRANSFORM, UNION, \
38 35
         MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
39 36
     VERSION = (MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2)
40 37
     SPATIAL_BACKEND = 'postgis'
@@ -55,6 +52,18 @@
55 52
 else:
56 53
     raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
57 54
 
  55
+class SpatialBackend(object):
  56
+    "A container for properties of the Spatial Backend."
  57
+    as_kml = ASKML
  58
+    as_gml = ASGML
  59
+    distance = DISTANCE
  60
+    extent = EXTENT
  61
+    name = SPATIAL_BACKEND
  62
+    select = GEOM_SELECT
  63
+    transform = TRANSFORM
  64
+    union = UNION
  65
+    version = VERSION
  66
+
58 67
 ####    query.py overloaded functions    ####
59 68
 # parse_lookup() and lookup_inner() are modified from their django/db/models/query.py
60 69
 #  counterparts to support constructing SQL for geographic queries.
2  django/contrib/gis/db/backend/postgis/__init__.py
@@ -6,4 +6,4 @@
6 6
 from django.contrib.gis.db.backend.postgis.query import \
7 7
     get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
8 8
     MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2, \
9  
-    ASKML, ASGML, DISTANCE, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT
  9
+    ASKML, ASGML, DISTANCE, EXTENT, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT
1  django/contrib/gis/db/backend/postgis/query.py
@@ -40,6 +40,7 @@ def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
40 40
     ASKML = get_func('AsKML')
41 41
     ASGML = get_func('AsGML')
42 42
     DISTANCE = get_func('Distance')
  43
+    EXTENT = get_func('extent')
43 44
     GEOM_FROM_TEXT = get_func('GeomFromText')
44 45
     GEOM_FROM_WKB = get_func('GeomFromWKB')
45 46
     TRANSFORM = get_func('Transform')
3  django/contrib/gis/db/models/manager.py
@@ -10,6 +10,9 @@ def get_query_set(self):
10 10
     def distance(self, *args, **kwargs):
11 11
         return self.get_query_set().distance(*args, **kwargs)
12 12
 
  13
+    def extent(self, *args, **kwargs):
  14
+        return self.get_query_set().extent(*args, **kwargs)
  15
+
13 16
     def gml(self, *args, **kwargs):
14 17
         return self.get_query_set().gml(*args, **kwargs)
15 18
 
62  django/contrib/gis/db/models/query.py
@@ -6,13 +6,12 @@
6 6
 from django.utils.datastructures import SortedDict
7 7
 from django.contrib.gis.db.models.fields import GeometryField
8 8
 # parse_lookup depends on the spatial database backend.
9  
-from django.contrib.gis.db.backend import parse_lookup, \
10  
-    ASGML, ASKML, DISTANCE, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION, VERSION
  9
+from django.contrib.gis.db.backend import parse_lookup, SpatialBackend
11 10
 from django.contrib.gis.geos import GEOSGeometry
12 11
 
13 12
 # Shortcut booleans for determining the backend.
14  
-oracle = SPATIAL_BACKEND == 'oracle'
15  
-postgis = SPATIAL_BACKEND == 'postgis'
  13
+oracle  = SpatialBackend.name == 'oracle'
  14
+postgis = SpatialBackend.name == 'postgis'
16 15
 
17 16
 class GeoQ(Q):
18 17
     "Geographical query encapsulation object."
@@ -23,7 +22,7 @@ def get_sql(self, opts):
23 22
 
24 23
 class GeoQuerySet(QuerySet):
25 24
     "Geographical-enabled QuerySet object."
26  
-    
  25
+        
27 26
     #### Overloaded QuerySet Routines ####
28 27
     def __init__(self, model=None):
29 28
         super(GeoQuerySet, self).__init__(model=model)
@@ -37,10 +36,10 @@ def __init__(self, model=None):
37 36
 
38 37
         # If GEOM_SELECT is defined in the backend, then it will be used
39 38
         # for the selection format of the geometry column.
40  
-        if GEOM_SELECT:
  39
+        if SpatialBackend.select:
41 40
             # Transformed geometries in Oracle use EWKT so that the SRID
42 41
             # on the transformed lazy geometries is set correctly).
43  
-            self._geo_fmt = GEOM_SELECT
  42
+            self._geo_fmt = SpatialBackend.select
44 43
         else:
45 44
             self._geo_fmt = '%s'
46 45
 
@@ -259,6 +258,7 @@ def distance(self, *args, **kwargs):
259 258
         given geometry in a `distance` attribute on each element of the
260 259
         GeoQuerySet.
261 260
         """
  261
+        DISTANCE = SpatialBackend.distance
262 262
         if not DISTANCE:
263 263
             raise ImproperlyConfigured('Distance() stored proecedure not available.')
264 264
 
@@ -299,12 +299,55 @@ def distance(self, *args, **kwargs):
299 299
             dist_select = {'distance' : '%s(%s, %s)' % (DISTANCE, field_col, geom_sql)}
300 300
         return self.extra(select=dist_select)
301 301
 
  302
+    def extent(self, field_name=None):
  303
+        """
  304
+        Returns the extent (aggregate) of the features in the GeoQuerySet.  The
  305
+        extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax).
  306
+        """
  307
+        EXTENT = SpatialBackend.extent
  308
+        if not EXTENT:
  309
+            raise ImproperlyConfigured('Extent stored procedure not available.')
  310
+
  311
+        if not field_name:
  312
+            field_name = self._get_geofield()
  313
+
  314
+        field_col = self._geo_column(field_name)
  315
+        if not field_col:
  316
+            raise TypeError('Extent information only available on GeometryFields.')
  317
+
  318
+        # Getting the SQL for the query.
  319
+        try:
  320
+            select, sql, params = self._get_sql_clause()
  321
+        except EmptyResultSet:
  322
+            return None
  323
+        
  324
+        # Constructing the query that will select the extent.
  325
+        extent_sql = ('SELECT %s(%s)' % (EXTENT, field_col)) + sql
  326
+
  327
+        # Getting a cursor, executing the query, and extracting the returned
  328
+        # value from the extent function.
  329
+        cursor = connection.cursor()
  330
+        cursor.execute(extent_sql, params)
  331
+        box = cursor.fetchone()[0]
  332
+
  333
+        if box: 
  334
+            # TODO: Parsing of BOX3D, Oracle support (patches welcome!)
  335
+            #  Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)"; 
  336
+            #  parsing out and returning as a 4-tuple.
  337
+            ll, ur = box[4:-1].split(',')
  338
+            xmin, ymin = map(float, ll.split())
  339
+            xmax, ymax = map(float, ur.split())
  340
+            return (xmin, ymin, xmax, ymax)
  341
+        else: 
  342
+            return None
  343
+
302 344
     def gml(self, field_name=None, precision=8, version=2):
303 345
         """
304 346
         Returns GML representation of the given field in a `gml` attribute
305 347
         on each element of the GeoQuerySet.
306 348
         """
307 349
         # Is GML output supported?
  350
+        ASGML = SpatialBackend.as_gml
308 351
         if not ASGML:
309 352
             raise ImproperlyConfigured('AsGML() stored procedure not available.')
310 353
 
@@ -322,7 +365,7 @@ def gml(self, field_name=None, precision=8, version=2):
322 365
         elif postgis:
323 366
             # PostGIS AsGML() aggregate function parameter order depends on the
324 367
             # version -- uggh.
325  
-            major, minor1, minor2 = VERSION
  368
+            major, minor1, minor2 = SpatialBackend.version
326 369
             if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)):
327 370
                 gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, version, field_col, precision)}
328 371
             else:
@@ -337,6 +380,7 @@ def kml(self, field_name=None, precision=8):
337 380
         attribute on each element of the GeoQuerySet.
338 381
         """
339 382
         # Is KML output supported?
  383
+        ASKML = SpatialBackend.as_kml
340 384
         if not ASKML:
341 385
             raise ImproperlyConfigured('AsKML() stored procedure not available.')
342 386
 
@@ -375,6 +419,7 @@ def transform(self, field_name=None, srid=4326):
375 419
 
376 420
         # Setting the key for the field's column with the custom SELECT SQL to 
377 421
         # override the geometry column returned from the database.
  422
+        TRANSFORM = SpatialBackend.transform
378 423
         if oracle:
379 424
             custom_sel = '%s(%s, %s)' % (TRANSFORM, col, srid)
380 425
             self._ewkt = srid
@@ -391,6 +436,7 @@ def union(self, field_name=None, tolerance=0.0005):
391 436
         Oracle backends only.
392 437
         """
393 438
         # Making sure backend supports the Union stored procedure
  439
+        UNION = SpatialBackend.union
394 440
         if not UNION:
395 441
             raise ImproperlyConfigured('Union stored procedure not available.')
396 442
 
14  django/contrib/gis/tests/geoapp/tests.py
@@ -172,6 +172,20 @@ def test04_transform(self):
172 172
             self.assertAlmostEqual(ptown.x, p.point.x, prec)
173 173
             self.assertAlmostEqual(ptown.y, p.point.y, prec)
174 174
 
  175
+    @no_oracle # Most likely can do this in Oracle, however, it is not yet implemented (patches welcome!)
  176
+    def test05_extent(self):
  177
+        "Testing the extent() GeoManager method."
  178
+        # Reference query:
  179
+        # `SELECT ST_extent(point) FROM geoapp_city WHERE (name='Houston' or name='Dallas');`
  180
+        #   =>  BOX(-96.8016128540039 29.7633724212646,-95.3631439208984 32.7820587158203)
  181
+        expected = (-96.8016128540039, 29.7633724212646, -95.3631439208984, 32.782058715820)
  182
+
  183
+        qs = City.objects.filter(name__in=('Houston', 'Dallas'))
  184
+        extent = qs.extent()
  185
+
  186
+        for val, exp in zip(extent, expected):
  187
+            self.assertAlmostEqual(exp, val, 8)
  188
+
175 189
     def test09_disjoint(self):
176 190
         "Testing the `disjoint` lookup type."
177 191
         if DISABLE: return
4  django/contrib/gis/utils/layermapping.py
@@ -112,7 +112,7 @@ def __str__(self):
112 112
 from decimal import Decimal
113 113
 from django.core.exceptions import ObjectDoesNotExist
114 114
 from django.contrib.gis.db.models.fields import GeometryField
115  
-from django.contrib.gis.db.backend import SPATIAL_BACKEND
  115
+from django.contrib.gis.db.backend import SpatialBackend
116 116
 from django.contrib.gis.gdal import CoordTransform, DataSource, \
117 117
     OGRException, OGRGeometry, OGRGeomType, SpatialReference
118 118
 from django.contrib.gis.gdal.field import \
@@ -506,7 +506,7 @@ def geometry_column(self):
506 506
         # Getting the GeometryColumn object.
507 507
         try:
508 508
             db_table = self.model._meta.db_table
509  
-            if SPATIAL_BACKEND == 'oracle': db_table = db_table.upper()
  509
+            if SpatialBackend.name == 'oracle': db_table = db_table.upper()
510 510
             gc_kwargs = {GeometryColumns.table_name_col() : db_table}
511 511
             return GeometryColumns.objects.get(**gc_kwargs)
512 512
         except Exception, msg:

0 notes on commit 398eca3

Please sign in to comment.
Something went wrong with that request. Please try again.