Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

gis: Added distance querying capabilites via the `distance` manager m…

…ethod and the `distance_[gt|gte|lt|lte]` lookup types (works for both PostGIS and Oracle); improved Oracle query construction and fixed `transform` issues.

git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@6886 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit fae19f8cedf14c946febd10513bfbaf9377eaf64 1 parent cc0cc9f
Justin Bronn authored December 04, 2007

Showing 26 changed files with 786 additions and 269 deletions. Show diff stats Hide diff stats

  1. 47  django/contrib/gis/db/backend/__init__.py
  2. 2  django/contrib/gis/db/backend/oracle/__init__.py
  3. 48  django/contrib/gis/db/backend/oracle/field.py
  4. 6  django/contrib/gis/db/backend/oracle/models.py
  5. 131  django/contrib/gis/db/backend/oracle/query.py
  6. 2  django/contrib/gis/db/backend/postgis/__init__.py
  7. 4  django/contrib/gis/db/backend/postgis/adaptor.py
  8. 68  django/contrib/gis/db/backend/postgis/field.py
  9. 10  django/contrib/gis/db/backend/postgis/models.py
  10. 105  django/contrib/gis/db/backend/postgis/query.py
  11. 3  django/contrib/gis/db/backend/util.py
  12. 110  django/contrib/gis/db/models/fields/__init__.py
  13. 3  django/contrib/gis/db/models/manager.py
  14. 150  django/contrib/gis/db/models/query.py
  15. 99  django/contrib/gis/models.py
  16. 13  django/contrib/gis/tests/__init__.py
  17. BIN  django/contrib/gis/tests/distapp/cities/cities.dbf
  18. 1  django/contrib/gis/tests/distapp/cities/cities.prj
  19. BIN  django/contrib/gis/tests/distapp/cities/cities.shp
  20. BIN  django/contrib/gis/tests/distapp/cities/cities.shx
  21. 20  django/contrib/gis/tests/distapp/models.py
  22. 86  django/contrib/gis/tests/distapp/tests.py
  23. 4  django/contrib/gis/tests/geoapp/models.py
  24. 8  django/contrib/gis/tests/geoapp/sql/city.oracle.sql
  25. 135  django/contrib/gis/tests/geoapp/tests.py
  26. 0  gis/tests/distapp/__init__.py b/django/contrib/gis/tests/distapp/__init__.py
47  django/contrib/gis/db/backend/__init__.py
@@ -26,41 +26,33 @@
26 26
 from django.contrib.gis.geos import GEOSGeometry
27 27
 
28 28
 # These routines (needed by GeoManager), default to False.
29  
-ASGML, ASKML, TRANSFORM, UNION= (False, False, False, False)
  29
+ASGML, ASKML, DISTANCE, TRANSFORM, UNION= (False, False, False, False, False)
30 30
 
31 31
 if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
32 32
     # PostGIS is the spatial database, getting the rquired modules, 
33 33
     # renaming as necessary.
34 34
     from django.contrib.gis.db.backend.postgis import \
35 35
         PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
36  
-        create_spatial_db, get_geo_where_clause, gqn, \
37  
-        ASGML, ASKML, GEOM_SELECT, TRANSFORM, UNION
  36
+        create_spatial_db, get_geo_where_clause, \
  37
+        ASGML, ASKML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
38 38
     SPATIAL_BACKEND = 'postgis'
39 39
 elif settings.DATABASE_ENGINE == 'oracle':
40 40
     from django.contrib.gis.db.backend.oracle import \
41 41
          OracleSpatialField as GeoBackendField, \
42 42
          ORACLE_SPATIAL_TERMS as GIS_TERMS, \
43  
-         create_spatial_db, get_geo_where_clause, gqn, \
44  
-         ASGML, GEOM_SELECT, TRANSFORM, UNION
  43
+         create_spatial_db, get_geo_where_clause, \
  44
+         ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
45 45
     SPATIAL_BACKEND = 'oracle'
46 46
 elif settings.DATABASE_ENGINE == 'mysql':
47 47
     from django.contrib.gis.db.backend.mysql import \
48 48
         MySQLGeoField as GeoBackendField, \
49 49
         MYSQL_GIS_TERMS as GIS_TERMS, \
50  
-        create_spatial_db, get_geo_where_clause, gqn, \
  50
+        create_spatial_db, get_geo_where_clause, \
51 51
         GEOM_SELECT
52 52
     SPATIAL_BACKEND = 'mysql'
53 53
 else:
54 54
     raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
55 55
 
56  
-def geo_quotename(value):
57  
-    """
58  
-    Returns the quotation used on a given Geometry value using the geometry
59  
-    quoting from the backend (the `gqn` function).
60  
-    """
61  
-    if isinstance(value, (StringType, UnicodeType)): return gqn(value)
62  
-    else: return str(value)
63  
-
64 56
 ####    query.py overloaded functions    ####
65 57
 # parse_lookup() and lookup_inner() are modified from their django/db/models/query.py
66 58
 #  counterparts to support constructing SQL for geographic queries.
@@ -117,7 +109,7 @@ def parse_lookup(kwarg_items, opts):
117 109
                 raise ValueError, "Cannot use None as a query value"
118 110
         elif callable(value):
119 111
             value = value()
120  
-
  112
+        
121 113
         joins2, where2, params2 = lookup_inner(path, lookup_type, value, opts, opts.db_table, None)
122 114
         joins.update(joins2)
123 115
         where.extend(where2)
@@ -287,28 +279,15 @@ def lookup_inner(path, lookup_type, value, opts, table, column):
287 279
         # If the field is a geometry field, then the WHERE clause will need to be obtained
288 280
         # with the get_geo_where_clause()
289 281
         if hasattr(field, '_geom'):
290  
-            # Do we have multiple arguments, e.g., `relate`, `dwithin` lookup types
291  
-            # need more than argument.
292  
-            multiple_args = isinstance(value, tuple)
293  
-
294 282
             # Getting the preparation SQL object from the field.
295  
-            if multiple_args:
296  
-                geo_prep = field.get_db_prep_lookup(lookup_type, value[0])
297  
-            else:
298  
-                geo_prep = field.get_db_prep_lookup(lookup_type, value)
299  
-
  283
+            geo_prep = field.get_db_prep_lookup(lookup_type, value)
  284
+            
300 285
             # Getting the adapted geometry from the field.
301 286
             gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)
