1
0
mirror of https://github.com/django/django.git synced 2025-07-04 17:59:13 +00:00

gis: Applied DRY to spatial SQL generation in anticipation of queryset-refactor; fixed gml function for PostGIS 1.3.2 parameter ordering.

git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@6919 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Justin Bronn 2007-12-15 00:30:48 +00:00
parent 9270d47d80
commit 5799c2e048
11 changed files with 256 additions and 217 deletions

View File

@ -26,7 +26,7 @@ from django.utils.datastructures import SortedDict
from django.contrib.gis.geos import GEOSGeometry
# These routines (needed by GeoManager), default to False.
ASGML, ASKML, DISTANCE, TRANSFORM, UNION= (False, False, False, False, False)
ASGML, ASKML, DISTANCE, TRANSFORM, UNION, VERSION = (False, False, False, False, False, False)
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
# PostGIS is the spatial database, getting the rquired modules,
@ -34,7 +34,9 @@ if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
from django.contrib.gis.db.backend.postgis import \
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
create_spatial_db, get_geo_where_clause, \
ASGML, ASKML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
ASGML, ASKML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION, \
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
VERSION = (MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2)
SPATIAL_BACKEND = 'postgis'
elif settings.DATABASE_ENGINE == 'oracle':
from django.contrib.gis.db.backend.oracle import \
@ -283,7 +285,7 @@ def lookup_inner(path, lookup_type, value, opts, table, column):
geo_prep = field.get_db_prep_lookup(lookup_type, value)
# Getting the adapted geometry from the field.
gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)
gwc = get_geo_where_clause(lookup_type, current_table, column, value)
# Substituting in the the where parameters into the geographic where
# clause, and extending the parameters.

View File

@ -5,6 +5,11 @@
from django.db import connection
qn = connection.ops.quote_name
# To ease implementation, WKT is passed to/from MySQL.
GEOM_FROM_TEXT = 'GeomFromText'
GEOM_FROM_WKB = 'GeomFromWKB'
GEOM_SELECT = 'AsText(%s)'
# WARNING: MySQL is NOT compliant w/the OpenGIS specification and
# _every_ one of these lookup types is on the _bounding box_ only.
MYSQL_GIS_FUNCTIONS = {
@ -31,24 +36,18 @@ MYSQL_GIS_TERMS = tuple(MYSQL_GIS_TERMS) # Making immutable
def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
"Returns the SQL WHERE clause for use in MySQL spatial SQL construction."
if table_prefix.endswith('.'):
table_prefix = qn(table_prefix[:-1])+'.'
field_name = qn(field_name)
# Getting the quoted field as `geo_col`.
geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
# See if a MySQL Geometry function matches the lookup type next
lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False)
if lookup_info:
return "%s(%s, %%s)" % (lookup_info, table_prefix + field_name)
return "%s(%s, %%s)" % (lookup_info, geo_col)
# Handling 'isnull' lookup type
# TODO: Is this needed because MySQL cannot handle NULL
# geometries in its spatial indices.
if lookup_type == 'isnull':
return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
# To ease implementation, WKT is passed to/from MySQL.
GEOM_FROM_TEXT = 'GeomFromText'
GEOM_FROM_WKB = 'GeomFromWKB'
GEOM_SELECT = 'AsText(%s)'

View File

@ -4,7 +4,7 @@ from django.db import connection
from django.db.backends.util import truncate_name
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 get_srid, GeoFieldSQL
from django.contrib.gis.db.backend.util import GeoFieldSQL
from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, DISTANCE_FUNCTIONS, TRANSFORM

View File

