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 from django.contrib.gis.geos import GEOSGeometry
# These routines (needed by GeoManager), default to False. # 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': if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
# PostGIS is the spatial database, getting the rquired modules, # 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 \ from django.contrib.gis.db.backend.postgis import \
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \ PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
create_spatial_db, get_geo_where_clause, \ 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' SPATIAL_BACKEND = 'postgis'
elif settings.DATABASE_ENGINE == 'oracle': elif settings.DATABASE_ENGINE == 'oracle':
from django.contrib.gis.db.backend.oracle import \ 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) geo_prep = field.get_db_prep_lookup(lookup_type, value)
# Getting the adapted geometry from the field. # 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 # Substituting in the the where parameters into the geographic where
# clause, and extending the parameters. # clause, and extending the parameters.

View File

@ -5,6 +5,11 @@
from django.db import connection from django.db import connection
qn = connection.ops.quote_name 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 # WARNING: MySQL is NOT compliant w/the OpenGIS specification and
# _every_ one of these lookup types is on the _bounding box_ only. # _every_ one of these lookup types is on the _bounding box_ only.
MYSQL_GIS_FUNCTIONS = { 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): def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
"Returns the SQL WHERE clause for use in MySQL spatial SQL construction." "Returns the SQL WHERE clause for use in MySQL spatial SQL construction."
if table_prefix.endswith('.'): # Getting the quoted field as `geo_col`.
table_prefix = qn(table_prefix[:-1])+'.' geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
field_name = qn(field_name)
# See if a MySQL Geometry function matches the lookup type next # See if a MySQL Geometry function matches the lookup type next
lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False) lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False)
if lookup_info: 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 # Handling 'isnull' lookup type
# TODO: Is this needed because MySQL cannot handle NULL # TODO: Is this needed because MySQL cannot handle NULL
# geometries in its spatial indices. # geometries in its spatial indices.
if lookup_type == 'isnull': 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)) 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.backends.util import truncate_name
from django.db.models.fields import Field # Django base Field class from django.db.models.fields import Field # Django base Field class
from django.contrib.gis.geos import GEOSGeometry 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.adaptor import OracleSpatialAdaptor
from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, DISTANCE_FUNCTIONS, TRANSFORM 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): class GeometryColumns(models.Model):
"Maps to the Oracle USER_SDO_GEOM_METADATA table." "Maps to the Oracle USER_SDO_GEOM_METADATA table."
table_name = models.CharField(maxlength=32) table_name = models.CharField(max_length=32)
column_name = models.CharField(maxlength=1024) column_name = models.CharField(max_length=1024)
srid = models.IntegerField(primary_key=True) srid = models.IntegerField(primary_key=True)
# TODO: Add support for `diminfo` column (type MDSYS.SDO_DIM_ARRAY). # TODO: Add support for `diminfo` column (type MDSYS.SDO_DIM_ARRAY).
class Meta: class Meta:
@ -28,11 +28,11 @@ class GeometryColumns(models.Model):
class SpatialRefSys(models.Model, SpatialRefSysMixin): class SpatialRefSys(models.Model, SpatialRefSysMixin):
"Maps to the Oracle MDSYS.CS_SRS table." "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) srid = models.IntegerField(primary_key=True)
auth_srid = models.IntegerField() auth_srid = models.IntegerField()
auth_name = models.CharField(maxlength=256) auth_name = models.CharField(max_length=256)
wktext = models.CharField(maxlength=2046) wktext = models.CharField(max_length=2046)
#cs_bounds = models.GeometryField() #cs_bounds = models.GeometryField()
class Meta: class Meta:

View File