302  
-            
303  
-            # A GeoFieldSQL object is returned by `get_db_prep_lookup` -- 
304  
-            # getting the substitution list and the geographic parameters.
305  
-            subst_list = geo_prep.where
306  
-            if multiple_args: subst_list += map(geo_quotename, value[1:])
307  
-            gwc = gwc % tuple(subst_list)
308  
-            
309  
-            # Finally, appending onto the WHERE clause, and extending with
310  
-            # the additional parameters.
311  
-            where.append(gwc)
  287
+
  288
+            # Substituting in the the where parameters into the geographic where
  289
+            # clause, and extending the parameters.
  290
+            where.append(gwc % tuple(geo_prep.where))
312 291
             params.extend(geo_prep.params)
313 292
         else:
314 293
             where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type))
2  django/contrib/gis/db/backend/oracle/__init__.py
@@ -10,5 +10,5 @@
10 10
 from django.contrib.gis.db.backend.oracle.field import OracleSpatialField, gqn
11 11
 from django.contrib.gis.db.backend.oracle.query import \
12 12
      get_geo_where_clause, ORACLE_SPATIAL_TERMS, \
13  
-     ASGML, GEOM_SELECT, TRANSFORM, UNION
  13
+     ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
14 14
 
48  django/contrib/gis/db/backend/oracle/field.py
@@ -6,7 +6,7 @@
6 6
 from django.contrib.gis.geos import GEOSGeometry
7 7
 from django.contrib.gis.db.backend.util import get_srid, GeoFieldSQL
8 8
 from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
9  
-from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, TRANSFORM
  9
+from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, DISTANCE_FUNCTIONS, TRANSFORM
10 10
 
11 11
 # Quotename & geographic quotename, respectively.
12 12
 qn = connection.ops.quote_name
@@ -21,12 +21,12 @@ class OracleSpatialField(Field):
21 21
 
22 22
     empty_strings_allowed = False
23 23
 
24  
-    def __init__(self, extent=(-180.0, -90.0, 180.0, 90.0), tolerance=0.00005, **kwargs):
  24
+    def __init__(self, extent=(-180.0, -90.0, 180.0, 90.0), tolerance=0.05, **kwargs):
25 25
         """
26 26
         Oracle Spatial backend needs to have the extent -- for projected coordinate
27 27
         systems _you must define the extent manually_, since the coordinates are
28 28
         for geodetic systems.  The `tolerance` keyword specifies the tolerance
29  
-        for error (in meters).
  29
+        for error (in meters), and defaults to 0.05 (5 centimeters).
30 30
         """
31 31
         # Oracle Spatial specific keyword arguments.
32 32
         self._extent = extent
@@ -104,32 +104,32 @@ def get_db_prep_lookup(self, lookup_type, value):
104 104
             # special case for isnull lookup
105 105
             if lookup_type == 'isnull': return GeoFieldSQL([], [])
106 106
 
107  
-            # When the input is not a GEOS geometry, attempt to construct one
108  
-            # from the given string input.
109  
-            if isinstance(value, GEOSGeometry):
110  
-                pass
111  
-            elif isinstance(value, (StringType, UnicodeType)):
112  
-                try:
113  
-                    value = GEOSGeometry(value)
114  
-                except GEOSException:
115  
-                    raise TypeError("Could not create geometry from lookup value: %s" % str(value))
116  
-            else:
117  
-                raise TypeError('Cannot use parameter of %s type as lookup parameter.' % type(value))
118  
-
119  
-            # Getting the SRID of the geometry, or defaulting to that of the field if
120  
-            # it is None.
121  
-            srid = get_srid(self, value)
  107
+            # Get the geometry with SRID; defaults SRID to that
  108
+            # of the field if it is None
  109
+            geom = self.get_geometry(value)
122 110
             
123 111
             # The adaptor will be used by psycopg2 for quoting the WKT.
124  
-            adapt = OracleSpatialAdaptor(value)
125  
-            if srid != self._srid:
  112
+            adapt = OracleSpatialAdaptor(geom)
  113
+
  114
+            if geom.srid != self._srid:
126 115
                 # Adding the necessary string substitutions and parameters
127 116
                 # to perform a geometry transformation.
128  
-                return GeoFieldSQL(['%s(SDO_GEOMETRY(%%s, %s), %%s)' % (TRANSFORM, srid)],
129  
-                                   [adapt, self._srid])
  117
+                where = ['%s(SDO_GEOMETRY(%%s, %s), %%s)' % (TRANSFORM, geom.srid)]
  118
+                params = [adapt, self._srid]
130 119
             else:
131  
-                return GeoFieldSQL(['SDO_GEOMETRY(%%s, %s)' % srid], [adapt])
132  
-
  120
+                where = ['SDO_GEOMETRY(%%s, %s)' % geom.srid]
  121
+                params = [adapt]
  122
+
  123
+            if isinstance(value, tuple):
  124
+                if lookup_type in DISTANCE_FUNCTIONS or lookup_type == 'dwithin':
  125
+                    # Getting the distance parameter in the units of the field
  126
+                    where += [self.get_distance(value[1])]
  127
+                elif lookup_type == 'relate':
  128
+                    # No extra where parameters for SDO_RELATE queries.
  129
+                    pass
  130
+                else:
  131
+                    where += map(gqn, value[1:])
  132
+            return GeoFieldSQL(where, params)
133 133
         else:
134 134
             raise TypeError("Field has invalid lookup: %s" % lookup_type)
135 135
 
6  django/contrib/gis/db/backend/oracle/models.py
@@ -20,7 +20,7 @@ class Meta:
20 20
         db_table = 'USER_SDO_GEOM_METADATA'
21 21
 
22 22
     @classmethod
23  
-    def table_name_col(self):
  23
+    def table_name_col(cls):
24 24
         return 'table_name'
25 25
 
26 26
     def __unicode__(self):
@@ -43,3 +43,7 @@ class Meta:
43 43
     @property
44 44
     def wkt(self):
45 45
         return self.wktext
  46
+
  47
+    @classmethod
  48
+    def wkt_col(cls):
  49
+        return 'wktext'
131  django/contrib/gis/db/backend/oracle/query.py
@@ -2,24 +2,99 @@
2 2
  This module contains the spatial lookup types, and the get_geo_where_clause()
3 3
  routine for Oracle Spatial.