@ -12,8 +12,8 @@ from django.contrib.gis.models import SpatialRefSysMixin
class GeometryColumns(models.Model):
"Maps to the Oracle USER_SDO_GEOM_METADATA table."
table_name = models.CharField(maxlength=32)
column_name = models.CharField(maxlength=1024)
table_name = models.CharField(max_length=32)
column_name = models.CharField(max_length=1024)
srid = models.IntegerField(primary_key=True)
# TODO: Add support for `diminfo` column (type MDSYS.SDO_DIM_ARRAY).
class Meta:
@ -28,11 +28,11 @@ class GeometryColumns(models.Model):
class SpatialRefSys(models.Model, SpatialRefSysMixin):
"Maps to the Oracle MDSYS.CS_SRS table."
cs_name = models.CharField(maxlength=68)
cs_name = models.CharField(max_length=68)
srid = models.IntegerField(primary_key=True)
auth_srid = models.IntegerField()
auth_name = models.CharField(maxlength=256)
wktext = models.CharField(maxlength=2046)
auth_name = models.CharField(max_length=256)
wktext = models.CharField(max_length=2046)
#cs_bounds = models.GeometryField()
class Meta:

View File

@ -5,6 +5,7 @@
import re
from decimal import Decimal
from django.db import connection
from django.contrib.gis.db.backend.util import SpatialFunction
from django.contrib.gis.measure import Distance
qn = connection.ops.quote_name
@ -14,61 +15,44 @@ DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
TRANSFORM = 'SDO_CS.TRANSFORM'
UNION = 'SDO_AGGR_UNION'
class SDOOperation(object):
# We want to get SDO Geometries as WKT because it is much easier to
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
# However, this adversely affects performance (i.e., Java is called
# to convert to WKT on every query). If someone wishes to write a
# SDO_GEOMETRY(...) parser in Python, let me know =)
GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
#### Classes used in constructing Oracle spatial SQL ####
class SDOOperation(SpatialFunction):
"Base class for SDO* Oracle operations."
def __init__(self, func, end_subst=") %s '%s'"):
super(SDOOperation, self).__init__(func, end_subst=end_subst, operator='=', result='TRUE')
def __init__(self, lookup, subst='', operator='=', result="'TRUE'",
beg_subst='%s(%s%s, %%s'):
self.lookup = lookup
self.subst = subst
self.operator = operator
self.result = result
self.beg_subst = beg_subst
self.end_subst = ') %s %s' % (self.operator, self.result)
@property
def sql_subst(self):
return ''.join([self.beg_subst, self.subst, self.end_subst])
def as_sql(self, table, field):
return self.sql_subst % self.params(table, field)
def params(self, table, field):
return (self.lookup, table, field)
class SDODistance(SDOOperation):
class SDODistance(SpatialFunction):
"Class for Distance queries."
def __init__(self, op, tolerance=0.05):
super(SDODistance, self).__init__(DISTANCE, subst=", %s", operator=op, result='%%s')
self.tolerance = tolerance
super(SDODistance, self).__init__(DISTANCE, end_subst=', %s) %%s %%s' % tolerance,
operator=op, result='%%s')
def params(self, table, field):
return (self.lookup, table, field, self.tolerance)
class SDOGeomRelate(SDOOperation):
class SDOGeomRelate(SpatialFunction):
"Class for using SDO_GEOM.RELATE."
def __init__(self, mask, tolerance=0.05):
super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst="%s(%s%s, '%s'",
subst=", %%s, %s", result="'%s'" % mask)
self.mask = mask
self.tolerance = tolerance
# SDO_GEOM.RELATE(...) has a peculiar argument order: column, mask, geom, tolerance.
# Moreover, the runction result is the mask (e.g., 'DISJOINT' instead of 'TRUE').
end_subst = "%s%s) %s '%s'" % (', %%s, ', tolerance, '=', mask)
beg_subst = "%%s(%%s, '%s'" % mask
super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst=beg_subst, end_subst=end_subst)
def params(self, table, field):
return (self.lookup, table, field, self.mask, self.tolerance)
class SDORelate(SDOOperation):
class SDORelate(SpatialFunction):
"Class for using SDO_RELATE."
masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
def __init__(self, mask, **kwargs):
super(SDORelate, self).__init__('SDO_RELATE', subst=", 'mask=%s'", **kwargs)
def __init__(self, mask):
if not self.mask_regex.match(mask):
raise ValueError('Invalid %s mask: "%s"' % (self.lookup, mask))
self.mask = mask
super(SDORelate, self).__init__('SDO_RELATE', end_subst=", 'mask=%s') = 'TRUE'" % mask)
def params(self, table, field):
return (self.lookup, table, field, self.mask)
#### Lookup type mapping dictionaries of Oracle spatial operations ####
# Valid distance types and substitutions
dtypes = (Decimal, Distance, float, int)
@ -84,7 +68,7 @@ ORACLE_GEOMETRY_FUNCTIONS = {
'coveredby' : SDOOperation('SDO_COVEREDBY'),
'covers' : SDOOperation('SDO_COVERS'),
'disjoint' : SDOGeomRelate('DISJOINT'),
'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', "%%s, 'distance=%%s'"), dtypes),
'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', end_subst=", %%s, 'distance=%%s') %s '%s'"), dtypes),
'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
'equals' : SDOOperation('SDO_EQUAL'),
'exact' : SDOOperation('SDO_EQUAL'),
@ -104,20 +88,20 @@ ORACLE_SPATIAL_TERMS = ORACLE_GEOMETRY_FUNCTIONS.keys()
ORACLE_SPATIAL_TERMS += MISC_TERMS
ORACLE_SPATIAL_TERMS = tuple(ORACLE_SPATIAL_TERMS) # Making immutable
#### The `get_geo_where_clause` function for Oracle ####
def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
"Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
if table_prefix.endswith('.'):
table_prefix = qn(table_prefix[:-1])+'.'
field_name = qn(field_name)
# Getting the quoted table name as `geo_col`.
geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
# See if a Oracle Geometry function matches the lookup type next
lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False)
if lookup_info:
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
# 'dwithin' lookup types.
# 'dwithin' lookup types.
if isinstance(lookup_info, tuple):
# First element of tuple is lookup type, second element is the type
# of the expected argument (e.g., str, float)
# of the expected argument (e.g., str, float)
sdo_op, arg_type = lookup_info
# Ensuring that a tuple _value_ was passed in from the user
@ -131,27 +115,19 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
if lookup_type == 'relate':
# The SDORelate class handles construction for these queries, and verifies
# the mask argument.
return sdo_op(value[1]).as_sql(table_prefix, field_name)
elif lookup_type in DISTANCE_FUNCTIONS:
op = DISTANCE_FUNCTIONS[lookup_type][0]
return op.as_sql(table_prefix, field_name)
# return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op)
# The SDORelate class handles construction for these queries,
# and verifies the mask argument.
return sdo_op(value[1]).as_sql(geo_col)
else:
return sdo_op.as_sql(table_prefix, field_name)
# Otherwise, just call the `as_sql` method on the SDOOperation instance.
return sdo_op.as_sql(geo_col)
else:
# Lookup info is a SDOOperation instance, whos `as_sql` method returns
# the SQL necessary for the geometry function call. For example:
# SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
return lookup_info.as_sql(table_prefix, field_name)
# Handling 'isnull' lookup type
if lookup_type == 'isnull':
return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
return lookup_info.as_sql(geo_col)
elif lookup_type == 'isnull':
# Handling 'isnull' lookup type
return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
# Want to get SDO Geometries as WKT (much easier to instantiate GEOS proxies
# from WKT than SDO_GEOMETRY(...) strings ;)
GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'

