1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +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,11 +88,11 @@ 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)
@ -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)
return lookup_info.as_sql(geo_col)
elif lookup_type == 'isnull':
# Handling 'isnull' lookup type
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))
# 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,49 +19,6 @@ 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
# means that 'ST_' is prefixes geometry function names.
@ -71,6 +30,12 @@ if MAJOR_VERSION >= 1:
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)
# 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:
return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name)
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)
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
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))
# 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):
"""
Gets the SRID depending on the value of the SRID setting of the field
and that of the given geometry.
"""
if geom.srid is None or (geom.srid == -1 and field._srid != -1):
return field._srid
def as_sql(self, quote=False):
if not quote:
return self.where[0] % tuple(self.params)
else:
return geom.srid
# 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):
"""
Base class for generating spatial SQL.
"""
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,6 +326,12 @@ class GeoQuerySet(QuerySet):
if oracle:
gml_select = {'gml':'%s(%s)' % (ASGML, field_col)}
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)}