4 4
 """
  5
+import re
  6
+from decimal import Decimal
5 7
 from django.db import connection
  8
+from django.contrib.gis.measure import Distance
6 9
 qn = connection.ops.quote_name
7 10
 
  11
+# The GML, distance, transform, and union procedures.
  12
+ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
  13
+DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
  14
+TRANSFORM = 'SDO_CS.TRANSFORM'
  15
+UNION = 'SDO_AGGR_UNION'
  16
+
  17
+class SDOOperation(object):
  18
+    "Base class for SDO* Oracle operations."
  19
+
  20
+    def __init__(self, lookup, subst='', operator='=', result="'TRUE'",
  21
+                 beg_subst='%s(%s%s, %%s'):
  22
+        self.lookup = lookup
  23
+        self.subst = subst
  24
+        self.operator = operator
  25
+        self.result = result
  26
+        self.beg_subst = beg_subst
  27
+        self.end_subst = ') %s %s' % (self.operator, self.result)
  28
+
  29
+    @property
  30
+    def sql_subst(self):
  31
+        return ''.join([self.beg_subst, self.subst, self.end_subst])
  32
+
  33
+    def as_sql(self, table, field):
  34
+        return self.sql_subst % self.params(table, field)
  35
+
  36
+    def params(self, table, field):
  37
+        return (self.lookup, table, field)
  38
+
  39
+class SDODistance(SDOOperation):
  40
+    "Class for Distance queries."
  41
+    def __init__(self, op, tolerance=0.05):
  42
+        super(SDODistance, self).__init__(DISTANCE, subst=", %s", operator=op, result='%%s')
  43
+        self.tolerance = tolerance
  44
+
  45
+    def params(self, table, field):
  46
+        return (self.lookup, table, field, self.tolerance)
  47
+
  48
+class SDOGeomRelate(SDOOperation):
  49
+    "Class for using SDO_GEOM.RELATE."
  50
+    def __init__(self, mask, tolerance=0.05):
  51
+        super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE',  beg_subst="%s(%s%s, '%s'",
  52
+                                            subst=", %%s, %s", result="'%s'" % mask)
  53
+        self.mask = mask
  54
+        self.tolerance = tolerance
  55
+
  56
+    def params(self, table, field):
  57
+        return (self.lookup, table, field, self.mask, self.tolerance)
  58
+
  59
+class SDORelate(SDOOperation):
  60
+    "Class for using SDO_RELATE."
  61
+    masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
  62
+    mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
  63
+    
  64
+    def __init__(self, mask, **kwargs):
  65
+        super(SDORelate, self).__init__('SDO_RELATE',  subst=", 'mask=%s'", **kwargs)
  66
+        if not self.mask_regex.match(mask):
  67
+            raise ValueError('Invalid %s mask: "%s"' % (self.lookup, mask))
  68
+        self.mask = mask
  69
+
  70
+    def params(self, table, field):
  71
+        return (self.lookup, table, field, self.mask)
  72
+
  73
+# Valid distance types and substitutions
  74
+dtypes = (Decimal, Distance, float, int)
  75
+DISTANCE_FUNCTIONS = {
  76
+    'distance_gt' : (SDODistance('>'), dtypes),
  77
+    'distance_gte' : (SDODistance('>='), dtypes),
  78
+    'distance_lt' : (SDODistance('<'), dtypes),
  79
+    'distance_lte' : (SDODistance('<='), dtypes),
  80
+    }
  81
+
8 82
 ORACLE_GEOMETRY_FUNCTIONS = {
9  
-    'contains' : 'SDO_CONTAINS',
10  
-    'coveredby' : 'SDO_COVEREDBY',
11  
-    'covers' : 'SDO_COVERS',
12  
-    'disjoint' : 'SDO_DISJOINT',
13  
-    'dwithin' : ('SDO_WITHIN_DISTANCE', float),
14  
-    'intersects' : 'SDO_OVERLAPBDYINTERSECT', # TODO: Is this really the same as ST_Intersects()?
15  
-    'equals' : 'SDO_EQUAL',
16  
-    'exact' : 'SDO_EQUAL',
17  
-    'overlaps' : 'SDO_OVERLAPS',
18  
-    'same_as' : 'SDO_EQUAL',
19  
-    #'relate' : ('SDO_RELATE', str), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
20  
-    'touches' : 'SDO_TOUCH',
21  
-    'within' : 'SDO_INSIDE',
  83
+    'contains' : SDOOperation('SDO_CONTAINS'),
  84
+    'coveredby' : SDOOperation('SDO_COVEREDBY'),
  85
+    'covers' : SDOOperation('SDO_COVERS'),
  86
+    'disjoint' : SDOGeomRelate('DISJOINT'),
  87
+    'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', "%%s, 'distance=%%s'"), dtypes),
  88
+    'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
  89
+    'equals' : SDOOperation('SDO_EQUAL'),
  90
+    'exact' : SDOOperation('SDO_EQUAL'),
  91
+    'overlaps' : SDOOperation('SDO_OVERLAPS'),
  92
+    'same_as' : SDOOperation('SDO_EQUAL'),
  93
+    'relate' : (SDORelate, basestring), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
  94
+    'touches' : SDOOperation('SDO_TOUCH'),
  95
+    'within' : SDOOperation('SDO_INSIDE'),
22 96
     }
  97
+ORACLE_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
23 98
 
24 99
 # This lookup type does not require a mapping.
25 100
 MISC_TERMS = ['isnull']
@@ -43,25 +118,33 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
43 118
         if isinstance(lookup_info, tuple):
44 119
             # First element of tuple is lookup type, second element is the type
45 120
             #  of the expected argument (e.g., str, float)
46  
-            func, arg_type = lookup_info
  121
+            sdo_op, arg_type = lookup_info
47 122
 
48 123
             # Ensuring that a tuple _value_ was passed in from the user
49  
-            if not isinstance(value, tuple) or len(value) != 2: 
50  
-                raise TypeError('2-element tuple required for %s lookup type.' % lookup_type)
  124
+            if not isinstance(value, tuple):
  125
+                raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
  126
+            if len(value) != 2: 
  127
+                raise ValueError('2-element tuple required for %s lookup type.' % lookup_type)
51 128
             
52 129
             # Ensuring the argument type matches what we expect.
53 130
             if not isinstance(value[1], arg_type):
54 131
                 raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
55 132
 
56  
-            if func == 'dwithin':
57  
-                # TODO: test and consider adding different distance options.
58  
-                return "%s(%s, %%s, 'distance=%s')" % (func, table_prefix + field_name, value[1])
  133
+            if lookup_type == 'relate':
  134
+                # The SDORelate class handles construction for these queries, and verifies
  135
+                # the mask argument.
  136
+                return sdo_op(value[1]).as_sql(table_prefix, field_name)
  137
+            elif lookup_type in DISTANCE_FUNCTIONS:
  138
+                op = DISTANCE_FUNCTIONS[lookup_type][0]
  139
+                return op.as_sql(table_prefix, field_name)
  140
+                #    return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op)
59 141
             else:
60  
-                return "%s(%s, %%s, %%s) = 'TRUE'" % (func, table_prefix + field_name)
  142
+                return sdo_op.as_sql(table_prefix, field_name)
61 143
         else:
62  
-            # Returning the SQL necessary for the geometry function call. For example: 
  144
+            # Lookup info is a SDOOperation instance, whos `as_sql` method returns
  145
+            # the SQL necessary for the geometry function call. For example:  
63 146
             #  SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
64  
-            return "%s(%s, %%s) = 'TRUE'" % (lookup_info, table_prefix + field_name)
  147
+            return lookup_info.as_sql(table_prefix, field_name)
65 148
     
66 149
     # Handling 'isnull' lookup type
67 150
     if lookup_type == 'isnull':
@@ -69,10 +152,6 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
69 152
 
70 153
     raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
71 154
 
72  
-ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
73  
-UNION = 'SDO_AGGR_UNION'
74  
-TRANSFORM = 'SDO_CS.TRANSFORM'
75  
-
76 155
 # Want to get SDO Geometries as WKT (much easier to instantiate GEOS proxies
77 156
 # from WKT than SDO_GEOMETRY(...) strings ;)
78 157
 GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
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, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT
  9
+    ASKML, ASGML, DISTANCE, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT
4  django/contrib/gis/db/backend/postgis/adaptor.py
@@ -7,11 +7,11 @@
7 7
 from psycopg2.extensions import ISQLQuote
8 8
 
9 9
 class PostGISAdaptor(object):
10  
-    def __init__(self, geom, srid):
  10
+    def __init__(self, geom):
11 11
         "Initializes on the geometry and the SRID."
12 12
         # Getting the WKB and the SRID
13 13
         self.wkb = geom.wkb
14  
-        self.srid = srid
  14
+        self.srid = geom.srid
15 15
 
16 16
     def __conform__(self, proto):
17 17
         # Does the given protocol conform to what Psycopg2 expects?
68  django/contrib/gis/db/backend/postgis/field.py
... ...
@@ -1,19 +1,26 @@
1  
-from types import StringType, UnicodeType
  1
+from types import UnicodeType
2 2
 from django.db import connection
3 3
 from django.db.models.fields import Field # Django base Field class
4 4
 from django.contrib.gis.geos import GEOSGeometry, GEOSException 
5 5
 from django.contrib.gis.db.backend.util import get_srid, GeoFieldSQL
6 6
 from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
7  
-from django.contrib.gis.db.backend.postgis.query import POSTGIS_TERMS, TRANSFORM
8  
-from psycopg2 import Binary
  7
+from django.contrib.gis.db.backend.postgis.query import \
  8
+    DISTANCE, DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
9 9
 
10 10
 # Quotename & geographic quotename, respectively
11 11
 qn = connection.ops.quote_name
12 12
 def gqn(value):
13  
-    if isinstance(value, UnicodeType): value = value.encode('ascii')
14  
-    return "'%s'" % value
  13
+    if isinstance(value, basestring):
  14
+        if isinstance(value, UnicodeType): value = value.encode('ascii')
  15
+        return "'%s'" % value
  16
+    else: 
  17
+        return str(value)
15 18
 
16 19
 class PostGISField(Field):
  20
+    """
  21
