Commits

Anonymous committed b2e5190

gis: Refactor of the `GeoQuerySet`; new features include:
(1) Creation of internal API that eases generation of `GeoQuerySet` methods.
(2) `GeoQuerySet.distance` now returns `Distance` objects instead of floats.
(3) Added the new `GeoQuerySet` methods: `area`, `centroid`, `difference`, `envelope`, `intersection`, `length`, `make_line`, `mem_size`, `num_geom`, `num_points`, `perimeter`, `point_on_surface`, `scale`, `svg`, `sym_difference`, `translate`, `union`.
(4) The `model_att` keyword may be used to customize the attribute that `GeoQuerySet` methods attach output to.
(5) Geographic distance lookups and `GeoQuerySet.distance` calls now use `ST_distance_sphere` by default (performance benefits far outweigh small loss in accuracy); `ST_distance_spheroid` may still be used by specifying an option.
(6) `GeoQuerySet` methods may now operate accross ForeignKey relations specified via the `field_name` keyword (but this does not work on Oracle).
(7) `Area` now has the same units of measure as `Distance`.

Backward Incompatibilites:
* The aggregate union method is now known as `unionagg`.
* The `field_name` keyword used for `GeoQuerySet` methods may no longer be specified via positional arguments.
* `Distance` objects returned instead of floats from `GeoQuerySet.distance`.
* `ST_Distance_sphere` used by default for geographic distance calculations.

Comments (0)

Files changed (27)

django/contrib/gis/db/backend/__init__.py

 
  Specifically, this module will import the correct routines and modules
  needed for GeoDjango to interface with the spatial database.