@ -5,6 +5,7 @@
import re import re
from decimal import Decimal from decimal import Decimal
from django.db import connection from django.db import connection
from django.contrib.gis.db.backend.util import SpatialFunction
from django.contrib.gis.measure import Distance from django.contrib.gis.measure import Distance
qn = connection.ops.quote_name qn = connection.ops.quote_name
@ -14,61 +15,44 @@ DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
TRANSFORM = 'SDO_CS.TRANSFORM' TRANSFORM = 'SDO_CS.TRANSFORM'
UNION = 'SDO_AGGR_UNION' 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." "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'", class SDODistance(SpatialFunction):
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 for Distance queries." "Class for Distance queries."
def __init__(self, op, tolerance=0.05): def __init__(self, op, tolerance=0.05):
super(SDODistance, self).__init__(DISTANCE, subst=", %s", operator=op, result='%%s') super(SDODistance, self).__init__(DISTANCE, end_subst=', %s) %%s %%s' % tolerance,
self.tolerance = tolerance operator=op, result='%%s')
def params(self, table, field): class SDOGeomRelate(SpatialFunction):
return (self.lookup, table, field, self.tolerance)
class SDOGeomRelate(SDOOperation):
"Class for using SDO_GEOM.RELATE." "Class for using SDO_GEOM.RELATE."
def __init__(self, mask, tolerance=0.05): def __init__(self, mask, tolerance=0.05):
super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst="%s(%s%s, '%s'", # SDO_GEOM.RELATE(...) has a peculiar argument order: column, mask, geom, tolerance.
subst=", %%s, %s", result="'%s'" % mask) # Moreover, the runction result is the mask (e.g., 'DISJOINT' instead of 'TRUE').
self.mask = mask end_subst = "%s%s) %s '%s'" % (', %%s, ', tolerance, '=', mask)
self.tolerance = tolerance 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): class SDORelate(SpatialFunction):
return (self.lookup, table, field, self.mask, self.tolerance)
class SDORelate(SDOOperation):
"Class for using SDO_RELATE." "Class for using SDO_RELATE."
masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON' masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I) mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
def __init__(self, mask):
def __init__(self, mask, **kwargs):
super(SDORelate, self).__init__('SDO_RELATE', subst=", 'mask=%s'", **kwargs)
if not self.mask_regex.match(mask): if not self.mask_regex.match(mask):
raise ValueError('Invalid %s mask: "%s"' % (self.lookup, 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): #### Lookup type mapping dictionaries of Oracle spatial operations ####
return (self.lookup, table, field, self.mask)
# Valid distance types and substitutions # Valid distance types and substitutions
dtypes = (Decimal, Distance, float, int) dtypes = (Decimal, Distance, float, int)
@ -84,7 +68,7 @@ ORACLE_GEOMETRY_FUNCTIONS = {
'coveredby' : SDOOperation('SDO_COVEREDBY'), 'coveredby' : SDOOperation('SDO_COVEREDBY'),
'covers' : SDOOperation('SDO_COVERS'), 'covers' : SDOOperation('SDO_COVERS'),
'disjoint' : SDOGeomRelate('DISJOINT'), '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()? 'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
'equals' : SDOOperation('SDO_EQUAL'), 'equals' : SDOOperation('SDO_EQUAL'),
'exact' : 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 += MISC_TERMS
ORACLE_SPATIAL_TERMS = tuple(ORACLE_SPATIAL_TERMS) # Making immutable 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): def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
"Returns the SQL WHERE clause for use in Oracle spatial SQL construction." "Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
if table_prefix.endswith('.'): # Getting the quoted table name as `geo_col`.
table_prefix = qn(table_prefix[:-1])+'.' geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
field_name = qn(field_name)
# See if a Oracle Geometry function matches the lookup type next # See if a Oracle Geometry function matches the lookup type next
lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False) lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False)
if lookup_info: if lookup_info:
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and # Lookup types that are tuples take tuple arguments, e.g., 'relate' and
# 'dwithin' lookup types. # 'dwithin' lookup types.
if isinstance(lookup_info, tuple): if isinstance(lookup_info, tuple):
# First element of tuple is lookup type, second element is the type # 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 sdo_op, arg_type = lookup_info
# Ensuring that a tuple _value_ was passed in from the user # 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]))) raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
if lookup_type == 'relate': if lookup_type == 'relate':
# The SDORelate class handles construction for these queries, and verifies # The SDORelate class handles construction for these queries,
# the mask argument. # and verifies the mask argument.
return sdo_op(value[1]).as_sql(table_prefix, field_name) return sdo_op(value[1]).as_sql(geo_col)
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)
else: 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: else:
# Lookup info is a SDOOperation instance, whos `as_sql` method returns # Lookup info is a SDOOperation instance, whos `as_sql` method returns
# the SQL necessary for the geometry function call. For example: # the SQL necessary for the geometry function call. For example:
# SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE' # 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 # Handling 'isnull' lookup type
if lookup_type == 'isnull': return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) 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: try:
# Trying to create the database first. # Trying to create the database first.
cursor.execute(create_sql) cursor.execute(create_sql)
#print create_sql
except Exception, e: except Exception, e:
# Drop and recreate, if necessary. # Drop and recreate, if necessary.
if not autoclobber: if not autoclobber:
@ -56,6 +57,7 @@ def _create_with_cursor(db_name, verbosity=1, autoclobber=False):
cursor.execute(create_sql) cursor.execute(create_sql)
else: else:
raise Exception('Spatial Database Creation canceled.') raise Exception('Spatial Database Creation canceled.')
foo = _create_with_cursor
created_regex = re.compile(r'^createdb: database creation failed: ERROR: database ".+" already exists') created_regex = re.compile(r'^createdb: database creation failed: ERROR: database ".+" already exists')
def _create_with_shell(db_name, verbosity=1, autoclobber=False): def _create_with_shell(db_name, verbosity=1, autoclobber=False):

View File