+    The backend-specific geographic field for PostGIS.
  22
+    """
  23
+
17 24
     def _add_geom(self, style, db_table):
18 25
         """
19 26
         Constructs the addition of the geometry to the table using the
@@ -92,53 +99,44 @@ def get_db_prep_lookup(self, lookup_type, value):
92 99
         """
93 100
         if lookup_type in POSTGIS_TERMS:
94 101
             # special case for isnull lookup
95  
-            if lookup_type == 'isnull':
96  
-                return GeoFieldSQL([], [value])
97  
-
98  
-            # When the input is not a GEOS geometry, attempt to construct one
99  
-            # from the given string input.
100  
-            if isinstance(value, GEOSGeometry):
101  
-                pass
102  
-            elif isinstance(value, (StringType, UnicodeType)):
103  
-                try:
104  
-                    value = GEOSGeometry(value)
105  
-                except GEOSException:
106  
-                    raise TypeError("Could not create geometry from lookup value: %s" % str(value))
107  
-            else:
108  
-                raise TypeError('Cannot use parameter of %s type as lookup parameter.' % type(value))
  102
+            if lookup_type == 'isnull': return GeoFieldSQL([], [])
109 103
 
110  
-            # Getting the SRID of the geometry, or defaulting to that of the field if
111  
-            # it is None.
112  
-            srid = get_srid(self, value)
  104
+            # Get the geometry with SRID; defaults SRID to 
  105
+            # that of the field if it is None.
  106
+            geom = self.get_geometry(value)
113 107
 
114 108
             # The adaptor will be used by psycopg2 for quoting the WKB.
115  
-            adapt = PostGISAdaptor(value, srid)
  109
+            adapt = PostGISAdaptor(geom)
116 110
 
117  
-            if srid != self._srid:
  111
+            if geom.srid != self._srid:
118 112
                 # Adding the necessary string substitutions and parameters
119 113
                 # to perform a geometry transformation.
120  
-                return GeoFieldSQL(['%s(%%s,%%s)' % TRANSFORM],
121  
-                                   [adapt, self._srid])
  114
+                where = ['%s(%%s,%%s)' % TRANSFORM]
  115
+                params = [adapt, self._srid]
122 116
             else:
123  
-                return GeoFieldSQL(['%s'], [adapt])
  117
+                # Otherwise, the adaptor will take care of everything.
  118
+                where = ['%s']
  119
+                params = [adapt]
  120
+
  121
+            if isinstance(value, tuple):
  122
+                if lookup_type in DISTANCE_FUNCTIONS or lookup_type == 'dwithin':
  123
+                    # Getting the distance parameter in the units of the field.
  124
+                    where += [self.get_distance(value[1])]
  125
+                else:
  126
+                    where += map(gqn, value[1:])
  127
+            return GeoFieldSQL(where, params)
124 128
         else:
125 129
             raise TypeError("Field has invalid lookup: %s" % lookup_type)
126 130
 
  131
+
127 132
     def get_db_prep_save(self, value):
128 133
         "Prepares the value for saving in the database."
129 134
         if not bool(value): return None
130 135
         if isinstance(value, GEOSGeometry):
131  
-            return PostGISAdaptor(value, value.srid)
  136
+            return PostGISAdaptor(value)
132 137
         else:
133 138
             raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
134 139
 
135  
-    def get_internal_type(self):
136  
-        """
137  
-        Returns NoField because a stored procedure is used by PostGIS to create
138  
-        the Geometry Fields.
139  
-        """
140  
-        return 'NoField'
141  
-
142 140
     def get_placeholder(self, value):
143 141
         """