- 
- Some of the more important classes and routines from the spatial backend
- include:
-
- (1) `GeoBackEndField`, a base class needed for GeometryField.
- (2) `get_geo_where_clause`, a routine used by `GeoWhereNode`.
- (3) `GIS_TERMS`, a listing of all valid GeoDjango lookup types.
- (4) `SpatialBackend`, a container object for information specific to the
-     spatial backend.
 """
 from django.conf import settings
-from django.db.models.sql.query import QUERY_TERMS
 from django.contrib.gis.db.backend.util import gqn
 
-# These routines (needed by GeoManager), default to False.
-ASGML, ASKML, DISTANCE, DISTANCE_SPHEROID, EXTENT, TRANSFORM, UNION, VERSION = tuple(False for i in range(8))
-
-# Lookup types in which the rest of the parameters are not
-# needed to be substitute in the WHERE SQL (e.g., the 'relate'
-# operation on Oracle does not need the mask substituted back
-# into the query SQL.).
-LIMITED_WHERE = []
-
 # Retrieving the necessary settings from the backend.
 if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
-    from django.contrib.gis.db.backend.postgis.adaptor import \
-        PostGISAdaptor as GeoAdaptor
-    from django.contrib.gis.db.backend.postgis.field import \
-        PostGISField as GeoBackendField
-    from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
-    from django.contrib.gis.db.backend.postgis.query import \
-        get_geo_where_clause, POSTGIS_TERMS as GIS_TERMS, \
-        ASGML, ASKML, DISTANCE, DISTANCE_SPHEROID, DISTANCE_FUNCTIONS, \
-        EXTENT, GEOM_SELECT, TRANSFORM, UNION, \
-        MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
-    # PostGIS version info is needed to determine calling order of some
-    # stored procedures (e.g., AsGML()).
-    VERSION = (MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2)
-    SPATIAL_BACKEND = 'postgis'
+    from django.contrib.gis.db.backend.postgis import create_spatial_db, get_geo_where_clause, SpatialBackend
 elif settings.DATABASE_ENGINE == 'oracle':
-    from django.contrib.gis.db.backend.adaptor import WKTAdaptor as GeoAdaptor
-    from django.contrib.gis.db.backend.oracle.field import \
-        OracleSpatialField as GeoBackendField
-    from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
-    from django.contrib.gis.db.backend.oracle.query import \
-        get_geo_where_clause, ORACLE_SPATIAL_TERMS as GIS_TERMS, \
-        ASGML, DISTANCE, DISTANCE_FUNCTIONS, GEOM_SELECT, TRANSFORM, UNION
-    SPATIAL_BACKEND = 'oracle'
-    LIMITED_WHERE = ['relate']
+    from django.contrib.gis.db.backend.oracle import create_spatial_db, get_geo_where_clause, SpatialBackend
 elif settings.DATABASE_ENGINE == 'mysql':
-    from django.contrib.gis.db.backend.adaptor import WKTAdaptor as GeoAdaptor
-    from django.contrib.gis.db.backend.mysql.field import \
-        MySQLGeoField as GeoBackendField
-    from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
-    from django.contrib.gis.db.backend.mysql.query import \
-        get_geo_where_clause, MYSQL_GIS_TERMS as GIS_TERMS, GEOM_SELECT
-    DISTANCE_FUNCTIONS = {}
-    SPATIAL_BACKEND = 'mysql'
+    from django.contrib.gis.db.backend.mysql import create_spatial_db, get_geo_where_clause, SpatialBackend
 else:
     raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
-
-class SpatialBackend(object):
-    "A container for properties of the SpatialBackend."
-    # Stored procedure names used by the `GeoManager`.
-    as_kml = ASKML
-    as_gml = ASGML
-    distance = DISTANCE
-    distance_spheroid = DISTANCE_SPHEROID
-    extent = EXTENT
-    name = SPATIAL_BACKEND
-    select = GEOM_SELECT
-    transform = TRANSFORM
-    union = UNION
-    
-    # Version information, if defined.
-    version = VERSION
-    
-    # All valid GIS lookup terms, and distance functions.
-    gis_terms = GIS_TERMS
-    distance_functions = DISTANCE_FUNCTIONS
-    
-    # Lookup types where additional WHERE parameters are excluded.
-    limited_where = LIMITED_WHERE
-
-    # Shortcut booleans.
-    mysql = SPATIAL_BACKEND == 'mysql'
-    oracle = SPATIAL_BACKEND == 'oracle'
-    postgis = SPATIAL_BACKEND == 'postgis'
-
-    # Class for the backend field.
-    Field = GeoBackendField
-
-    # Adaptor class used for quoting GEOS geometries in the database.
-    Adaptor = GeoAdaptor

django/contrib/gis/db/backend/base.py

+"""
+ This module holds the base `SpatialBackend` object, which is
+ instantiated by each spatial backend with the features it has.
+"""
+# TODO: Create a `Geometry` protocol and allow user to use
+# different Geometry objects -- for now we just use GEOSGeometry.
+from django.contrib.gis.geos import GEOSGeometry, GEOSException
+
+class BaseSpatialBackend(object):
+    Geometry = GEOSGeometry
+    GeometryException = GEOSException
+
+    def __init__(self, **kwargs):
+        kwargs.setdefault('distance_functions', {})
+        kwargs.setdefault('limited_where', {})
+        for k, v in kwargs.iteritems(): setattr(self, k, v)
+ 
+    def __getattr__(self, name):
+        """
+        All attributes of the spatial backend return False by default.
+        """
+        try:
+            return self.__dict__[name]
+        except KeyError:
+            return False
+            
+        
+        
+    

django/contrib/gis/db/backend/mysql/__init__.py

+__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
 
+from django.contrib.gis.db.backend.base import BaseSpatialBackend
+from django.contrib.gis.db.backend.adaptor import WKTAdaptor
+from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
+from django.contrib.gis.db.backend.mysql.field import MySQLGeoField
+from django.contrib.gis.db.backend.mysql.query import *
+
+SpatialBackend = BaseSpatialBackend(name='mysql', mysql=True,
+                                    gis_terms=MYSQL_GIS_TERMS,
+                                    select=GEOM_SELECT,
+                                    Adaptor=WKTAdaptor,
+                                    Field=MySQLGeoField)

django/contrib/gis/db/backend/oracle/__init__.py

+__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
+
+from django.contrib.gis.db.backend.base import BaseSpatialBackend
+from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
+from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
+from django.contrib.gis.db.backend.oracle.field import OracleSpatialField
+from django.contrib.gis.db.backend.oracle.query import *
+
+SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True,
+                                    area=AREA,
+                                    centroid=CENTROID,
+                                    difference=DIFFERENCE,
+                                    distance=DISTANCE,
+                                    distance_functions=DISTANCE_FUNCTIONS,
+                                    gis_terms=ORACLE_SPATIAL_TERMS,
+                                    gml=ASGML,
+                                    intersection=INTERSECTION,
+                                    length=LENGTH,
+                                    limited_where = {'relate' : None},
+                                    num_geom=NUM_GEOM,
+                                    num_points=NUM_POINTS,
+                                    perimeter=LENGTH,
+                                    point_on_surface=POINT_ON_SURFACE,
+                                    select=GEOM_SELECT,
+                                    sym_difference=SYM_DIFFERENCE,
+                                    transform=TRANSFORM,
+                                    unionagg=UNIONAGG,
+                                    union=UNION,
+                                    Adaptor=OracleSpatialAdaptor,
+                                    Field=OracleSpatialField,
+                                    )

django/contrib/gis/db/backend/oracle/adaptor.py

+from cx_Oracle import CLOB
+from django.contrib.gis.db.backend.adaptor import WKTAdaptor
+
+class OracleSpatialAdaptor(WKTAdaptor):
+    input_size = CLOB

django/contrib/gis/db/backend/oracle/query.py

 qn = connection.ops.quote_name
 
 # The GML, distance, transform, and union procedures.
+AREA = 'SDO_GEOM.SDO_AREA'
 ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
+CENTROID = 'SDO_GEOM.SDO_CENTROID'
+DIFFERENCE = 'SDO_GEOM.SDO_DIFFERENCE'
 DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
+EXTENT = 'SDO_AGGR_MBR'
+INTERSECTION = 'SDO_GEOM.SDO_INTERSECTION'
+LENGTH = 'SDO_GEOM.SDO_LENGTH'
+NUM_GEOM = 'SDO_UTIL.GETNUMELEM'
+NUM_POINTS = 'SDO_UTIL.GETNUMVERTICES'
+POINT_ON_SURFACE = 'SDO_GEOM.SDO_POINTONSURFACE'
+SYM_DIFFERENCE = 'SDO_GEOM.SDO_XOR'
 TRANSFORM = 'SDO_CS.TRANSFORM'
-UNION = 'SDO_AGGR_UNION'
+UNION = 'SDO_GEOM.SDO_UNION'
+UNIONAGG = 'SDO_AGGR_UNION'
 
 # We want to get SDO Geometries as WKT because it is much easier to 
 # instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.  

django/contrib/gis/db/backend/postgis/__init__.py

+__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
+
+from django.contrib.gis.db.backend.base import BaseSpatialBackend
+from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
+from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
+from django.contrib.gis.db.backend.postgis.field import PostGISField
+from django.contrib.gis.db.backend.postgis.query import *
+
+SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True,
+                                    area=AREA,
+                                    centroid=CENTROID,
+                                    difference=DIFFERENCE,
+                                    distance=DISTANCE,
+                                    distance_functions=DISTANCE_FUNCTIONS,
+                                    distance_sphere=DISTANCE_SPHERE,
+                                    distance_spheroid=DISTANCE_SPHEROID,
+                                    envelope=ENVELOPE,
+                                    extent=EXTENT,
+                                    gis_terms=POSTGIS_TERMS,
+                                    gml=ASGML,
+                                    intersection=INTERSECTION,
+                                    kml=ASKML,
+                                    length=LENGTH,
+                                    length_spheroid=LENGTH_SPHEROID,
+                                    make_line=MAKE_LINE,
+                                    mem_size=MEM_SIZE,
+                                    num_geom=NUM_GEOM,
+                                    num_points=NUM_POINTS,
+                                    perimeter=PERIMETER,
+                                    point_on_surface=POINT_ON_SURFACE,
+                                    scale=SCALE,
+                                    select=GEOM_SELECT,
+                                    svg=ASSVG,
+                                    sym_difference=SYM_DIFFERENCE,
+                                    transform=TRANSFORM,
+                                    translate=TRANSLATE,
+                                    union=UNION,
+                                    unionagg=UNIONAGG,
+                                    version=(MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2),
+                                    Adaptor=PostGISAdaptor,
+                                    Field=PostGISField,
+                                    )

django/contrib/gis/db/backend/postgis/field.py

 from django.db import connection
 from django.db.models.fields import Field # Django base Field class
-from django.contrib.gis.geos import GEOSGeometry
 from django.contrib.gis.db.backend.util import gqn
 from django.contrib.gis.db.backend.postgis.query import TRANSFORM
 

django/contrib/gis/db/backend/postgis/query.py

 
 # Versions of PostGIS >= 1.2.2 changed their naming convention to be
 #  'SQL-MM-centric' to conform with the ISO standard. Practically, this
-#  means that 'ST_' is prefixes geometry function names.
+#  means that 'ST_' prefixes geometry function names.
 GEOM_FUNC_PREFIX = ''
 if MAJOR_VERSION >= 1:
     if (MINOR_VERSION1 > 2 or
 
     def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
 
-    # Custom selection not needed for PostGIS since GEOS geometries may be
+    # Custom selection not needed for PostGIS because GEOS geometries are
     # instantiated directly from the HEXEWKB returned by default.  If
     # WKT is needed for some reason in the future, this value may be changed,
-    # 'AsText(%s)'
+    # e.g,, 'AsText(%s)'.
     GEOM_SELECT = None
 
     # Functions used by the GeoManager & GeoQuerySet
+    AREA = get_func('Area')
     ASKML = get_func('AsKML')
     ASGML = get_func('AsGML')
+    ASSVG = get_func('AsSVG')
+    CENTROID = get_func('Centroid')
+    DIFFERENCE = get_func('Difference')
     DISTANCE = get_func('Distance')
+    DISTANCE_SPHERE = get_func('distance_sphere')
     DISTANCE_SPHEROID = get_func('distance_spheroid')
+    ENVELOPE = get_func('Envelope')
     EXTENT = get_func('extent')
     GEOM_FROM_TEXT = get_func('GeomFromText')
     GEOM_FROM_WKB = get_func('GeomFromWKB')
+    INTERSECTION = get_func('Intersection')
+    LENGTH = get_func('Length')
+    LENGTH_SPHEROID = get_func('length_spheroid')
+    MAKE_LINE = get_func('MakeLine')
+    MEM_SIZE = get_func('mem_size')
+    NUM_GEOM = get_func('NumGeometries')
+    NUM_POINTS = get_func('npoints')
+    PERIMETER = get_func('Perimeter')
+    POINT_ON_SURFACE = get_func('PointOnSurface')
+    SCALE = get_func('Scale')
+    SYM_DIFFERENCE = get_func('SymDifference')
     TRANSFORM = get_func('Transform')
+    TRANSLATE = get_func('Translate')
 
     # Special cases for union and KML methods.
     if MINOR_VERSION1 < 3:
-        UNION = 'GeomUnion'
+        UNIONAGG = 'GeomUnion'
+        UNION = 'Union'
     else:
+        UNIONAGG = 'ST_Union'
         UNION = 'ST_Union'
 
     if MINOR_VERSION1 == 1:
         super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s', 
                                               operator=operator, result='%%s')
 
-class PostGISSphereDistance(PostGISFunction):
-    "For PostGIS spherical distance operations."
+class PostGISSpheroidDistance(PostGISFunction):
+    "For PostGIS spherical distance operations (using the spheroid)."
     dist_func = 'distance_spheroid'
     def __init__(self, operator):
         # An extra parameter in `end_subst` is needed for the spheroid string.
-        super(PostGISSphereDistance, self).__init__(self.dist_func, 
-                                                    beg_subst='%s(%s, %%s, %%s', 
-                                                    end_subst=') %s %s',
+        super(PostGISSpheroidDistance, self).__init__(self.dist_func, 
+                                                      beg_subst='%s(%s, %%s, %%s', 
+                                                      end_subst=') %s %s',
+                                                      operator=operator, result='%%s')
+
+class PostGISSphereDistance(PostGISFunction):
+    "For PostGIS spherical distance operations."
+    dist_func = 'distance_sphere'
+    def __init__(self, operator):
+        super(PostGISSphereDistance, self).__init__(self.dist_func, end_subst=') %s %s',
                                                     operator=operator, result='%%s')
-
+                                                    
 class PostGISRelate(PostGISFunctionParam):
     "For PostGIS Relate(<geom>, <pattern>) calls."
     pattern_regex = re.compile(r'^[012TF\*]{9}$')
 dtypes = (Decimal, Distance, float, int, long)
 def get_dist_ops(operator):
     "Returns operations for both regular and spherical distances."
-    return (PostGISDistance(operator), PostGISSphereDistance(operator))
+    return (PostGISDistance(operator), PostGISSphereDistance(operator), PostGISSpheroidDistance(operator))
 DISTANCE_FUNCTIONS = {
     'distance_gt' : (get_dist_ops('>'), dtypes),
     'distance_gte' : (get_dist_ops('>='), dtypes),
 POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
 POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
 
+# For checking tuple parameters -- not very pretty but gets job done.
+def exactly_two(val): return val == 2
+def two_to_three(val): return val >= 2 and val <=3
+def num_params(lookup_type, val):
+    if lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': return two_to_three(val)
+    else: return exactly_two(val)
+
 #### The `get_geo_where_clause` function for PostGIS. ####
 def get_geo_where_clause(lookup_type, table_prefix, field, value):
     "Returns the SQL WHERE clause for use in PostGIS SQL construction."
             # Ensuring that a tuple _value_ was passed in from the user
             if not isinstance(value, (tuple, list)): 
                 raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
-            if len(value) != 2:
-                raise ValueError('2-element tuple required or `%s` lookup type.' % lookup_type)
+            # Number of valid tuple parameters depends on the lookup type.
+            nparams = len(value)
+            if not num_params(lookup_type, nparams):
+                raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
             
             # Ensuring the argument type matches what we expect.
             if not isinstance(value[1], arg_type):
                         raise TypeError('PostGIS spherical operations are only valid on PointFields.')
                     if value[0].geom_typeid != 0:
                         raise TypeError('PostGIS geometry distance parameter is required to be of type Point.')
-                    op = op[1]
+                    # Setting up the geodetic operation appropriately.
+                    if nparams == 3 and value[2] == 'spheroid': op = op[2]
+                    else: op = op[1]
                 else:
                     op = op[0]
         else:

django/contrib/gis/db/models/fields/__init__.py

 from django.contrib.gis.db.backend import SpatialBackend, gqn
 # GeometryProxy, GEOS, Distance, and oldforms imports.
 from django.contrib.gis.db.models.proxy import GeometryProxy
-from django.contrib.gis.geos import GEOSException, GEOSGeometry
 from django.contrib.gis.measure import Distance
 from django.contrib.gis.oldforms import WKTField
 
-# Attempting to get the spatial reference system.
-try:
-    from django.contrib.gis.models import SpatialRefSys
-except ImportError:
-    SpatialRefSys = None
+# The `get_srid_info` function gets SRID information from the spatial
+# reference system table w/o using the ORM.
+from django.contrib.gis.models import get_srid_info
 
 #TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
 class GeometryField(SpatialBackend.Field):
         # Setting the SRID and getting the units.  Unit information must be 
         # easily available in the field instance for distance queries.
         self._srid = srid
-        if SpatialRefSys:
-            # Getting the spatial reference WKT associated with the SRID from the
-            # `spatial_ref_sys` (or equivalent) spatial database table.
-            #
-            # The following doesn't work: SpatialRefSys.objects.get(srid=srid)
-            # Why?  `syncdb` fails to recognize installed geographic models when there's
-            # an ORM query instantiated within a model field.
-            cur = connection.cursor()
-            qn = connection.ops.quote_name
-            stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)'
-            stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table),
-                           'wkt_col' : qn(SpatialRefSys.wkt_col()),
-                           'srid_col' : qn('srid'),
-                           'srid' : srid,
-                           }
-            cur.execute(stmt)
-            srs_wkt = cur.fetchone()[0]
-            
-            # Getting metadata associated with the spatial reference system identifier.
-            # Specifically, getting the unit information and spheroid information 
-            # (both required for distance queries).
-            self._unit, self._unit_name = SpatialRefSys.get_units(srs_wkt)
-            self._spheroid = SpatialRefSys.get_spheroid(srs_wkt)
+        self._unit, self._unit_name, self._spheroid = get_srid_info(srid)
 
         # Setting the dimension of the geometry field.
         self._dim = dim
     ### Routines specific to GeometryField ###
     @property
     def geodetic(self):
+        """
+        Returns true if this field's SRID corresponds with a coordinate
+        system that uses non-projected units (e.g., latitude/longitude).
+        """
         return self._unit_name in self.geodetic_units
 
-    def get_distance(self, dist, lookup_type):
+    def get_distance(self, dist_val, lookup_type):
         """
         Returns a distance number in units of the field.  For example, if 
         `D(km=1)` was passed in and the units of the field were in meters,
         then 1000 would be returned.
         """
-        postgis = SpatialBackend.name == 'postgis'
+        # Getting the distance parameter and any options.
+        if len(dist_val) == 1: dist, option = dist_val[0], None
+        else: dist, option = dist_val
+
         if isinstance(dist, Distance):
             if self.geodetic:
                 # Won't allow Distance objects w/DWithin lookups on PostGIS.
-                if postgis and lookup_type == 'dwithin':
+                if SpatialBackend.postgis and lookup_type == 'dwithin':
                     raise TypeError('Only numeric values of degree units are allowed on geographic DWithin queries.')
                 # Spherical distance calculation parameter should be in meters.
                 dist_param = dist.m
         else:
             # Assuming the distance is in the units of the field.
             dist_param = dist
-
-        # Sphereical distance query; returning meters.
-        if postgis and self.geodetic and lookup_type != 'dwithin':
+       
+        if SpatialBackend.postgis and self.geodetic and lookup_type != 'dwithin' and option == 'spheroid':
+            # On PostGIS, by default `ST_distance_sphere` is used; but if the 
+            # accuracy of `ST_distance_spheroid` is needed than the spheroid 
+            # needs to be passed to the SQL stored procedure.
             return [gqn(self._spheroid), dist_param]
         else:
             return [dist_param]
 
         # When the input is not a GEOS geometry, attempt to construct one
         # from the given string input.
-        if isinstance(geom, GEOSGeometry):
+        if isinstance(geom, SpatialBackend.Geometry):
             pass
         elif isinstance(geom, basestring):
             try:
-                geom = GEOSGeometry(geom)
-            except GEOSException:
+                geom = SpatialBackend.Geometry(geom)
+            except SpatialBackend.GeometryException:
                 raise ValueError('Could not create geometry from lookup value: %s' % str(value))
         else:
             raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
     def contribute_to_class(self, cls, name):
         super(GeometryField, self).contribute_to_class(cls, name)
         
-        # Setup for lazy-instantiated GEOSGeometry object.
-        setattr(cls, self.attname, GeometryProxy(GEOSGeometry, self))
+        # Setup for lazy-instantiated Geometry object.
+        setattr(cls, self.attname, GeometryProxy(SpatialBackend.Geometry, self))
 
     def get_db_prep_lookup(self, lookup_type, value):
         """
             geom = self.get_geometry(value)
 
             # Getting the WHERE clause list and the associated params list. The params 