@ -1,11 +1,11 @@
from types import UnicodeType from types import UnicodeType
from django.db import connection from django.db import connection
from django.db.models.fields import Field # Django base Field class from django.db.models.fields import Field # Django base Field class
from django.contrib.gis.geos import GEOSGeometry, GEOSException 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.postgis.adaptor import PostGISAdaptor from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
from django.contrib.gis.db.backend.postgis.query import \ from django.contrib.gis.db.backend.postgis.query import \
DISTANCE, DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
# Quotename & geographic quotename, respectively # Quotename & geographic quotename, respectively
qn = connection.ops.quote_name 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 The 'geometry_columns' table from the PostGIS. See the PostGIS
documentation at Ch. 4.2.2. documentation at Ch. 4.2.2.
""" """
f_table_catalog = models.CharField(maxlength=256) f_table_catalog = models.CharField(max_length=256)
f_table_schema = models.CharField(maxlength=256) f_table_schema = models.CharField(max_length=256)
f_table_name = models.CharField(maxlength=256) f_table_name = models.CharField(max_length=256)
f_geometry_column = models.CharField(maxlength=256) f_geometry_column = models.CharField(max_length=256)
coord_dimension = models.IntegerField() coord_dimension = models.IntegerField()
srid = models.IntegerField(primary_key=True) srid = models.IntegerField(primary_key=True)
type = models.CharField(maxlength=30) type = models.CharField(max_length=30)
class Meta: class Meta:
db_table = 'geometry_columns' db_table = 'geometry_columns'
@ -41,10 +41,10 @@ class SpatialRefSys(models.Model, SpatialRefSysMixin):
documentaiton at Ch. 4.2.1. documentaiton at Ch. 4.2.1.
""" """
srid = models.IntegerField(primary_key=True) srid = models.IntegerField(primary_key=True)
auth_name = models.CharField(maxlength=256) auth_name = models.CharField(max_length=256)
auth_srid = models.IntegerField() auth_srid = models.IntegerField()
srtext = models.CharField(maxlength=2048) srtext = models.CharField(max_length=2048)
proj4text = models.CharField(maxlength=2048) proj4text = models.CharField(max_length=2048)
class Meta: class Meta:
db_table = 'spatial_ref_sys' db_table = 'spatial_ref_sys'

View File