144 142
         Provides a proper substitution value for Geometries that are not in the
10  django/contrib/gis/db/backend/postgis/models.py
@@ -16,17 +16,17 @@ class GeometryColumns(models.Model):
16 16
     """
17 17
     f_table_catalog = models.CharField(maxlength=256)
18 18
     f_table_schema = models.CharField(maxlength=256)
19  
-    f_table_name = models.CharField(maxlength=256, primary_key=True)
  19
+    f_table_name = models.CharField(maxlength=256)
20 20
     f_geometry_column = models.CharField(maxlength=256)
21 21
     coord_dimension = models.IntegerField()
22  
-    srid = models.IntegerField()
  22
+    srid = models.IntegerField(primary_key=True)
23 23
     type = models.CharField(maxlength=30)
24 24
 
25 25
     class Meta:
26 26
         db_table = 'geometry_columns'
27 27
 
28 28
     @classmethod
29  
-    def table_name_col(self):
  29
+    def table_name_col(cls):
30 30
         "Class method for returning the table name column for this model."
31 31
         return 'f_table_name'
32 32
 
@@ -52,3 +52,7 @@ class Meta:
52 52
     @property
53 53
     def wkt(self):
54 54
         return self.srtext
  55
+
  56
+    @classmethod
  57
+    def wkt_col(cls):
  58
+        return 'srtext'
105  django/contrib/gis/db/backend/postgis/query.py
@@ -2,9 +2,10 @@
2 2
  This module contains the spatial lookup types, and the get_geo_where_clause()
3 3
  routine for PostGIS.
4 4
 """
  5
+from decimal import Decimal
5 6
 from django.db import connection
  7
+from django.contrib.gis.measure import Distance
6 8
 from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple
7  
-from types import StringType, UnicodeType
8 9
 qn = connection.ops.quote_name
9 10
 
10 11
 # Getting the PostGIS version information
@@ -62,16 +63,38 @@
62 63
 # Versions of PostGIS >= 1.2.2 changed their naming convention to be
63 64
 #  'SQL-MM-centric' to conform with the ISO standard. Practically, this 
64 65
 #  means that 'ST_' is prefixes geometry function names.
65  
-if MAJOR_VERSION > 1 or (MAJOR_VERSION == 1 and (MINOR_VERSION1 > 2 or (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2))):
66  
-    GEOM_FUNC_PREFIX = 'ST_'
  66
+GEOM_FUNC_PREFIX = ''
  67
+if MAJOR_VERSION >= 1:
  68
+    if (MINOR_VERSION1 > 2 or 
  69
+        (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)):
  70
+        GEOM_FUNC_PREFIX = 'ST_'
  71
+    
  72
+    def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
  73
+
  74
+    # Functions used by the GeoManager & GeoQuerySet
  75
+    ASKML = get_func('AsKML')
  76
+    ASGML = get_func('AsGML')
  77
+    DISTANCE = get_func('Distance')
  78
+    GEOM_FROM_TEXT = get_func('GeomFromText')
  79
+    GEOM_FROM_WKB = get_func('GeomFromWKB')
  80
+    TRANSFORM = get_func('Transform')
  81
+
  82
+    # Special cases for union and KML methods.
  83
+    if MINOR_VERSION1 < 3:
  84
+        UNION = 'GeomUnion'
  85
+    else:
  86
+        UNION = 'ST_Union'
  87
+
  88
+    if MINOR_VERSION1 == 1:
  89
+        ASKML = False
67 90
 else:
68  
-    GEOM_FUNC_PREFIX = ''
  91
+    raise NotImplementedError('PostGIS versions < 1.0 are not supported.')
69 92
 
70 93
 # For PostGIS >= 1.2.2 the following lookup types will do a bounding box query
71  
-#  first before calling the more computationally expensive GEOS routines (called
72  
-#  "inline index magic"):
73  
-#    'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and
74  
-#    'covers'.
  94
+# first before calling the more computationally expensive GEOS routines (called
  95
+# "inline index magic"):
  96
+# 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and
  97
+# 'covers'.
75 98
 POSTGIS_GEOMETRY_FUNCTIONS = {
76 99
     'equals' : 'Equals',
77 100
     'disjoint' : 'Disjoint',
@@ -81,25 +104,36 @@
81 104
     'overlaps' : 'Overlaps',
82 105
     'contains' : 'Contains',
83 106
     'intersects' : 'Intersects',
84  
-    'relate' : ('Relate', str),
  107
+    'relate' : ('Relate', basestring),
  108
+    }
  109
+
  110
+# Valid distance types and substitutions
  111
+dtypes = (Decimal, Distance, float, int)
  112
+DISTANCE_FUNCTIONS = {
  113
+    'distance_gt' : ('>', dtypes),
  114
+    'distance_gte' : ('>=', dtypes),
  115
+    'distance_lt' : ('<', dtypes),
  116
+    'distance_lte' : ('<=', dtypes),
85 117
     }
86 118
 
87 119
 if GEOM_FUNC_PREFIX == 'ST_':
88 120
     # Adding the GEOM_FUNC_PREFIX to the lookup functions.
89  
-    for lookup, func in POSTGIS_GEOMETRY_FUNCTIONS.items():
90  
-        if isinstance(func, tuple):
91  
-            POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (GEOM_FUNC_PREFIX + func[0], func[1])
  121