-            # list is populated with the Adaptor wrapping the GEOSGeometry for the 
+            # list is populated with the Adaptor wrapping the Geometry for the 
             # backend.  The WHERE clause list contains the placeholder for the adaptor
             # (e.g. any transformation SQL).
             where = [self.get_placeholder(geom)]
             if isinstance(value, (tuple, list)):
                 if lookup_type in SpatialBackend.distance_functions:
                     # Getting the distance parameter in the units of the field.
-                    where += self.get_distance(value[1], lookup_type)
+                    where += self.get_distance(value[1:], lookup_type)
                 elif lookup_type in SpatialBackend.limited_where:
                     pass
                 else:
 
     def get_db_prep_save(self, value):
         "Prepares the value for saving in the database."
-        if isinstance(value, GEOSGeometry):
+        if isinstance(value, SpatialBackend.Geometry):
             return SpatialBackend.Adaptor(value)
         elif value is None:
             return None
         else:
-            raise TypeError('Geometry Proxy should only return GEOSGeometry objects or None.')
+            raise TypeError('Geometry Proxy should only return Geometry objects or None.')
 
     def get_manipulator_field_objs(self):
-        "Using the WKTField (defined above) to be our manipulator."
+        "Using the WKTField (oldforms) to be our manipulator."
         return [WKTField]
 
 # The OpenGIS Geometry Type Fields

django/contrib/gis/db/models/manager.py

     def get_query_set(self):
         return GeoQuerySet(model=self.model)
 
+    def area(self, *args, **kwargs):
+        return self.get_query_set().area(*args, **kwargs)
+
+    def centroid(self, *args, **kwargs):
+        return self.get_query_set().centroid(*args, **kwargs)
+
+    def difference(self, *args, **kwargs):
+        return self.get_query_set().difference(*args, **kwargs)
+
     def distance(self, *args, **kwargs):
         return self.get_query_set().distance(*args, **kwargs)
 
+    def envelope(self, *args, **kwargs):
+        return self.get_query_set().envelope(*args, **kwargs)
+
     def extent(self, *args, **kwargs):
         return self.get_query_set().extent(*args, **kwargs)
 
     def gml(self, *args, **kwargs):
         return self.get_query_set().gml(*args, **kwargs)
 
+    def intersection(self, *args, **kwargs):
+        return self.get_query_set().intersection(*args, **kwargs)
+
     def kml(self, *args, **kwargs):
         return self.get_query_set().kml(*args, **kwargs)
 
+    def length(self, *args, **kwargs):
+        return self.get_query_set().length(*args, **kwargs)
+
+    def make_line(self, *args, **kwargs):
+        return self.get_query_set().make_line(*args, **kwargs)
+    
+    def mem_size(self, *args, **kwargs):
+        return self.get_query_set().mem_size(*args, **kwargs)
+
+    def num_geom(self, *args, **kwargs):
+        return self.get_query_set().num_geom(*args, **kwargs)
+
+    def num_points(self, *args, **kwargs):
+        return self.get_query_set().num_points(*args, **kwargs)
+
+    def perimeter(self, *args, **kwargs):
+        return self.get_query_set().perimeter(*args, **kwargs)
+
+    def point_on_surface(self, *args, **kwargs):
+        return self.get_query_set().point_on_surface(*args, **kwargs)
+
+    def scale(self, *args, **kwargs):
+        return self.get_query_set().scale(*args, **kwargs)
+
+    def svg(self, *args, **kwargs):
+        return self.get_query_set().svg(*args, **kwargs)
+
+    def sym_difference(self, *args, **kwargs):
+        return self.get_query_set().sym_difference(*args, **kwargs)
+
     def transform(self, *args, **kwargs):
         return self.get_query_set().transform(*args, **kwargs)
 
+    def translate(self, *args, **kwargs):
+        return self.get_query_set().translate(*args, **kwargs)
+
     def union(self, *args, **kwargs):
         return self.get_query_set().union(*args, **kwargs)
+
+    def unionagg(self, *args, **kwargs):
+        return self.get_query_set().unionagg(*args, **kwargs)

django/contrib/gis/db/models/proxy.py

 """
  The GeometryProxy object, allows for lazy-geometries.  The proxy uses
-  Python descriptors for instantiating and setting GEOS Geometry objects
-  corresponding to geographic model fields.
+ Python descriptors for instantiating and setting Geometry objects
+ corresponding to geographic model fields.
 
  Thanks to Robert Coup for providing this functionality (see #4322).
 """
         elif (geom_value is None) or (geom_value==''):
             geom = None
         else: 
-            # Otherwise, a GEOSGeometry object is built using the field's contents,
-            #  and the model's corresponding attribute is set.
+            # Otherwise, a Geometry object is built using the field's contents,
+            # and the model's corresponding attribute is set.
             geom = self._klass(geom_value)
             setattr(obj, self._field.attname, geom) 
         return geom 

django/contrib/gis/db/models/query.py