View File

@ -45,6 +45,7 @@ def _create_with_cursor(db_name, verbosity=1, autoclobber=False):
try:
# Trying to create the database first.
cursor.execute(create_sql)
#print create_sql
except Exception, e:
# Drop and recreate, if necessary.
if not autoclobber:
@ -56,6 +57,7 @@ def _create_with_cursor(db_name, verbosity=1, autoclobber=False):
cursor.execute(create_sql)
else:
raise Exception('Spatial Database Creation canceled.')
foo = _create_with_cursor
created_regex = re.compile(r'^createdb: database creation failed: ERROR: database ".+" already exists')
def _create_with_shell(db_name, verbosity=1, autoclobber=False):

View File

@ -1,11 +1,11 @@
from types import UnicodeType
from django.db import connection
from django.db.models.fields import Field # Django base Field class
from django.contrib.gis.geos import GEOSGeometry, GEOSException
from django.contrib.gis.db.backend.util import get_srid, GeoFieldSQL
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.db.backend.util import GeoFieldSQL
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
from django.contrib.gis.db.backend.postgis.query import \
DISTANCE, DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
# Quotename & geographic quotename, respectively
qn = connection.ops.quote_name

View File

@ -14,13 +14,13 @@ class GeometryColumns(models.Model):
The 'geometry_columns' table from the PostGIS. See the PostGIS
documentation at Ch. 4.2.2.
"""
f_table_catalog = models.CharField(maxlength=256)
f_table_schema = models.CharField(maxlength=256)
f_table_name = models.CharField(maxlength=256)
f_geometry_column = models.CharField(maxlength=256)
f_table_catalog = models.CharField(max_length=256)
f_table_schema = models.CharField(max_length=256)
f_table_name = models.CharField(max_length=256)
f_geometry_column = models.CharField(max_length=256)
coord_dimension = models.IntegerField()
srid = models.IntegerField(primary_key=True)
type = models.CharField(maxlength=30)
type = models.CharField(max_length=30)
class Meta:
db_table = 'geometry_columns'
@ -41,10 +41,10 @@ class SpatialRefSys(models.Model, SpatialRefSysMixin):
documentaiton at Ch. 4.2.1.
"""
srid = models.IntegerField(primary_key=True)
auth_name = models.CharField(maxlength=256)
auth_name = models.CharField(max_length=256)
auth_srid = models.IntegerField()
srtext = models.CharField(maxlength=2048)
proj4text = models.CharField(maxlength=2048)
srtext = models.CharField(max_length=2048)
proj4text = models.CharField(max_length=2048)
class Meta:
db_table = 'spatial_ref_sys'