@ -2,10 +2,12 @@
This module contains the spatial lookup types, and the get_geo_where_clause() This module contains the spatial lookup types, and the get_geo_where_clause()
routine for PostGIS. routine for PostGIS.
""" """
import re
from decimal import Decimal from decimal import Decimal
from django.db import connection from django.db import connection
from django.contrib.gis.measure import Distance 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.postgis.management import postgis_version_tuple
from django.contrib.gis.db.backend.util import SpatialOperation, SpatialFunction
qn = connection.ops.quote_name qn = connection.ops.quote_name
# Getting the PostGIS version information # 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): if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1):
raise Exception('PostGIS version %s not supported.' % POSTGIS_VERSION) 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 # 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. # means that 'ST_' is prefixes geometry function names.
GEOM_FUNC_PREFIX = '' GEOM_FUNC_PREFIX = ''
if MAJOR_VERSION >= 1: if MAJOR_VERSION >= 1:
if (MINOR_VERSION1 > 2 or if (MINOR_VERSION1 > 2 or
(MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)): (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)):
GEOM_FUNC_PREFIX = 'ST_' GEOM_FUNC_PREFIX = 'ST_'
def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func) 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 # Functions used by the GeoManager & GeoQuerySet
ASKML = get_func('AsKML') ASKML = get_func('AsKML')
ASGML = get_func('AsGML') ASGML = get_func('AsGML')
@ -90,48 +55,115 @@ if MAJOR_VERSION >= 1:
else: else:
raise NotImplementedError('PostGIS versions < 1.0 are not supported.') 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 # 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 # first before calling the more computationally expensive GEOS routines (called
# "inline index magic"): # "inline index magic"):
# 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and # 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and
# 'covers'. # 'covers'.
POSTGIS_GEOMETRY_FUNCTIONS = { POSTGIS_GEOMETRY_FUNCTIONS = {
'equals' : 'Equals', 'equals' : PostGISFunction('Equals'),
'disjoint' : 'Disjoint', 'disjoint' : PostGISFunction('Disjoint'),
'touches' : 'Touches', 'touches' : PostGISFunction('Touches'),
'crosses' : 'Crosses', 'crosses' : PostGISFunction('Crosses'),
'within' : 'Within', 'within' : PostGISFunction('Within'),
'overlaps' : 'Overlaps', 'overlaps' : PostGISFunction('Overlaps'),
'contains' : 'Contains', 'contains' : PostGISFunction('Contains'),
'intersects' : 'Intersects', 'intersects' : PostGISFunction('Intersects'),
'relate' : ('Relate', basestring), 'relate' : (PostGISRelate, basestring),
} }
# Valid distance types and substitutions # Valid distance types and substitutions
dtypes = (Decimal, Distance, float, int) dtypes = (Decimal, Distance, float, int)
DISTANCE_FUNCTIONS = { DISTANCE_FUNCTIONS = {
'distance_gt' : ('>', dtypes), 'distance_gt' : (PostGISDistance('>'), dtypes),
'distance_gte' : ('>=', dtypes), 'distance_gte' : (PostGISDistance('>='), dtypes),
'distance_lt' : ('<', dtypes), 'distance_lt' : (PostGISDistance('<'), dtypes),
'distance_lte' : ('<=', dtypes), 'distance_lte' : (PostGISDistance('<='), dtypes),
} }
if GEOM_FUNC_PREFIX == 'ST_': 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+ # The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
POSTGIS_GEOMETRY_FUNCTIONS.update( POSTGIS_GEOMETRY_FUNCTIONS.update(
{'dwithin' : ('ST_DWithin', dtypes), {'dwithin' : (PostGISFunctionParam('DWithin'), dtypes),
'coveredby' : 'ST_CoveredBy', 'coveredby' : PostGISFunction('CoveredBy'),
'covers' : 'ST_Covers', 'covers' : PostGISFunction('Covers'),
} })
)
# Distance functions are a part of PostGIS geometry functions.
POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS) POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
# Any other lookup types that do not require a mapping. # 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 += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
### PostGIS-specific Methods ### #### The `get_geo_where_clause` function for PostGIS. ####
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
def get_geo_where_clause(lookup_type, table_prefix, field_name, value): def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
"Returns the SQL WHERE clause for use in PostGIS SQL construction." "Returns the SQL WHERE clause for use in PostGIS SQL construction."
if table_prefix.endswith('.'): # Getting the quoted field as `geo_col`.
table_prefix = qn(table_prefix[:-1])+'.' geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
field_name = qn(field_name)
# See if a PostGIS operator matches the lookup type first
if lookup_type in POSTGIS_OPERATORS: 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 # Lookup types that are tuples take tuple arguments, e.g., 'relate' and
# 'dwithin' lookup types. # distance lookups.
if isinstance(lookup_info, tuple): if isinstance(tmp, tuple):
# First element of tuple is lookup type, second element is the type # First element of tuple is the PostGISOperation instance, and the
# of the expected argument (e.g., str, float) # second element is either the type or a tuple of acceptable types
func, arg_type = lookup_info # 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 # Ensuring that a tuple _value_ was passed in from the user
if not isinstance(value, tuple): 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): if not isinstance(value[1], arg_type):
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1]))) raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
if lookup_type in DISTANCE_FUNCTIONS: # For lookup type `relate`, the op instance is not yet created (has
op = DISTANCE_FUNCTIONS[lookup_type][0] # to be instantiated here to check the pattern parameter).
return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op) if lookup_type == 'relate': op = op(value[1])
else:
return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name)
else: else:
# Returning the SQL necessary for the geometry function call. For example: op = tmp
# ST_Contains("geoapp_country"."poly", ST_GeomFromWKB(..)) # Calling the `as_sql` function on the operation instance.
return '%s(%s%s, %%s)' % (lookup_info, table_prefix, field_name) return op.as_sql(geo_col)
elif lookup_type == 'isnull':
# Handling 'isnull' lookup type # Handling 'isnull' lookup type
if lookup_type == 'isnull': return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) 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 self.params = params
def __str__(self): 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 Base class for generating spatial SQL.
and that of the given geometry.
""" """
if geom.srid is None or (geom.srid == -1 and field._srid != -1): def __init__(self, function='', operator='', result='', beg_subst='', end_subst=''):
return field._srid self.function = function
else: self.operator = operator
return geom.srid 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 from django.contrib.gis.db.models.fields import GeometryField
# parse_lookup depends on the spatial database backend. # parse_lookup depends on the spatial database backend.
from django.contrib.gis.db.backend import parse_lookup, \ 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 from django.contrib.gis.geos import GEOSGeometry
# Flag indicating whether the backend is Oracle. # Shortcut booleans for determining the backend.
oracle = SPATIAL_BACKEND == 'oracle' oracle = SPATIAL_BACKEND == 'oracle'
postgis = SPATIAL_BACKEND == 'postgis'
class GeoQ(Q): class GeoQ(Q):
"Geographical query encapsulation object." "Geographical query encapsulation object."
@ -325,8 +326,14 @@ class GeoQuerySet(QuerySet):
if oracle: if oracle:
gml_select = {'gml':'%s(%s)' % (ASGML, field_col)} gml_select = {'gml':'%s(%s)' % (ASGML, field_col)}
else: elif postgis:
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)} # 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. # Adding GML function call to SELECT part of the SQL.
return self.extra(select=gml_select) return self.extra(select=gml_select)