-from itertools import izip
 from django.core.exceptions import ImproperlyConfigured
 from django.db import connection
 from django.db.models.query import sql, QuerySet, Q
 
 from django.contrib.gis.db.backend import SpatialBackend
 from django.contrib.gis.db.models.fields import GeometryField, PointField
-from django.contrib.gis.db.models.sql import GeoQuery, GeoWhereNode
-from django.contrib.gis.geos import GEOSGeometry, Point
+from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode
+from django.contrib.gis.measure import Area, Distance
+from django.contrib.gis.models import get_srid_info
 qn = connection.ops.quote_name
 
 # For backwards-compatibility; Q object should work just fine
         super(GeoQuerySet, self).__init__(model=model, query=query)
         self.query = query or GeoQuery(self.model, connection)
 
-    def distance(self, *args, **kwargs):
+    def area(self, tolerance=0.05, **kwargs):
+        """
+        Returns the area of the geographic field in an `area` attribute on 
+        each element of this GeoQuerySet.
+        """
+        # Peforming setup here rather than in `_spatial_attribute` so that
+        # we can get the units for `AreaField`.
+        procedure_args, geo_field = self._spatial_setup('area', field_name=kwargs.get('field_name', None))
+        s = {'procedure_args' : procedure_args,
+             'geo_field' : geo_field,
+             'setup' : False,
+             }
+        if SpatialBackend.oracle:
+            s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
+            s['procedure_args']['tolerance'] = tolerance
+            s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters.
+        elif SpatialBackend.postgis:
+            if not geo_field.geodetic:
+                # Getting the area units of the geographic field.
+                s['select_field'] = AreaField(Area.unit_attname(geo_field._unit_name))
+            else:
+                # TODO: Do we want to support raw number areas for geodetic fields?
+                raise Exception('Area on geodetic coordinate systems not supported.')
+        return self._spatial_attribute('area', s, **kwargs)
+
+    def centroid(self, **kwargs):
+        """
+        Returns the centroid of the geographic field in a `centroid`
+        attribute on each element of this GeoQuerySet.
+        """
+        return self._geom_attribute('centroid', **kwargs)
+
+    def difference(self, geom, **kwargs):
+        """
+        Returns the spatial difference of the geographic field in a `difference`
+        attribute on each element of this GeoQuerySet.
+        """
+        return self._geomset_attribute('difference', geom, **kwargs)
+
+    def distance(self, geom, **kwargs):
         """
         Returns the distance from the given geographic field name to the
         given geometry in a `distance` attribute on each element of the
         GeoQuerySet.
+
+        Keyword Arguments:
+         `spheroid`  => If the geometry field is geodetic and PostGIS is
+                        the spatial database, then the more accurate 
+                        spheroid calculation will be used instead of the
+                        quicker sphere calculation.
+                        
+         `tolerance` => Used only for Oracle. The tolerance is 
+                        in meters -- a default of 5 centimeters (0.05) 
+                        is used.
         """
-        DISTANCE = SpatialBackend.distance
-        if not DISTANCE:
-            raise ImproperlyConfigured('Distance() stored proecedure not available.')
+        return self._distance_attribute('distance', geom, **kwargs)
 
-        # Getting the geometry field and GEOSGeometry object to base distance
-        # calculations from.
-        nargs = len(args)
-        if nargs == 1:
-            field_name = None
-            geom = args[0]
-        elif nargs == 2:
-            field_name, geom = args
-        else:
-            raise ValueError('Maximum two arguments allowed for `distance` aggregate.')
+    def envelope(self, **kwargs):
+        """
+        Returns a Geometry representing the bounding box of the 
+        Geometry field in an `envelope` attribute on each element of
+        the GeoQuerySet. 
+        """
+        return self._geom_attribute('envelope', **kwargs)
 
-        # Getting the GeometryField and quoted column.
-        geo_field = self.query._geo_field(field_name)
-        if not geo_field:
-            raise TypeError('Distance output only available on GeometryFields.')
-        geo_col = self.query._field_column(geo_field)
-
-        # Using the field's get_db_prep_lookup() to get any needed
-        # transformation SQL -- we pass in a 'dummy' `contains`
-        # `distance_lte` lookup type.
-        where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0))
-        if SpatialBackend.oracle:
-            # The `tolerance` keyword may be used for Oracle; the tolerance is 
-            # in meters -- a default of 5 centimeters is used.
-            tolerance = kwargs.get('tolerance', 0.05)
-            dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, geo_col, where[0], tolerance)}
-        else:
-            if len(where) == 3:
-                # Spherical distance calculation was requested (b/c spheroid 
-                # parameter was attached) However, the PostGIS ST_distance_spheroid() 
-                # procedure may only do queries from point columns to point geometries
-                # some error checking is required.
-                if not isinstance(geo_field, PointField): 
-                    raise TypeError('Spherical distance calculation only supported on PointFields.')
-                if not isinstance(GEOSGeometry(buffer(params[0].wkb)), Point):
-                    raise TypeError('Spherical distance calculation only supported with Point Geometry parameters')
-
-                # Call to distance_spheroid() requires the spheroid as well.
-                dist_sql = '%s(%s, %s, %s)' % (SpatialBackend.distance_spheroid, geo_col, where[0], where[1])
-            else:
-                dist_sql = '%s(%s, %s)' % (DISTANCE, geo_col, where[0])
-            dist_select = {'distance' : dist_sql}
-        return self.extra(select=dist_select, select_params=params)
-
-    def extent(self, field_name=None):
+    def extent(self, **kwargs):
         """
         Returns the extent (aggregate) of the features in the GeoQuerySet.  The
         extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax).
         """
-        EXTENT = SpatialBackend.extent
-        if not EXTENT:
-            raise ImproperlyConfigured('Extent stored procedure not available.')
+        convert_extent = None
+        if SpatialBackend.postgis:
+            def convert_extent(box, geo_field):
+                # TODO: Parsing of BOX3D, Oracle support (patches welcome!)
+                # Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)"; 
+                # parsing out and returning as a 4-tuple.
+                ll, ur = box[4:-1].split(',')
+                xmin, ymin = map(float, ll.split())
+                xmax, ymax = map(float, ur.split())
+                return (xmin, ymin, xmax, ymax)
+        elif SpatialBackend.oracle:
+            def convert_extent(wkt, geo_field):
+                raise NotImplementedError
+        return self._spatial_aggregate('extent', convert_func=convert_extent, **kwargs)
 
-        # Getting the GeometryField and quoted column.
-        geo_field = self.query._geo_field(field_name)
-        if not geo_field:
-            raise TypeError('Extent information only available on GeometryFields.')
-        geo_col = self.query._field_column(geo_field)
-
-        # Constructing the query that will select the extent.
-        extent_sql = '%s(%s)' % (EXTENT, geo_col)
-
-        self.query.select = [GeomSQL(extent_sql)]
-        self.query.select_fields = [None]
-        try:
-            esql, params = self.query.as_sql()
-        except sql.datastructures.EmptyResultSet:
-            return None        
-
-        # Getting a cursor, executing the query, and extracting the returned
-        # value from the extent function.
-        cursor = connection.cursor()
-        cursor.execute(esql, params)
-        box = cursor.fetchone()[0]
-
-        if box: 
-            # TODO: Parsing of BOX3D, Oracle support (patches welcome!)
-            #  Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)"; 
-            #  parsing out and returning as a 4-tuple.
-            ll, ur = box[4:-1].split(',')
-            xmin, ymin = map(float, ll.split())
-            xmax, ymax = map(float, ur.split())
-            return (xmin, ymin, xmax, ymax)
-        else: 
-            return None
-
-    def gml(self, field_name=None, precision=8, version=2):
+    def gml(self, precision=8, version=2, **kwargs):
         """
         Returns GML representation of the given field in a `gml` attribute
         on each element of the GeoQuerySet.
         """
-        # Is GML output supported?
-        ASGML = SpatialBackend.as_gml
-        if not ASGML:
-            raise ImproperlyConfigured('AsGML() stored procedure not available.')
- 
-        # Getting the GeometryField and quoted column.
-        geo_field = self.query._geo_field(field_name)
-        if not geo_field:
-            raise TypeError('GML output only available on GeometryFields.')
-        geo_col = self.query._field_column(geo_field)
-
-        if SpatialBackend.oracle:
-            gml_select = {'gml':'%s(%s)' % (ASGML, geo_col)}
-        elif SpatialBackend.postgis:
+        s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}}
+        if SpatialBackend.postgis:
             # PostGIS AsGML() aggregate function parameter order depends on the 
             # version -- uggh.
             major, minor1, minor2 = SpatialBackend.version
             if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)):
-                gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, version, geo_col, precision)}
+                procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s'
             else:
-                gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, geo_col, precision, version)}
+                procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s'
+            s['procedure_args'] = {'precision' : precision, 'version' : version}
 
-        # Adding GML function call to SELECT part of the SQL.
-        return self.extra(select=gml_select)
+        return self._spatial_attribute('gml', s, **kwargs)
 
-    def kml(self, field_name=None, precision=8):
+    def intersection(self, geom, **kwargs):
         """
-        Returns KML representation of the given field name in a `kml`
-        attribute on each element of the GeoQuerySet.
+        Returns the spatial intersection of the Geometry field in
+        an `intersection` attribute on each element of this
+        GeoQuerySet.
         """