+    for lookup, f in POSTGIS_GEOMETRY_FUNCTIONS.items():
  122
+        if isinstance(f, tuple):
  123
+            POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (get_func(f[0]), f[1])
92 124
         else:
93  
-            POSTGIS_GEOMETRY_FUNCTIONS[lookup] = GEOM_FUNC_PREFIX + func
  125
+            POSTGIS_GEOMETRY_FUNCTIONS[lookup] = get_func(f)
94 126
 
95 127
     # The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
96 128
     POSTGIS_GEOMETRY_FUNCTIONS.update(
97  
-        {'dwithin' : ('ST_DWithin', float),
  129
+        {'dwithin' : ('ST_DWithin', dtypes),
98 130
          'coveredby' : 'ST_CoveredBy',
99 131
          'covers' : 'ST_Covers',
100 132
          }
101 133
         )
102 134
 
  135
+POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
  136
+
103 137
 # Any other lookup types that do not require a mapping.
104 138
 MISC_TERMS = ['isnull']
105 139
 
@@ -139,52 +173,31 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
139 173
             func, arg_type = lookup_info
140 174
 
141 175
             # Ensuring that a tuple _value_ was passed in from the user
142  
-            if not isinstance(value, tuple) or len(value) != 2: 
143  
-                raise TypeError('2-element tuple required for `%s` lookup type.' % lookup_type)
  176
+            if not isinstance(value, tuple): 
  177
+                raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
  178
+            if len(value) != 2:
  179
+                raise ValueError('2-element tuple required or `%s` lookup type.' % lookup_type)
144 180
             
145 181
             # Ensuring the argument type matches what we expect.
146 182
             if not isinstance(value[1], arg_type):
147 183
                 raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
148  
-            
149  
-            return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name)
  184
+
  185
+            if lookup_type in DISTANCE_FUNCTIONS:
  186
+                op = DISTANCE_FUNCTIONS[lookup_type][0]
  187
+                return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op)
  188
+            else:
  189
+                return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name)
150 190
         else:
151 191
             # Returning the SQL necessary for the geometry function call. For example: 
152 192
             #  ST_Contains("geoapp_country"."poly", ST_GeomFromWKB(..))
153 193
             return '%s(%s%s, %%s)' % (lookup_info, table_prefix, field_name)
154  
-    
  194
+
155 195
     # Handling 'isnull' lookup type
156 196
     if lookup_type == 'isnull':
157 197
         return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
158 198
 
159 199
     raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
160 200
 
161  
-# Functions that we define manually.
162  
-if MAJOR_VERSION == 1:
163  
-    if MINOR_VERSION1 == 3:
164  
-        # PostGIS versions 1.3.x
165  
-        ASKML = 'ST_AsKML'
166  
-        ASGML = 'ST_AsGML'
167  
-        GEOM_FROM_TEXT = 'ST_GeomFromText'
168  
-        GEOM_FROM_WKB = 'ST_GeomFromWKB'
169  
-        UNION = 'ST_Union'
170  
-        TRANSFORM = 'ST_Transform'
171  
-    elif MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 1:
172  
-        # PostGIS versions 1.2.x
173  
-        ASKML = 'AsKML'
174  
-        ASGML = 'AsGML'
175  
-        GEOM_FROM_TEXT = 'GeomFromText'
176  
-        GEOM_FROM_WKB = 'GeomFromWKB'
177  
-        UNION = 'GeomUnion'
178  
-        TRANSFORM = 'Transform'
179  
-    elif MINOR_VERSION1 == 1 and MINOR_VERSION2 >= 0:
180  
-        # PostGIS versions 1.1.x
181  
-        ASKML = False
182  
-        ASGML = 'AsGML'
183  
-        GEOM_FROM_TEXT = 'GeomFromText'
184  
-        GEOM_FROM_WKB = 'GeomFromWKB'
185  
-        TRANSFORM = 'Transform'
186  
-        UNION = 'GeomUnion'
187  
-
188 201
 # Custom selection not needed for PostGIS since GEOS geometries may be
189 202
 # instantiated directly from the HEXEWKB returned by default.  If
190 203
 # WKT is needed for some reason in the future, this value may be changed,
3  django/contrib/gis/db/backend/util.py
@@ -7,6 +7,9 @@ def __init__(self, where=[], params=[]):
7 7
         self.where = where
8 8
         self.params = params
9 9
 
  10
+    def __str__(self):
  11
+        return self.where[0] % tuple(self.params)
  12
+
10 13
 def get_srid(field, geom):
11 14
     """
12 15
     Gets the SRID depending on the value of the SRID setting of the field
110  django/contrib/gis/db/models/fields/__init__.py
... ...
@@ -1,8 +1,17 @@
  1
+from decimal import Decimal
1 2
 from django.conf import settings
  3
+from django.db import connection
2 4
 from django.contrib.gis.db.backend import GeoBackendField # these depend on the spatial database backend.
3 5
 from django.contrib.gis.db.models.proxy import GeometryProxy
  6
+from django.contrib.gis.geos import GEOSException, GEOSGeometry
  7
+from django.contrib.gis.measure import Distance
4 8
 from django.contrib.gis.oldforms import WKTField
5  
-from django.contrib.gis.geos import GEOSGeometry
  9
+
  10
+# Attempting to get the spatial reference system.
  11
+try:
  12
+    from django.contrib.gis.models import SpatialRefSys
  13
+except NotImplementedError:
  14
+    SpatialRefSys = None
6 15
 
7 16
 #TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
8 17
 class GeometryField(GeoBackendField):
@@ -11,30 +20,107 @@ class GeometryField(GeoBackendField):
11 20
     # The OpenGIS Geometry name.
12 21
     _geom = 'GEOMETRY'
13 22
 
14  
-    def __init__(self, srid=4326, index=True, dim=2, **kwargs):
15  
-        """The initialization function for geometry fields.  Takes the following
  23
+    def __init__(self, srid=4326, spatial_index=True, dim=2, **kwargs):
  24
