Permalink
Browse files

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.


git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7641 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent eb30cad commit 4ec80c4333b618fc1ef2a02c7b8ca8719792f25f @jbronn jbronn committed Jun 15, 2008
@@ -3,97 +3,16 @@
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
@@ -0,0 +1,29 @@
+"""
+ 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
+
+
+
+
@@ -1 +1,13 @@
+__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)
@@ -0,0 +1,31 @@
+__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,
+ )
@@ -0,0 +1,5 @@
+from cx_Oracle import CLOB
+from django.contrib.gis.db.backend.adaptor import WKTAdaptor
+
+class OracleSpatialAdaptor(WKTAdaptor):
+ input_size = CLOB
@@ -15,10 +15,21 @@
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.
@@ -0,0 +1,42 @@
+__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,
+ )
@@ -1,6 +1,5 @@
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
@@ -21,7 +21,7 @@
# 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
@@ -30,26 +30,46 @@
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:
@@ -80,16 +100,23 @@ def __init__(self, operator):
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',
- operator=operator, result='%%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}$')
@@ -164,7 +191,7 @@ def __init__(self, pattern):
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),
@@ -193,6 +220,13 @@ def get_dist_ops(operator):
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."
@@ -216,8 +250,10 @@ def get_geo_where_clause(lookup_type, table_prefix, field, value):
# 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):
@@ -234,7 +270,9 @@ def get_geo_where_clause(lookup_type, table_prefix, field, value):
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:
Oops, something went wrong.

0 comments on commit 4ec80c4

Please sign in to comment.