View File

@ -2,10 +2,12 @@
This module contains the spatial lookup types, and the get_geo_where_clause()
routine for PostGIS.
"""
import re
from decimal import Decimal
from django.db import connection
from django.contrib.gis.measure import Distance
from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple
from django.contrib.gis.db.backend.util import SpatialOperation, SpatialFunction
qn = connection.ops.quote_name
# Getting the PostGIS version information
@ -17,60 +19,23 @@ POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = postgis_version
if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1):
raise Exception('PostGIS version %s not supported.' % POSTGIS_VERSION)
# PostGIS-specific operators. The commented descriptions of these
# operators come from Section 6.2.2 of the official PostGIS documentation.
POSTGIS_OPERATORS = {
# The "&<" operator returns true if A's bounding box overlaps or
# is to the left of B's bounding box.
'overlaps_left' : '&<',
# The "&>" operator returns true if A's bounding box overlaps or
# is to the right of B's bounding box.
'overlaps_right' : '&>',
# The "<<" operator returns true if A's bounding box is strictly
# to the left of B's bounding box.
'left' : '<<',
# The ">>" operator returns true if A's bounding box is strictly
# to the right of B's bounding box.
'right' : '>>',
# The "&<|" operator returns true if A's bounding box overlaps or
# is below B's bounding box.
'overlaps_below' : '&<|',
# The "|&>" operator returns true if A's bounding box overlaps or
# is above B's bounding box.
'overlaps_above' : '|&>',
# The "<<|" operator returns true if A's bounding box is strictly
# below B's bounding box.
'strictly_below' : '<<|',
# The "|>>" operator returns true if A's bounding box is strictly
# above B's bounding box.
'strictly_above' : '|>>',
# The "~=" operator is the "same as" operator. It tests actual
# geometric equality of two features. So if A and B are the same feature,
# vertex-by-vertex, the operator returns true.
'same_as' : '~=',
'exact' : '~=',
# The "@" operator returns true if A's bounding box is completely contained
# by B's bounding box.
'contained' : '@',
# The "~" operator returns true if A's bounding box completely contains
# by B's bounding box.
'bbcontains' : '~',
# The "&&" operator returns true if A's bounding box overlaps
# B's bounding box.
'bboverlaps' : '&&',
}
# Versions of PostGIS >= 1.2.2 changed their naming convention to be
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
# means that 'ST_' is prefixes geometry function names.
GEOM_FUNC_PREFIX = ''
if MAJOR_VERSION >= 1:
if (MINOR_VERSION1 > 2 or
if (MINOR_VERSION1 > 2 or
(MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)):
GEOM_FUNC_PREFIX = 'ST_'
def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
# Custom selection not needed for PostGIS since GEOS geometries may be
# 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)'
GEOM_SELECT = None
# Functions used by the GeoManager & GeoQuerySet
ASKML = get_func('AsKML')
ASGML = get_func('AsGML')
@ -90,48 +55,115 @@ if MAJOR_VERSION >= 1:
else:
raise NotImplementedError('PostGIS versions < 1.0 are not supported.')
#### Classes used in constructing PostGIS spatial SQL ####
class PostGISOperator(SpatialOperation):
"For PostGIS operators (e.g. `&&`, `~`)."
def __init__(self, operator):
super(PostGISOperator, self).__init__(operator=operator, beg_subst='%s %s %%s')
class PostGISFunction(SpatialFunction):
"For PostGIS function calls (e.g., `ST_Contains(table, geom)`)."
def __init__(self, function, **kwargs):
super(PostGISFunction, self).__init__(get_func(function), **kwargs)
class PostGISFunctionParam(PostGISFunction):
"For PostGIS functions that take another parameter (e.g. DWithin, Relate)."
def __init__(self, func):
super(PostGISFunctionParam, self).__init__(func, end_subst=', %%s)')
class PostGISDistance(PostGISFunction):
"For PostGIS distance operations."
def __init__(self, operator):
super(PostGISDistance, self).__init__('Distance', end_subst=') %s %s', operator=operator, result='%%s')
class PostGISRelate(PostGISFunctionParam):
"For PostGIS Relate(<geom>, <pattern>) calls."
pattern_regex = re.compile(r'^[012TF\*]{9}$')
def __init__(self, pattern):
if not self.pattern_regex.match(pattern):
raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
super(PostGISRelate, self).__init__('Relate')
#### Lookup type mapping dictionaries of PostGIS operations. ####
# PostGIS-specific operators. The commented descriptions of these
# operators come from Section 6.2.2 of the official PostGIS documentation.
POSTGIS_OPERATORS = {
# The "&<" operator returns true if A's bounding box overlaps or
# is to the left of B's bounding box.
'overlaps_left' : PostGISOperator('&<'),
# The "&>" operator returns true if A's bounding box overlaps or
# is to the right of B's bounding box.
'overlaps_right' : PostGISOperator('&>'),
# The "<<" operator returns true if A's bounding box is strictly
# to the left of B's bounding box.
'left' : PostGISOperator('<<'),
# The ">>" operator returns true if A's bounding box is strictly
# to the right of B's bounding box.
'right' : PostGISOperator('>>'),
# The "&<|" operator returns true if A's bounding box overlaps or
# is below B's bounding box.
'overlaps_below' : PostGISOperator('&<|'),
# The "|&>" operator returns true if A's bounding box overlaps or
# is above B's bounding box.
'overlaps_above' : PostGISOperator('|&>'),
# The "<<|" operator returns true if A's bounding box is strictly
# below B's bounding box.
'strictly_below' : PostGISOperator('<<|'),
# The "|>>" operator returns true if A's bounding box is strictly
# above B's bounding box.
'strictly_above' : PostGISOperator('|>>'),
# The "~=" operator is the "same as" operator. It tests actual
# geometric equality of two features. So if A and B are the same feature,
# vertex-by-vertex, the operator returns true.
'same_as' : PostGISOperator('~='),
'exact' : PostGISOperator('~='),
# The "@" operator returns true if A's bounding box is completely contained
# by B's bounding box.
'contained' : PostGISOperator('@'),
# The "~" operator returns true if A's bounding box completely contains
# by B's bounding box.
'bbcontains' : PostGISOperator('~'),
# The "&&" operator returns true if A's bounding box overlaps
# B's bounding box.
'bboverlaps' : PostGISOperator('&&'),
}
# For PostGIS >= 1.2.2 the following lookup types will do a bounding box query
# first before calling the more computationally expensive GEOS routines (called
# "inline index magic"):
# 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and
# 'covers'.
POSTGIS_GEOMETRY_FUNCTIONS = {
'equals' : 'Equals',
'disjoint' : 'Disjoint',
'touches' : 'Touches',
'crosses' : 'Crosses',
'within' : 'Within',
'overlaps' : 'Overlaps',
'contains' : 'Contains',
'intersects' : 'Intersects',
'relate' : ('Relate', basestring),
'equals' : PostGISFunction('Equals'),
'disjoint' : PostGISFunction('Disjoint'),
'touches' : PostGISFunction('Touches'),
'crosses' : PostGISFunction('Crosses'),
'within' : PostGISFunction('Within'),
'overlaps' : PostGISFunction('Overlaps'),
'contains' : PostGISFunction('Contains'),
'intersects' : PostGISFunction('Intersects'),
'relate' : (PostGISRelate, basestring),
}
# Valid distance types and substitutions
dtypes = (Decimal, Distance, float, int)
DISTANCE_FUNCTIONS = {
'distance_gt' : ('>', dtypes),
'distance_gte' : ('>=', dtypes),
'distance_lt' : ('<', dtypes),
'distance_lte' : ('<=', dtypes),
'distance_gt' : (PostGISDistance('>'), dtypes),
'distance_gte' : (PostGISDistance('>='), dtypes),
'distance_lt' : (PostGISDistance('<'), dtypes),
'distance_lte' : (PostGISDistance('<='), dtypes),
}
if GEOM_FUNC_PREFIX == 'ST_':
# Adding the GEOM_FUNC_PREFIX to the lookup functions.
for lookup, f in POSTGIS_GEOMETRY_FUNCTIONS.items():
if isinstance(f, tuple):
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (get_func(f[0]), f[1])
else:
POSTGIS_GEOMETRY_FUNCTIONS[lookup] = get_func(f)
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
POSTGIS_GEOMETRY_FUNCTIONS.update(
{'dwithin' : ('ST_DWithin', dtypes),
'coveredby' : 'ST_CoveredBy',
'covers' : 'ST_Covers',
}
)
{'dwithin' : (PostGISFunctionParam('DWithin'), dtypes),
'coveredby' : PostGISFunction('CoveredBy'),
'covers' : PostGISFunction('Covers'),
})
# Distance functions are a part of PostGIS geometry functions.
POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
# Any other lookup types that do not require a mapping.
@ -144,33 +176,25 @@ POSTGIS_TERMS += POSTGIS_GEOMETRY_FUNCTIONS.keys() # Adding on the Geometry Func
POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
### PostGIS-specific Methods ###
def get_geom_func(lookup_type):
func_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
if isinstance(func_info, tuple):
return func_info[0]
else:
return func_info
#### The `get_geo_where_clause` function for PostGIS. ####
def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
"Returns the SQL WHERE clause for use in PostGIS SQL construction."
if table_prefix.endswith('.'):
table_prefix = qn(table_prefix[:-1])+'.'
field_name = qn(field_name)
# See if a PostGIS operator matches the lookup type first
# Getting the quoted field as `geo_col`.
geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
if lookup_type in POSTGIS_OPERATORS:
return '%s%s %s %%s' % (table_prefix, field_name, POSTGIS_OPERATORS[lookup_type])
# See if a PostGIS operator matches the lookup type.
return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col)
elif lookup_type in POSTGIS_GEOMETRY_FUNCTIONS:
# See if a PostGIS geometry function matches the lookup type.
tmp = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
# See if a PostGIS Geometry function matches the lookup type next
if lookup_type in POSTGIS_GEOMETRY_FUNCTIONS:
lookup_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
# 'dwithin' lookup types.
if isinstance(lookup_info, tuple):
# First element of tuple is lookup type, second element is the type
# of the expected argument (e.g., str, float)
func, arg_type = lookup_info
# distance lookups.
if isinstance(tmp, tuple):
# First element of tuple is the PostGISOperation instance, and the
# second element is either the type or a tuple of acceptable types
# that may passed in as further parameters for the lookup type.
op, arg_type = tmp
# Ensuring that a tuple _value_ was passed in from the user
if not isinstance(value, tuple):
@ -182,24 +206,15 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
if not isinstance(value[1], arg_type):
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
if lookup_type in DISTANCE_FUNCTIONS:
op = DISTANCE_FUNCTIONS[lookup_type][0]
return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op)
else:
return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name)
# For lookup type `relate`, the op instance is not yet created (has
# to be instantiated here to check the pattern parameter).
if lookup_type == 'relate': op = op(value[1])
else:
# Returning the SQL necessary for the geometry function call. For example:
# ST_Contains("geoapp_country"."poly", ST_GeomFromWKB(..))
return '%s(%s%s, %%s)' % (lookup_info, table_prefix, field_name)
# Handling 'isnull' lookup type
if lookup_type == 'isnull':
return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
op = tmp
# Calling the `as_sql` function on the operation instance.
return op.as_sql(geo_col)
elif lookup_type == 'isnull':
# Handling 'isnull' lookup type
return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
# Custom selection not needed for PostGIS since GEOS geometries may be
# 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)'
GEOM_SELECT = None

View File

@ -8,14 +8,52 @@ class GeoFieldSQL(object):
self.params = params
def __str__(self):
return self.where[0] % tuple(self.params)
return self.as_sql()
def get_srid(field, geom):
def as_sql(self, quote=False):
if not quote:
return self.where[0] % tuple(self.params)
else:
# Used for quoting WKT on certain backends.
tmp_params = ["'%s'" % self.params[0]]
tmp_params.extend(self.params[1:])
return self.where[0] % tuple(tmp_params)
class SpatialOperation(object):
"""
Gets the SRID depending on the value of the SRID setting of the field
and that of the given geometry.
Base class for generating spatial SQL.
"""
if geom.srid is None or (geom.srid == -1 and field._srid != -1):
return field._srid
else:
return geom.srid
def __init__(self, function='', operator='', result='', beg_subst='', end_subst=''):
self.function = function
self.operator = operator
self.result = result
self.beg_subst = beg_subst
try:
# Try and put the operator and result into to the
# end substitution.
self.end_subst = end_subst % (operator, result)
except TypeError:
self.end_subst = end_subst
@property
def sql_subst(self):
return ''.join([self.beg_subst, self.end_subst])
def as_sql(self, geo_col):
return self.sql_subst % self.params(geo_col)
def params(self, geo_col):
return (geo_col, self.operator)
class SpatialFunction(SpatialOperation):
"""
Base class for generating spatial SQL related to a function.
"""
def __init__(self, func, beg_subst='%s(%s, %%s', end_subst=')', result='', operator=''):
# Getting the function prefix.
kwargs = {'function' : func, 'operator' : operator, 'result' : result,
'beg_subst' : beg_subst, 'end_subst' : end_subst,}
super(SpatialFunction, self).__init__(**kwargs)
def params(self, geo_col):
return (self.function, geo_col)

View File

@ -7,11 +7,12 @@ from django.utils.datastructures import SortedDict
from django.contrib.gis.db.models.fields import GeometryField
# parse_lookup depends on the spatial database backend.
from django.contrib.gis.db.backend import parse_lookup, \
ASGML, ASKML, DISTANCE, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION
ASGML, ASKML, DISTANCE, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION, VERSION
from django.contrib.gis.geos import GEOSGeometry
# Flag indicating whether the backend is Oracle.
# Shortcut booleans for determining the backend.
oracle = SPATIAL_BACKEND == 'oracle'
postgis = SPATIAL_BACKEND == 'postgis'
class GeoQ(Q):
"Geographical query encapsulation object."
@ -325,8 +326,14 @@ class GeoQuerySet(QuerySet):
if oracle:
gml_select = {'gml':'%s(%s)' % (ASGML, field_col)}
else:
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)}
elif postgis:
# PostGIS AsGML() aggregate function parameter order depends on the
# version -- uggh.
major, minor1, minor2 = VERSION
if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)):
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, version, field_col, precision)}
else:
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)}
# Adding GML function call to SELECT part of the SQL.
return self.extra(select=gml_select)