+        """
  25
+        The initialization function for geometry fields.  Takes the following
16 26
         as keyword arguments:
17 27
 
18  
-          srid  - The spatial reference system identifier.  An OGC standard.
19  
-                  Defaults to 4326 (WGS84)
  28
+        srid:
  29
+         The spatial reference system identifier, an OGC standard.
  30
+         Defaults to 4326 (WGS84).
20 31
 
21  
-          index - Indicates whether to create a GiST index.  Defaults to True.
22  
-                  Set this instead of 'db_index' for geographic fields since index
23  
-                  creation is different for geometry columns.
  32
+        spatial_index:
  33
+         Indicates whether to create a spatial index.  Defaults to True.
  34
+         Set this instead of 'db_index' for geographic fields since index
  35
+         creation is different for geometry columns.
24 36
                   
25  
-          dim   - The number of dimensions for this geometry.  Defaults to 2.
  37
+        dim:
  38
+         The number of dimensions for this geometry.  Defaults to 2.
26 39
         """
27  
-        self._index = index
  40
+
  41
+        # Backward-compatibility notice, this will disappear in future revisions.
  42
+        if 'index' in kwargs:
  43
+            from warnings import warn
  44
+            warn('The `index` keyword has been deprecated, please use the `spatial_index` keyword instead.')
  45
+            self._index = kwargs['index']
  46
+        else:
  47
+            self._index = spatial_index
  48
+
  49
+        # Setting the SRID and getting the units.
28 50
         self._srid = srid
  51
+        if SpatialRefSys:
  52
+            # This doesn't work when we actually use: SpatialRefSys.objects.get(srid=srid)
  53
+            # Why?  `syncdb` fails to recognize installed geographic models when there's
  54
+            # an ORM query instantiated within a model field.  No matter, this works fine
  55
+            # too.
  56
+            cur = connection.cursor()
  57
+            qn = connection.ops.quote_name
  58
+            stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)'
  59
+            stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table),
  60
+                           'wkt_col' : qn(SpatialRefSys.wkt_col()),
  61
+                           'srid_col' : qn('srid'),
  62
+                           'srid' : srid,
  63
+                           }
  64
+            cur.execute(stmt)
  65
+            row = cur.fetchone()
  66
+            self._unit, self._unit_name = SpatialRefSys.get_units(row[0])
  67
+            
  68
+        # Setting the dimension of the geometry field.
29 69
         self._dim = dim
30 70
         super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
31 71
 
  72
+    ### Routines specific to GeometryField ###
  73
+    def get_distance(self, dist):
  74
+        if isinstance(dist, Distance):
  75
+            return getattr(dist, Distance.unit_attname(self._unit_name))
  76
+        elif isinstance(dist, (int, float, Decimal)):
  77
+            # Assuming the distance is in the units of the field.
  78
+            return dist
  79
+
  80
+    def get_geometry(self, value):
  81
+        """
  82
+        Retrieves the geometry, setting the default SRID from the given
  83
+        lookup parameters.
  84
+        """
  85
+        if isinstance(value, tuple): 
  86
+            geom = value[0]
  87
+        else:
  88
+            geom = value
  89
+
  90
+        # When the input is not a GEOS geometry, attempt to construct one
  91
+        # from the given string input.
  92
+        if isinstance(geom, GEOSGeometry):
  93
+            pass
  94
+        elif isinstance(geom, basestring):
  95
+            try:
  96
+                geom = GEOSGeometry(geom)
  97
+            except GEOSException:
  98
+                raise ValueError('Could not create geometry from lookup value: %s' % str(value))
  99
+        else:
  100
+            raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
  101
+
  102
+        # Assigning the SRID value.
  103
+        geom.srid = self.get_srid(geom)
  104
+        
  105
+        return geom
  106
+
  107
+    def get_srid(self, geom):
  108
+        """
  109
+        Has logic for retrieving the default SRID taking into account 
  110
+        the SRID of the field.
  111
+        """
  112
+        if geom.srid is None or (geom.srid == -1 and self._srid != -1):
  113
+            return self._srid
  114
+        else:
  115
+            return geom.srid
  116
+
  117
+    ### Routines overloaded from Field ###
32 118
     def contribute_to_class(self, cls, name):
33 119
         super(GeometryField, self).contribute_to_class(cls, name)
34  
-
  120
+        
35 121
         # Setup for lazy-instantiated GEOSGeometry object.
36 122
         setattr(cls, self.attname, GeometryProxy(GEOSGeometry, self))
37  
-        
  123
+
38 124
     def get_manipulator_field_objs(self):
39 125
         "Using the WKTField (defined above) to be our manipulator."
40 126
         return [WKTField]
3  django/contrib/gis/db/models/manager.py
@@ -7,6 +7,9 @@ class GeoManager(Manager):
7 7
     def get_query_set(self):
8 8
         return GeoQuerySet(model=self.model)
9 9
 
  10
+    def distance(self, *args, **kwargs):
  11
+        return self.get_query_set().distance(*args, **kwargs)
  12
+
10 13
     def gml(self, *args, **kwargs):
11 14
         return self.get_query_set().gml(*args, **kwargs)
12 15
 
150  django/contrib/gis/db/models/query.py
@@ -6,9 +6,13 @@
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, ASGML, ASKML, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION
  9
+from django.contrib.gis.db.backend import parse_lookup, \
  10
+    ASGML, ASKML, DISTANCE, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION
10 11
 from django.contrib.gis.geos import GEOSGeometry
11 12
 
  13
+# Flag indicating whether the backend is Oracle.
  14
+oracle = SPATIAL_BACKEND == 'oracle'
  15
+
12 16
 class GeoQ(Q):
13 17
     "Geographical query encapsulation object."
14 18
 
@@ -28,11 +32,23 @@ def __init__(self, model=None):
28 32
 
29 33
         # For replacement fields in the SELECT.
30 34
         self._custom_select = {}
  35
+        self._ewkt = None
31 36
 
32 37
         # If GEOM_SELECT is defined in the backend, then it will be used
33 38
         # for the selection format of the geometry column.
34  
-        if GEOM_SELECT: self._geo_fmt = GEOM_SELECT
35  
-        else: self._geo_fmt = '%s'
  39
+        if GEOM_SELECT:
  40
+            #if oracle and hasattr(self, '_ewkt'):
  41
+            # Transformed geometries in Oracle use EWKT so that the SRID
  42
+            # on the transformed lazy geometries is set correctly).
  43
+            #print '-=' * 20
  44
+            #print self._ewkt, GEOM_SELECT
  45
+            #self._geo_fmt = "'SRID=%d;'||%s" % (self._ewkt, GEOM_SELECT)
  46
+            #self._geo_fmt = GEOM_SELECT
  47
+            #else:
  48