-        # Is KML output supported?
-        ASKML = SpatialBackend.as_kml
-        if not ASKML:
-            raise ImproperlyConfigured('AsKML() stored procedure not available.')
+        return self._geomset_attribute('intersection', geom, **kwargs)
 
-        # Getting the GeometryField and quoted column.
-        geo_field = self.query._geo_field(field_name)
-        if not geo_field:
-            raise TypeError('KML output only available on GeometryFields.')
+    def kml(self, **kwargs):
+        """
+        Returns KML representation of the geometry field in a `kml`
+        attribute on each element of this GeoQuerySet.
+        """
+        s = {'desc' : 'KML',
+             'procedure_fmt' : '%(geo_col)s,%(precision)s',
+             'procedure_args' : {'precision' : kwargs.pop('precision', 8)},
+             }
+        return self._spatial_attribute('kml', s, **kwargs)
 
-        geo_col = self.query._field_column(geo_field)
+    def length(self, **kwargs):
+        """
+        Returns the length of the geometry field as a `Distance` object
+        stored in a `length` attribute on each element of this GeoQuerySet.
+        """
+        return self._distance_attribute('length', None, **kwargs)
 
-        # Adding the AsKML function call to SELECT part of the SQL.
-        return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, geo_col, precision)})
+    def make_line(self, **kwargs):
+        """
+        Creates a linestring from all of the PointField geometries in the
+        this GeoQuerySet and returns it.  This is a spatial aggregate
+        method, and thus returns a geometry rather than a GeoQuerySet.
+        """
+        kwargs['geo_field_type'] = PointField
+        kwargs['agg_field'] = GeometryField
+        return self._spatial_aggregate('make_line', **kwargs)
 
-    def transform(self, field_name=None, srid=4326):
+    def mem_size(self, **kwargs):
+        """
+        Returns the memory size (number of bytes) that the geometry field takes
+        in a `mem_size` attribute  on each element of this GeoQuerySet.
+        """
+        return self._spatial_attribute('mem_size', {}, **kwargs)
+
+    def num_geom(self, **kwargs):
+        """
+        Returns the number of geometries if the field is a
+        GeometryCollection or Multi* Field in a `num_geom`
+        attribute on each element of this GeoQuerySet; otherwise
+        the sets with None.
+        """
+        return self._spatial_attribute('num_geom', {}, **kwargs)
+
+    def num_points(self, **kwargs):
+        """
+        Returns the number of points in the first linestring in the 
+        Geometry field in a `num_points` attribute on each element of
+        this GeoQuerySet; otherwise sets with None.
+        """
+        return self._spatial_attribute('num_points', {}, **kwargs)
+
+    def perimeter(self, **kwargs):
+        """
+        Returns the perimeter of the geometry field as a `Distance` object
+        stored in a `perimeter` attribute on each element of this GeoQuerySet.
+        """
+        return self._distance_attribute('perimeter', None, **kwargs)
+
+    def point_on_surface(self, **kwargs):
+        """
+        Returns a Point geometry guaranteed to lie on the surface of the
+        Geometry field in a `point_on_surface` attribute on each element
+        of this GeoQuerySet; otherwise sets with None.
+        """
+        return self._geom_attribute('point_on_surface', **kwargs)
+
+    def scale(self, x, y, z=0.0, **kwargs):
+        """
+        Scales the geometry to a new size by multiplying the ordinates
+        with the given x,y,z scale factors.
+        """
+        s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
+             'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
+             'select_field' : GeomField(),
+             }
+        return self._spatial_attribute('scale', s, **kwargs)
+
+    def svg(self, **kwargs):
+        """
+        Returns SVG representation of the geographic field in a `svg`
+        attribute on each element of this GeoQuerySet.
+        """
+        s = {'desc' : 'SVG',
+             'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s',
+             'procedure_args' : {'rel' : int(kwargs.pop('relative', 0)),
+                                 'precision' : kwargs.pop('precision', 8)},
+             }
+        return self._spatial_attribute('svg', s, **kwargs)
+
+    def sym_difference(self, geom, **kwargs):
+        """
+        Returns the symmetric difference of the geographic field in a 
+        `sym_difference` attribute on each element of this GeoQuerySet.
+        """
+        return self._geomset_attribute('sym_difference', geom, **kwargs)
+
+    def translate(self, x, y, z=0.0, **kwargs):
+        """
+        Translates the geometry to a new location using the given numeric
+        parameters as offsets.
+        """
+        s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
+             'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
+             'select_field' : GeomField(),
+             }
+        return self._spatial_attribute('translate', s, **kwargs)
+
+    def transform(self, srid=4326, **kwargs):
         """
         Transforms the given geometry field to the given SRID.  If no SRID is
         provided, the transformation will default to using 4326 (WGS84).
         """
-        # Getting the geographic field.
-        TRANSFORM = SpatialBackend.transform
-        if not TRANSFORM:
-            raise ImproperlyConfigured('Transform stored procedure not available.')
+        if not isinstance(srid, (int, long)):
+            raise TypeError('An integer SRID must be provided.')
+        field_name = kwargs.get('field_name', None)
+        tmp, geo_field = self._spatial_setup('transform', field_name=field_name)
 
-        # `field_name` is first for backwards compatibility; but we want to
-        # be able to take integer srid as first parameter.
-        if isinstance(field_name, (int, long)):
-            srid = field_name
-            field_name = None
-
-        # Getting the GeometryField and quoted column.
-        geo_field = self.query._geo_field(field_name)
-        if not geo_field:
-            raise TypeError('%s() only available for GeometryFields' % TRANSFORM)
-        
-        # Getting the selection SQL for the given geograph
+        # Getting the selection SQL for the given geographic field.
         field_col = self._geocol_select(geo_field, field_name)
 
         # Why cascading substitutions? Because spatial backends like
         
         # Setting the key for the field's column with the custom SELECT SQL to
         # override the geometry column returned from the database.
-        if SpatialBackend.oracle:
-            custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid)
-            self.query.ewkt = srid
-        else:
-            custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid)
+        custom_sel = '%s(%s, %s)' % (SpatialBackend.transform, geo_col, srid)
+        # TODO: Should we have this as an alias?
+        # custom_sel = '(%s(%s, %s)) AS %s' % (SpatialBackend.transform, geo_col, srid, qn(geo_field.name))
+        self.query.transformed_srid = srid # So other GeoQuerySet methods
         self.query.custom_select[geo_field] = custom_sel
         return self._clone()
 
-    def union(self, field_name=None, tolerance=0.0005):
+    def union(self, geom, **kwargs):
+        """
+        Returns the union of the geographic field with the given
+        Geometry in a `union` attribute on each element of this GeoQuerySet.
+        """
+        return self._geomset_attribute('union', geom, **kwargs)
+
+    def unionagg(self, **kwargs):
         """
         Performs an aggregate union on the given geometry field.  Returns
         None if the GeoQuerySet is empty.  The `tolerance` keyword is for
         Oracle backends only.
         """
-        # Making sure backend supports the Union stored procedure
-        UNION = SpatialBackend.union
-        if not UNION:
-            raise ImproperlyConfigured('Union stored procedure not available.')
+        kwargs['agg_field'] = GeometryField
+        return self._spatial_aggregate('unionagg', **kwargs)
 
-        # Getting the GeometryField and quoted column.
+    ### Private API -- Abstracted DRY routines. ###
+    def _spatial_setup(self, att, aggregate=False, desc=None, field_name=None, geo_field_type=None):
+        """
+        Performs set up for executing the spatial function.
+        """
+        # Does the spatial backend support this?
+        func = getattr(SpatialBackend, att, False)
+        if desc is None: desc = att
+        if not func: raise ImproperlyConfigured('%s stored procedure not available.' % desc)
+
+        # Initializing the procedure arguments. 
+        procedure_args = {'function' : func}
+        
+        # Is there a geographic field in the model to perform this 
+        # operation on?
         geo_field = self.query._geo_field(field_name)
         if not geo_field:
-            raise TypeError('Aggregate Union only available on GeometryFields.')
-        geo_col = self.query._field_column(geo_field)
+            raise TypeError('%s output only available on GeometryFields.' % func)
 
-        # Replacing the select with a call to the ST_Union stored procedure
-        #  on the geographic field column.
+        # If the `geo_field_type` keyword was used, then enforce that 
+        # type limitation.
+        if not geo_field_type is None and not isinstance(geo_field, geo_field_type): 
+            raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__)) 
+
+        # Setting the procedure args.
+        procedure_args['geo_col'] = self._geocol_select(geo_field, field_name, aggregate)
+
+        return procedure_args, geo_field
+
+    def _spatial_aggregate(self, att, field_name=None, 
+                           agg_field=None, convert_func=None, 
+                           geo_field_type=None, tolerance=0.0005):
+        """
+        DRY routine for calling aggregate spatial stored procedures and
+        returning their result to the caller of the function.
+        """
+        # Constructing the setup keyword arguments.
+        setup_kwargs = {'aggregate' : True,
+                        'field_name' : field_name,
+                        'geo_field_type' : geo_field_type,
+                        }
+        procedure_args, geo_field = self._spatial_setup(att, **setup_kwargs)
+        
         if SpatialBackend.oracle:
-            union_sql = '%s' % SpatialBackend.select
-            union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, geo_col, tolerance))
+            procedure_args['tolerance'] = tolerance
+            # Adding in selection SQL for Oracle geometry columns.
+            if agg_field is GeometryField: 
+                agg_sql = '%s' % SpatialBackend.select
+            else: 
+                agg_sql = '%s'
+            agg_sql =  agg_sql % ('%(function)s(SDOAGGRTYPE(%(geo_col)s,%(tolerance)s))' % procedure_args)
         else:
-            union_sql = '%s(%s)' % (UNION, geo_col)
+            agg_sql = '%(function)s(%(geo_col)s)' % procedure_args
 
-        # Only want the union SQL to be selected.
-        self.query.select = [GeomSQL(union_sql)]
-        self.query.select_fields = [GeometryField]
+        # Wrapping our selection SQL in `GeomSQL` to bypass quoting, and
+        # specifying the type of the aggregate field.
+        self.query.select = [GeomSQL(agg_sql)]
+        self.query.select_fields = [agg_field]
+
         try:
-            usql, params = self.query.as_sql()
+            # `asql` => not overriding `sql` module.
+            asql, params = self.query.as_sql()
         except sql.datastructures.EmptyResultSet:
-            return None
+            return None   
 
-        # Getting a cursor, executing the query.
+        # Getting a cursor, executing the query, and extracting the returned
+        # value from the aggregate function.
         cursor = connection.cursor()
-        cursor.execute(usql, params)
+        cursor.execute(asql, params)
+        result = cursor.fetchone()[0]
+        
+        # If the `agg_field` is specified as a GeometryField, then autmatically
+        # set up the conversion function.
+        if agg_field is GeometryField and not callable(convert_func):
+            if SpatialBackend.postgis:
+                def convert_geom(hex, geo_field):
+                    if hex: return SpatialBackend.Geometry(hex)
+                    else: return None
+            elif SpatialBackend.oracle:
+                def convert_geom(clob, geo_field):
+                    if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid)
+                    else: return None
+            convert_func = convert_geom
+
+        # Returning the callback function evaluated on the result culled
+        # from the executed cursor.
+        if callable(convert_func):
+            return convert_func(result, geo_field)
+        else:
+            return result
+
+    def _spatial_attribute(self, att, settings, field_name=None, model_att=None):
+        """
+        DRY routine for calling a spatial stored procedure on a geometry column
+        and attaching its output as an attribute of the model.
+
+        Arguments:
+         att:
+          The name of the spatial attribute that holds the spatial
+          SQL function to call.
+
+         settings:
+          Dictonary of internal settings to customize for the spatial procedure. 
+
+        Public Keyword Arguments:
+
+         field_name:
+          The name of the geographic field to call the spatial
+          function on.  May also be a lookup to a geometry field
+          as part of a foreign key relation.
+
+         model_att:
+          The name of the model attribute to attach the output of
+          the spatial function to.
+        """
+        # Default settings.
+        settings.setdefault('desc', None)
+        settings.setdefault('geom_args', ())
+        settings.setdefault('geom_field', None)
+        settings.setdefault('procedure_args', {})
+        settings.setdefault('procedure_fmt', '%(geo_col)s')
+        settings.setdefault('select_params', [])
+
+        # Performing setup for the spatial column, unless told not to.
+        if settings.get('setup', True):
+            default_args, geo_field = self._spatial_setup(att, desc=settings['desc'], field_name=field_name)
+            for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v)
+        else:
+            geo_field = settings['geo_field']
+            
+        # The attribute to attach to the model.
+        if not isinstance(model_att, basestring): model_att = att
+
+        # Special handling for any argument that is a geometry.
+        for name in settings['geom_args']:
+            # Using the field's get_db_prep_lookup() to get any needed
+            # transformation SQL -- we pass in a 'dummy' `contains` lookup.
+            where, params = geo_field.get_db_prep_lookup('contains', settings['procedure_args'][name])
+            # Replacing the procedure format with that of any needed 
+            # transformation SQL.
+            old_fmt = '%%(%s)s' % name
+            new_fmt = where[0] % '%%s'
+            settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt)
+            settings['select_params'].extend(params)
+
+        # Getting the format for the stored procedure.
+        fmt = '%%(function)s(%s)' % settings['procedure_fmt']
+        
+        # If the result of this function needs to be converted.
+        if settings.get('select_field', False):
+            sel_fld = settings['select_field']
+            if isinstance(sel_fld, GeomField) and SpatialBackend.select:
+                self.query.custom_select[model_att] = SpatialBackend.select
+            self.query.extra_select_fields[model_att] = sel_fld
+
+        # Finally, setting the extra selection attribute with 
+        # the format string expanded with the stored procedure
+        # arguments.
+        return self.extra(select={model_att : fmt % settings['procedure_args']}, 
+                          select_params=settings['select_params'])
+
+    def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, **kwargs):
+        """
+        DRY routine for GeoQuerySet distance attribute routines.
+        """
+        # Setting up the distance procedure arguments.
+        procedure_args, geo_field = self._spatial_setup(func, field_name=kwargs.get('field_name', None))
+
+        # If geodetic defaulting distance attribute to meters (Oracle and
+        # PostGIS spherical distances return meters).  Otherwise, use the
+        # units of the geometry field.
+        if geo_field.geodetic:
+            dist_att = 'm'
+        else:
+            dist_att = Distance.unit_attname(geo_field._unit_name)
+
+        # Shortcut booleans for what distance function we're using.
+        distance = func == 'distance'
+        length = func == 'length'
+        perimeter = func == 'perimeter'
+        if not (distance or length or perimeter): 
+            raise ValueError('Unknown distance function: %s' % func)
+
+        # The field's get_db_prep_lookup() is used to get any 
+        # extra distance parameters.  Here we set up the
+        # parameters that will be passed in to field's function.
+        lookup_params = [geom or 'POINT (0 0)', 0]
+
+        # If the spheroid calculation is desired, either by the `spheroid`
+        # keyword or wehn calculating the length of geodetic field, make
+        # sure the 'spheroid' distance setting string is passed in so we
+        # get the correct spatial stored procedure.            
+        if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length): 
+            lookup_params.append('spheroid') 
+        where, params = geo_field.get_db_prep_lookup('distance_lte', lookup_params)
+
+        # The `geom_args` flag is set to true if a geometry parameter was 
+        # passed in.
+        geom_args = bool(geom)
+
         if SpatialBackend.oracle:
-            # On Oracle have to read out WKT from CLOB first.
-            clob = cursor.fetchone()[0]
-            if clob: u = clob.read()
-            else: u = None
+            if distance:
+                procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s'
+            elif length or perimeter:
+                procedure_fmt = '%(geo_col)s,%(tolerance)s'
+            procedure_args['tolerance'] = tolerance
         else:
-            u = cursor.fetchone()[0]
-        
-        if u: return GEOSGeometry(u)
-        else: return None
+            # Getting whether this field is in units of degrees since the field may have
+            # been transformed via the `transform` GeoQuerySet method.
+            if self.query.transformed_srid:
+                u, unit_name, s = get_srid_info(self.query.transformed_srid)
+                geodetic = unit_name in geo_field.geodetic_units
+            else:
+                geodetic = geo_field.geodetic
+            
+            if distance:
+                if self.query.transformed_srid:
+                    # Setting the `geom_args` flag to false because we want to handle
+                    # transformation SQL here, rather than the way done by default
+                    # (which will transform to the original SRID of the field rather
+                    #  than to what was transformed to).
+                    geom_args = False
+                    procedure_fmt = '%s(%%(geo_col)s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
+                    if geom.srid is None or geom.srid == self.query.transformed_srid:
+                        # If the geom parameter srid is None, it is assumed the coordinates 
+                        # are in the transformed units.  A placeholder is used for the
+                        # geometry parameter.
+                        procedure_fmt += ', %%s'
+                    else:
+                        # We need to transform the geom to the srid specified in `transform()`,
+                        # so wrapping the geometry placeholder in transformation SQL.
+                        procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
+                else:
+                    # `transform()` was not used on this GeoQuerySet.
+                    procedure_fmt  = '%(geo_col)s,%(geom)s'
 