+            #print '-=' * 20
  49
+            self._geo_fmt = GEOM_SELECT
  50
+        else:
  51
+            self._geo_fmt = '%s'
36 52
 
37 53
     def _filter_or_exclude(self, mapper, *args, **kwargs):
38 54
         # mapper is a callable used to transform Q objects,
@@ -59,15 +75,23 @@ def _get_sql_clause(self, get_full_query=False):
59 75
         select = []
60 76
 
61 77
         # This is the only component of this routine that is customized for the 
62  
-        #  GeoQuerySet. Specifically, this allows operations to be done on fields 
63  
-        #  in the SELECT, overriding their values -- this is different from using 
64  
-        #  QuerySet.extra(select=foo) because extra() adds an  an _additional_ 
65  
-        #  field to be selected.  Used in returning transformed geometries, and
66  
-        #  handling the selection of native database geometry formats.
  78
+        # GeoQuerySet. Specifically, this allows operations to be done on fields 
  79
+        # in the SELECT, overriding their values -- this is different from using 
  80
+        # QuerySet.extra(select=foo) because extra() adds an  an _additional_ 
  81
+        # field to be selected.  Used in returning transformed geometries, and
  82
+        # handling the selection of native database geometry formats.
67 83
         for f in opts.fields:
68 84
             # Getting the selection format string.
69  
-            if hasattr(f, '_geom'): sel_fmt = self._geo_fmt
70  
-            else: sel_fmt = '%s'
  85
+            if hasattr(f, '_geom'):
  86
+                sel_fmt = self._geo_fmt
  87
+
  88
+                # If an SRID needs to specified other than what is in the field
  89
+                # (like when `transform` is called), make sure to explicitly set
  90
+                # the SRID by returning EWKT.
  91
+                if self._ewkt and oracle:
  92
+                    sel_fmt = "'SRID=%d;'||%s" % (self._ewkt, sel_fmt)
  93
+            else:
  94
+                sel_fmt = '%s'
71 95
                 
72 96
             # Getting the field selection substitution string
73 97
             if f.column in self._custom_select:
@@ -147,7 +171,7 @@ def _get_sql_clause(self, get_full_query=False):
147 171
             sql.append("ORDER BY " + ", ".join(order_by))
148 172
 
149 173
         # LIMIT and OFFSET clauses
150  
-        if SPATIAL_BACKEND != 'oracle':
  174
+        if not oracle:
151 175
             if self._limit is not None:
152 176
                 sql.append("%s " % connection.ops.limit_offset_sql(self._limit, self._offset))
153 177
             else:
@@ -206,6 +230,7 @@ def _get_sql_clause(self, get_full_query=False):
206 230
     def _clone(self, klass=None, **kwargs):
207 231
         c = super(GeoQuerySet, self)._clone(klass, **kwargs)
208 232
         c._custom_select = self._custom_select
  233
+        c._ewkt = self._ewkt
209 234
         return c
210 235
 
211 236
     #### Methods specific to the GeoQuerySet ####
@@ -227,7 +252,60 @@ def _geo_column(self, field_name):
227 252
         else:
228 253
             return False
229 254
 
230  
-    def gml(self, field_name, precision=8, version=2):
  255
+    def _get_geofield(self):
  256
+        "Returns the name of the first Geometry field encountered."
  257
+        for field in self.model._meta.fields:
  258
+            if isinstance(field, GeometryField): 
  259
+                return field.name
  260
+        raise Exception('No GeometryFields in the model.')
  261
+
  262
+    def distance(self, *args, **kwargs):
  263
+        """
  264
+        Returns the distance from the given geographic field name to the
  265
+        given geometry in a `distance` attribute on each element of the
  266
+        GeoQuerySet.
  267
+        """
  268
+        if not DISTANCE:
  269
+            raise ImproperlyConfigured('Distance() stored proecedure not available.')
  270
+
  271
+        # Getting the geometry field and GEOSGeometry object to base distance 
  272
+        # calculations from.
  273
+        nargs = len(args)
  274
+        if nargs == 1:
  275
+            field_name = self._get_geofield()
  276
+            geom = args[0]
  277
+        elif nargs == 2:
  278
+            field_name, geom = args
  279
+        else:
  280
+            raise ValueError('Maximum two arguments allowed for `distance` aggregate.')
  281
+
  282
+        # Getting the quoted column.
  283
+        field_col = self._geo_column(field_name)
  284
+        if not field_col:
  285
+            raise TypeError('Distance output only available on GeometryFields.')
  286
+
  287
+        # Getting the geographic field instance.
  288
+        geo_field = self.model._meta.get_field(field_name)
  289
+
  290
+        # Using the field's get_db_prep_lookup() to get any needed 
  291
+        # transformation SQL -- we pass in a 'dummy' `contains` lookup
  292
+        # type.
  293
+        geom_sql = geo_field.get_db_prep_lookup('contains', geom)
  294
+        if oracle:
  295
+            # The `tolerance` keyword may be used for Oracle.
  296
+            tolerance = kwargs.get('tolerance', 0.05)
  297
+
  298
+            # More legwork here because the OracleSpatialAdaptor doesn't do
  299
+            # quoting of the WKT.
  300
+            params = ["'%s'" % geom_sql.params[0]]
  301
+            params.extend(geom_sql.params[1:])
  302
+            gsql = geom_sql.where[0] % tuple(params)
  303
+            dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, field_col, gsql, tolerance)}
  304
+        else:
  305
+            dist_select = {'distance' : '%s(%s, %s)' % (DISTANCE, field_col, geom_sql)}
  306
+        return self.extra(select=dist_select)
  307
+
  308
+    def gml(self, field_name=None, precision=8, version=2):
231 309
         """
232 310
         Returns GML representation of the given field in a `gml` attribute
233 311
         on each element of the GeoQuerySet.
@@ -236,12 +314,16 @@ def gml(self, field_name, precision=8, version=2):
236 314
         if not ASGML:
237 315
             raise ImproperlyConfigured('AsGML() stored procedure not available.')
238 316
 
239  
-        # Is the given field name a geographic field?
  317
+        # If no field name explicitly given, get the first GeometryField from
  318
+        # the model.
  319
+        if not field_name:
  320
+            field_name = self._get_geofield()
  321
+
240 322
         field_col = self._geo_column(field_name)
241 323
         if not field_col:
242  
-            raise TypeError('GML output only available on GeometryFields')