-    # Private API utilities, subject to change.
-    def _geocol_select(self, geo_field, field_name):
+                if geodetic:
+                    # Spherical distance calculation is needed (because the geographic
+                    # field is geodetic). However, the PostGIS ST_distance_sphere/spheroid() 
+                    # procedures may only do queries from point columns to point geometries
+                    # some error checking is required.
+                    if not isinstance(geo_field, PointField): 
+                        raise TypeError('Spherical distance calculation only supported on PointFields.')
+                    if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point':
+                        raise TypeError('Spherical distance calculation only supported with Point Geometry parameters')
+                    # The `function` procedure argument needs to be set differently for
+                    # geodetic distance calculations.
+                    if spheroid:
+                        # Call to distance_spheroid() requires spheroid param as well.
+                        procedure_fmt += ',%(spheroid)s'
+                        procedure_args.update({'function' : SpatialBackend.distance_spheroid, 'spheroid' : where[1]})
+                    else:
+                        procedure_args.update({'function' : SpatialBackend.distance_sphere})
+            elif length or perimeter:
+                procedure_fmt = '%(geo_col)s'
+                if geodetic and length:
+                    # There's no `length_sphere`
+                    procedure_fmt += ',%(spheroid)s'
+                    procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]})
+
+        # Setting up the settings for `_spatial_attribute`.
+        s = {'select_field' : DistanceField(dist_att),
+             'setup' : False, 
+             'geo_field' : geo_field,
+             'procedure_args' : procedure_args,
+             'procedure_fmt' : procedure_fmt,
+             }
+        if geom_args: 
+            s['geom_args'] = ('geom',)
+            s['procedure_args']['geom'] = geom
+        elif geom:
+            # The geometry is passed in as a parameter because we handled
+            # transformation conditions in this routine.
+            s['select_params'] = [SpatialBackend.Adaptor(geom)]
+        return self._spatial_attribute(func, s, **kwargs)
+
+    def _geom_attribute(self, func, tolerance=0.05, **kwargs):
+        """
+        DRY routine for setting up a GeoQuerySet method that attaches a
+        Geometry attribute (e.g., `centroid`, `point_on_surface`).
+        """
+        s = {'select_field' : GeomField(),}
+        if SpatialBackend.oracle:
+            s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
+            s['procedure_args'] = {'tolerance' : tolerance}
+        return self._spatial_attribute(func, s, **kwargs)
+                     
+    def _geomset_attribute(self, func, geom, tolerance=0.05, **kwargs):
+        """
+        DRY routine for setting up a GeoQuerySet method that attaches a
+        Geometry attribute and takes a Geoemtry parameter.  This is used
+        for geometry set-like operations (e.g., intersection, difference, 
+        union, sym_difference).
+        """
+        s = {'geom_args' : ('geom',),
+             'select_field' : GeomField(),
+             'procedure_fmt' : '%(geo_col)s,%(geom)s',
+             'procedure_args' : {'geom' : geom},
+            }
+        if SpatialBackend.oracle:
+            s['procedure_fmt'] += ',%(tolerance)s'
+            s['procedure_args']['tolerance'] = tolerance
+        return self._spatial_attribute(func, s, **kwargs)
+
+    def _geocol_select(self, geo_field, field_name, aggregate=False):
         """
         Helper routine for constructing the SQL to select the geographic
         column.  Takes into account if the geographic field is in a
             # (e.g., if 'location__point' was given as the field name).
             self.query.add_select_related([field_name])
             self.query.pre_sql_setup()
+            # Can't non-aggregate and aggregate selections together.
+            if aggregate: self.query.aggregate = True 
             rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)]
             return self.query._field_column(geo_field, rel_table)
         else:

django/contrib/gis/db/models/sql/__init__.py

-from django.contrib.gis.db.models.sql.query import GeoQuery
+from django.contrib.gis.db.models.sql.query import AreaField, DistanceField, GeomField, GeoQuery
 from django.contrib.gis.db.models.sql.where import GeoWhereNode

django/contrib/gis/db/models/sql/query.py

 from django.contrib.gis.db.backend import SpatialBackend
 from django.contrib.gis.db.models.fields import GeometryField
 from django.contrib.gis.db.models.sql.where import GeoWhereNode
+from django.contrib.gis.measure import Area, Distance
 
 # Valid GIS query types.
 ALL_TERMS = sql.constants.QUERY_TERMS.copy()
         # The following attributes are customized for the GeoQuerySet.
         # The GeoWhereNode and SpatialBackend classes contain backend-specific
         # routines and functions.
+        self.aggregate = False
         self.custom_select = {}
-        self.ewkt = None
+        self.transformed_srid = None
+        self.extra_select_fields = {}
 
     def clone(self, *args, **kwargs):
         obj = super(GeoQuery, self).clone(*args, **kwargs)
-        # Customized selection dictionary and EWKT flag have to be added to obj.
+        # Customized selection dictionary and transformed srid flag have
+        # to also be added to obj.
+        obj.aggregate = self.aggregate
         obj.custom_select = self.custom_select.copy()
-        obj.ewkt = self.ewkt
+        obj.transformed_srid = self.transformed_srid
+        obj.extra_select_fields = self.extra_select_fields.copy()
         return obj
 
     def get_columns(self, with_aliases=False):
         """
         qn = self.quote_name_unless_alias
         qn2 = self.connection.ops.quote_name
-        result = ['(%s) AS %s' % (col, qn2(alias)) for alias, col in self.extra_select.iteritems()]
+        result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col, qn2(alias)) 
+                  for alias, col in self.extra_select.iteritems()]
         aliases = set(self.extra_select.keys())
         if with_aliases:
             col_aliases = aliases.copy()
             result.extend(cols)
             aliases.update(new_aliases)
         # This loop customized for GeoQuery.
-        for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
-            r = self.get_field_select(field, table)
-            if with_aliases and col in col_aliases:
-                c_alias = 'Col%d' % len(col_aliases)
-                result.append('%s AS %s' % (r, c_alias))
-                aliases.add(c_alias)
-                col_aliases.add(c_alias)
-            else:
-                result.append(r)
-                aliases.add(r)
-                col_aliases.add(col)
+        if not self.aggregate:
+            for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
+                r = self.get_field_select(field, table)
+                if with_aliases and col in col_aliases:
+                    c_alias = 'Col%d' % len(col_aliases)
+                    result.append('%s AS %s' % (r, c_alias))
+                    aliases.add(c_alias)
+                    col_aliases.add(c_alias)
+                else:
+                    result.append(r)
+                    aliases.add(r)
+                    col_aliases.add(col)
 
         self._select_aliases = aliases
         return result
                     col_aliases.add(field.column)
         return result, aliases
 
+    def resolve_columns(self, row, fields=()):
+        """
+        This routine is necessary so that distances and geometries returned
+        from extra selection SQL get resolved appropriately into Python 
+        objects.
+        """
+        values = []
+        aliases = self.extra_select.keys()
+        index_start = len(aliases)
+        values = [self.convert_values(v, self.extra_select_fields.get(a, None)) 
+                  for v, a in izip(row[:index_start], aliases)]
+        if SpatialBackend.oracle:
+            # This is what happens normally in Oracle's `resolve_columns`.
+            for value, field in izip(row[index_start:], fields):
+                values.append(self.convert_values(value, field))
+        else:
+            values.extend(row[index_start:])
+        return values
+
+    def convert_values(self, value, field):
+        """
+        Using the same routines that Oracle does we can convert our
+        extra selection objects into Geometry and Distance objects.
+        TODO: Laziness.
+        """
+        if SpatialBackend.oracle:
+            # Running through Oracle's first.
+            value = super(GeoQuery, self).convert_values(value, field)
+        if isinstance(field, DistanceField):
+            # Using the field's distance attribute, can instantiate
+            # `Distance` with the right context.
+            value = Distance(**{field.distance_att : value})
+        elif isinstance(field, AreaField):
+            value = Area(**{field.area_att : value})
+        elif isinstance(field, GeomField):
+            value = SpatialBackend.Geometry(value)
+        return value
+
     #### Routines unique to GeoQuery ####
+    def get_extra_select_format(self, alias):
+        sel_fmt = '%s'
+        if alias in self.custom_select:
+            sel_fmt = sel_fmt % self.custom_select[alias]
+        return sel_fmt
+
     def get_field_select(self, fld, alias=None):
         """
         Returns the SELECT SQL string for the given field.  Figures out
             # the SRID is prefixed to the returned WKT to ensure that the
             # transformed geometries have an SRID different than that of the
             # field -- this is only used by `transform` for Oracle backends.
-            if self.ewkt and SpatialBackend.oracle:
-                sel_fmt = "'SRID=%d;'||%s" % (self.ewkt, sel_fmt)
+            if self.transformed_srid and SpatialBackend.oracle:
+                sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt)
         else:
             sel_fmt = '%s'
         return sel_fmt
         Related model field strings like 'address__point', may also be 
         used.
 
-        If a GeometryField exists according to the given name
-        parameter it will be returned, otherwise returns False.
+        If a GeometryField exists according to the given name parameter 
+        it will be returned, otherwise returns False.
         """
         if isinstance(name_param, basestring):
             # This takes into account the situation where the name is a 
             # Otherwise, check by the given field name -- which may be
             # a lookup to a _related_ geographic field.
             return self._check_geo_field(self.model, field_name)
+
+### Field Classes for `convert_values` ####
+class AreaField(object):
+    def __init__(self, area_att):
+        self.area_att = area_att
+
+class DistanceField(object):
+    def __init__(self, distance_att):
+        self.distance_att = distance_att
+
+# Rather than use GeometryField (which requires a SQL query
+# upon instantiation), use this lighter weight class.
+class GeomField(object): 
+    pass

django/contrib/gis/db/models/sql/where.py

 from django.db.models.sql.where import WhereNode
-from django.contrib.gis.db.backend import get_geo_where_clause, GIS_TERMS
+from django.contrib.gis.db.backend import get_geo_where_clause, SpatialBackend
 
 class GeoWhereNode(WhereNode):
     """
     def make_atom(self, child, qn):
         table_alias, name, field, lookup_type, value = child
         if hasattr(field, '_geom'):
-            if lookup_type in GIS_TERMS:
+            if lookup_type in SpatialBackend.gis_terms:
                 # Getting the geographic where clause; substitution parameters
                 # will be populated in the GeoFieldSQL object returned by the
                 # GeometryField.

django/contrib/gis/measure.py

 Distance and Area objects to allow for sensible and convienient calculation 
 and conversions.
 
-Author: Robert Coup
+Author: Robert Coup, Justin Bronn
 
 Inspired by GeoPy (http://exogen.case.edu/projects/geopy/)
 and Geoff Biggs' PhD work on dimensioned units for robotics.
 """
+__all__ = ['A', 'Area', 'D', 'Distance']
 from decimal import Decimal
 
-class Distance(object):
+class MeasureBase(object):
+    def default_units(self, kwargs):
+        """
+        Return the unit value and the the default units specified
+        from the given keyword arguments dictionary.
+        """
+        val = 0.0
+        for unit, value in kwargs.iteritems():
+            if unit in self.UNITS:
+                val += self.UNITS[unit] * value
+                default_unit = unit
+            elif unit in self.ALIAS:
+                u = self.ALIAS[unit]
+                val += self.UNITS[u] * value
+                default_unit = u
+            else:
+                lower = unit.lower()
+                if lower in self.UNITS:
+                    val += self.UNITS[lower] * value
+                    default_unit = lower
+                elif lower in self.LALIAS:
+                    u = self.LALIAS[lower]
+                    val += self.UNITS[u] * value
+                    default_unit = u
+                else:
+                    raise AttributeError('Unknown unit type: %s' % unit)
+        return val, default_unit
+
+    @classmethod
+    def unit_attname(cls, unit_str):
+        """
+        Retrieves the unit attribute name for the given unit string.  
+        For example, if the given unit string is 'metre', 'm' would be returned.
+        An exception is raised if an attribute cannot be found.
+        """
+        lower = unit_str.lower()
+        if unit_str in cls.UNITS:
+            return unit_str
+        elif lower in cls.UNITS:
+            return lower
+        elif lower in cls.LALIAS:
+            return cls.LALIAS[lower]
+        else:
+            raise Exception('Could not find a unit keyword associated with "%s"' % unit_str)
+
+class Distance(MeasureBase):
     UNITS = {
         'chain' : 20.1168,
         'chain_benoit' : 20.116782,
         'fathom' :  1.8288,
         'ft': 0.3048,
         'german_m' : 1.0000135965,
-        'grad' : 0.0157079632679,
         'gold_coast_ft' : 0.304799710181508,
         'indian_yd' : 0.914398530744,
         'in' : 0.0254,
         'British chain (Sears 1922)' : 'british_chain_sears',
         'British chain (Sears 1922 truncated)' : 'british_chain_sears_truncated',
         'British foot (Sears 1922)' : 'british_ft',
+        'British foot' : 'british_ft',
         'British yard (Sears 1922)' : 'british_yd',
+        'British yard' : 'british_yd',
         "Clarke's Foot" : 'clarke_ft',
-        "Clarke's foot" : 'clarke_ft',
         "Clarke's link" : 'clarke_link',
         'Chain (Benoit)' : 'chain_benoit',
         'Chain (Sears)' : 'chain_sears',
         'Yard (Indian)' : 'indian_yd',
         'Yard (Sears)' : 'sears_yd'
         }
-    REV_ALIAS = dict((value, key) for key, value in ALIAS.items())
+    LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
 
     def __init__(self, default_unit=None, **kwargs):
         # The base unit is in meters.
-        self.m = 0.0
-        self._default_unit = 'm'
-        
-        for unit,value in kwargs.items():
-            if unit in self.UNITS:
-                self.m += self.UNITS[unit] * value
-                self._default_unit = unit
-            elif unit in self.ALIAS:
-                u = self.ALIAS[unit]
-                self.m += self.UNITS[u] * value
-                self._default_unit = u
-            else:
-                lower = unit.lower()
-                if lower in self.UNITS:
-                    self.m += self.UNITS[lower] * value
-                    self._default_unit = lower
-                elif lower in self.ALIAS:
-                    u = self.ALIAS[lower]
-                    self.m += self.UNITS[u] * value
-                    self._default_unit = u
-                else:
-                    raise AttributeError('Unknown unit type: %s' % unit)
-
+        self.m, self._default_unit = self.default_units(kwargs)
         if default_unit and isinstance(default_unit, str):
             self._default_unit = default_unit
     
     def __nonzero__(self):
         return bool(self.m)
 
-    @classmethod
-    def unit_attname(cls, unit_str):
-        """
-        Retrieves the unit attribute name for the given unit string.  
-        For example, if the given unit string is 'metre', 'm' would be returned.  
-        An exception is raised if an attribute cannot be found.
-        """
-        lower = unit_str.lower()
-
-        if unit_str in cls.UNITS:
-            return unit_str
-        elif lower in cls.UNITS:
-            return lower
-        elif unit_str in cls.ALIAS:
-            return cls.ALIAS[unit_str]
-        elif lower in cls.ALIAS:
-            return cls.ALIAS[lower]
-        else:
-            raise Exception('Could not find a unit keyword associated with "%s"' % unit_str)
-
-class Area(object):
-    # TODO: Add units from above.
-    UNITS = {
-        'sq_m': 1.0,
-        'sq_km': 1000000.0,
-        'sq_mi': 2589988.110336,
-        'sq_ft': 0.09290304,
-        'sq_yd': 0.83612736,
-        'sq_nm': 3429904.0,
-    }
+class Area(MeasureBase):
+    # Getting the square units values and the alias dictionary.
+    UNITS = dict([('sq_%s' % k, v ** 2) for k, v in Distance.UNITS.items()])
+    ALIAS = dict([(k, 'sq_%s' % v) for k, v in Distance.ALIAS.items()])
+    LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
 
     def __init__(self, default_unit=None, **kwargs):
-        self.sq_m = 0.0
-        self._default_unit = 'sq_m'
-        
-        for unit,value in kwargs.items():
-            if unit in self.UNITS:
-                self.sq_m += self.UNITS[unit] * value
-                self._default_unit = unit
-            else:
-                raise AttributeError('Unknown unit type: ' + unit)
-
-        if default_unit:
+        self.sq_m, self._default_unit = self.default_units(kwargs)
+        if default_unit and isinstance(default_unit, str):
             self._default_unit = default_unit
     
     def __getattr__(self, name):
 
     def __nonzero__(self):
         return bool(self.sq_m)
-
         
 # Shortcuts
 D = Distance

django/contrib/gis/models.py

             return unicode(self.wkt)
 
 # The SpatialRefSys and GeometryColumns models
+_srid_info = True
 if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
     from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys
 elif settings.DATABASE_ENGINE == 'oracle':
     from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys
 else:
-    pass
+    _srid_info = False
+
+if _srid_info:
+    def get_srid_info(srid):
+        """
+        Returns the units, unit name, and spheroid WKT associated with the
+        given SRID from the `spatial_ref_sys` (or equivalent) spatial database
+        table.  We use a database cursor to execute the query because this
+        function is used when it is not possible to use the ORM (for example,
+        during field initialization).
+        """
+        from django.db import connection
+        # Getting the spatial reference WKT associated with the SRID from the
+        # `spatial_ref_sys` (or equivalent) spatial database table.
+        #
+        # The following doesn't work: SpatialRefSys.objects.get(srid=srid)
+        # Why?  `syncdb` fails to recognize installed geographic models when there's
+        # an ORM query instantiated within a model field.
+        cur = connection.cursor()
+        qn = connection.ops.quote_name
+        stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)'
+        stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table),
+                       'wkt_col' : qn(SpatialRefSys.wkt_col()),
+                       'srid_col' : qn('srid'),
+                       'srid' : srid,
+                       }
+        cur.execute(stmt)
+        srs_wkt = cur.fetchone()[0]
+        if srs_wkt is None:
+            raise ValueError('Failed to find Spatial Reference System entry corresponding to SRID=%s' % srid)
+
+        # Getting metadata associated with the spatial reference system identifier.
+        # Specifically, getting the unit information and spheroid information 
+        # (both required for distance queries).
+        unit, unit_name = SpatialRefSys.get_units(srs_wkt)
+        spheroid = SpatialRefSys.get_spheroid(srs_wkt)
+        return unit, unit_name, spheroid
+else:
+    def get_srid_info(srid):
+        """
+        Dummy routine for the backends that do not have the OGC required
+        spatial metadata tables (like MySQL).
+        """
+        return None, None, None
+   

django/contrib/gis/tests/__init__.py

 if not settings._target: settings.configure()
 
 # Tests that require use of a spatial database (e.g., creation of models)
-test_models = ['geoapp', 'relatedapp']
+test_models = ['geoapp',]
 
 # Tests that do not require setting up and tearing down a spatial database.
 test_suite_names = [
 ]
 if HAS_GDAL:
     if oracle:
-        # TODO: There is a problem with the `syncdb` SQL for the LayerMapping
-        # tests on Oracle.
-        test_models += ['distapp']
+        # TODO: There's a problem with `select_related` and GeoQuerySet on
+        # Oracle -- e.g., GeoModel.objects.distance(geom, field_name='fk__point')
+        # doesn't work so we don't test `relatedapp`.
+        test_models += ['distapp', 'layermap']
     elif postgis:
-        test_models += ['distapp', 'layermap']
+        test_models += ['distapp', 'layermap', 'relatedapp']
+    elif mysql:
+        test_models += ['relatedapp']
 
     test_suite_names += [
         'test_gdal_driver',

django/contrib/gis/tests/distapp/data.py

              ('Hillsdale', 151.231341, -33.952685),
              )
 
-stx_cities = (('Downtown Houston', 951640.547328, 4219369.26172),
-              ('West University Place', 943547.922328, 4213623.65345),
-              ('Southside Place', 944704.643307, 4212768.87617),
-              ('Bellaire', 942595.669129, 4212686.72583),
-              ('Pearland', 959674.616506, 4197465.6526),
-              ('Galveston', 1008151.16007, 4170027.47655),
-              ('Sealy', 874859.286808, 4219186.8641),
-              ('San Antonio', 649173.910483, 4176413.27786),
-              ('Round Rock', 726846.03695, 4297160.99715),
-              ('Saint Hedwig', 677644.649952, 4175467.06744),
+stx_cities = (('Downtown Houston', -95.363151, 29.763374),
+              ('West University Place&