1
from itertools import izip
2
from django.db.models.query import sql
3
from django.db.models.fields import FieldDoesNotExist
4
from django.db.models.fields.related import ForeignKey
6
from django.contrib.gis.db.backend import SpatialBackend
7
from django.contrib.gis.db.models.fields import GeometryField
8
from django.contrib.gis.db.models.sql.where import GeoWhereNode
9
from django.contrib.gis.measure import Area, Distance
11
# Valid GIS query types.
12
ALL_TERMS = sql.constants.QUERY_TERMS.copy()
13
ALL_TERMS.update(SpatialBackend.gis_terms)
15
class GeoQuery(sql.Query):
17
A single spatial SQL query.
19
# Overridding the valid query terms.
20
query_terms = ALL_TERMS
22
#### Methods overridden from the base Query class ####
23
def __init__(self, model, conn):
24
super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode)
25
# The following attributes are customized for the GeoQuerySet.
26
# The GeoWhereNode and SpatialBackend classes contain backend-specific
27
# routines and functions.
28
self.aggregate = False
29
self.custom_select = {}
30
self.transformed_srid = None
31
self.extra_select_fields = {}
33
def clone(self, *args, **kwargs):
34
obj = super(GeoQuery, self).clone(*args, **kwargs)
35
# Customized selection dictionary and transformed srid flag have
36
# to also be added to obj.
37
obj.aggregate = self.aggregate
38
obj.custom_select = self.custom_select.copy()
39
obj.transformed_srid = self.transformed_srid
40
obj.extra_select_fields = self.extra_select_fields.copy()
43
def get_columns(self, with_aliases=False):
45
Return the list of columns to use in the select statement. If no
46
columns have been specified, returns all columns relating to fields in
49
If 'with_aliases' is true, any column names that are duplicated
50
(without the table names) are given unique aliases. This is needed in
51
some cases to avoid ambiguitity with nested queries.
53
This routine is overridden from Query to handle customized selection of
56
qn = self.quote_name_unless_alias
57
qn2 = self.connection.ops.quote_name
58
result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias))
59
for alias, col in self.extra_select.iteritems()]
60
aliases = set(self.extra_select.keys())
62
col_aliases = aliases.copy()
66
# This loop customized for GeoQuery.
67
for col, field in izip(self.select, self.select_fields):
68
if isinstance(col, (list, tuple)):
69
r = self.get_field_select(field, col[0])
70
if with_aliases and col[1] in col_aliases:
71
c_alias = 'Col%d' % len(col_aliases)
72
result.append('%s AS %s' % (r, c_alias))
74
col_aliases.add(c_alias)
78
col_aliases.add(col[1])
80
result.append(col.as_sql(quote_func=qn))
81
if hasattr(col, 'alias'):
82
aliases.add(col.alias)
83
col_aliases.add(col.alias)
84
elif self.default_cols:
85
cols, new_aliases = self.get_default_columns(with_aliases,
88
aliases.update(new_aliases)
89
# This loop customized for GeoQuery.
90
if not self.aggregate:
91
for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
92
r = self.get_field_select(field, table)
93
if with_aliases and col in col_aliases:
94
c_alias = 'Col%d' % len(col_aliases)
95
result.append('%s AS %s' % (r, c_alias))
97
col_aliases.add(c_alias)
103
self._select_aliases = aliases
106
def get_default_columns(self, with_aliases=False, col_aliases=None,
107
start_alias=None, opts=None, as_pairs=False):
109
Computes the default columns for selecting every field in the base
112
Returns a list of strings, quoted appropriately for use in SQL
113
directly, as well as a set of aliases used in the select statement.
115
This routine is overridden from Query to handle customized selection of
120
opts = self.model._meta
122
table_alias = start_alias
124
table_alias = self.tables[0]
125
root_pk = self.model._meta.pk.column
126
seen = {None: table_alias}
128
for field, model in opts.get_fields_with_model():
132
alias = self.join((table_alias, model._meta.db_table,
133
root_pk, model._meta.pk.column))
136
result.append((alias, field.column))
138
# This part of the function is customized for GeoQuery. We
139
# see if there was any custom selection specified in the
140
# dictionary, and set up the selection format appropriately.
141
field_sel = self.get_field_select(field, alias)
142
if with_aliases and field.column in col_aliases:
143
c_alias = 'Col%d' % len(col_aliases)
144
result.append('%s AS %s' % (field_sel, c_alias))
145
col_aliases.add(c_alias)
152
col_aliases.add(field.column)
155
return result, aliases
157
def get_ordering(self):
159
This routine is overridden to disable ordering for aggregate
162
if not self.aggregate:
163
return super(GeoQuery, self).get_ordering()
167
def resolve_columns(self, row, fields=()):
169
This routine is necessary so that distances and geometries returned
170
from extra selection SQL get resolved appropriately into Python
174
aliases = self.extra_select.keys()
175
index_start = len(aliases)
176
values = [self.convert_values(v, self.extra_select_fields.get(a, None))
177
for v, a in izip(row[:index_start], aliases)]
178
if SpatialBackend.oracle:
179
# This is what happens normally in Oracle's `resolve_columns`.
180
for value, field in izip(row[index_start:], fields):
181
values.append(self.convert_values(value, field))
183
values.extend(row[index_start:])
186
def convert_values(self, value, field):
188
Using the same routines that Oracle does we can convert our
189
extra selection objects into Geometry and Distance objects.
192
if SpatialBackend.oracle:
193
# Running through Oracle's first.
194
value = super(GeoQuery, self).convert_values(value, field)
195
if isinstance(field, DistanceField):
196
# Using the field's distance attribute, can instantiate
197
# `Distance` with the right context.
198
value = Distance(**{field.distance_att : value})
199
elif isinstance(field, AreaField):
200
value = Area(**{field.area_att : value})
201
elif isinstance(field, GeomField):
202
value = SpatialBackend.Geometry(value)
205
#### Routines unique to GeoQuery ####
206
def get_extra_select_format(self, alias):
208
if alias in self.custom_select:
209
sel_fmt = sel_fmt % self.custom_select[alias]
212
def get_field_select(self, fld, alias=None):
214
Returns the SELECT SQL string for the given field. Figures out
215
if any custom selection SQL is needed for the column The `alias`
216
keyword may be used to manually specify the database table where
217
the column exists, if not in the model associated with this
220
sel_fmt = self.get_select_format(fld)
221
if fld in self.custom_select:
222
field_sel = sel_fmt % self.custom_select[fld]
224
field_sel = sel_fmt % self._field_column(fld, alias)
227
def get_select_format(self, fld):
229
Returns the selection format string, depending on the requirements
230
of the spatial backend. For example, Oracle and MySQL require custom
231
selection formats in order to retrieve geometries in OGC WKT. For all
232
other fields a simple '%s' format string is returned.
234
if SpatialBackend.select and hasattr(fld, '_geom'):
235
# This allows operations to be done on fields in the SELECT,
236
# overriding their values -- used by the Oracle and MySQL
237
# spatial backends to get database values as WKT, and by the
238
# `transform` method.
239
sel_fmt = SpatialBackend.select
241
# Because WKT doesn't contain spatial reference information,
242
# the SRID is prefixed to the returned WKT to ensure that the
243
# transformed geometries have an SRID different than that of the
244
# field -- this is only used by `transform` for Oracle backends.
245
if self.transformed_srid and SpatialBackend.oracle:
246
sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt)
251
# Private API utilities, subject to change.
252
def _check_geo_field(self, model, name_param):
254
Recursive utility routine for checking the given name parameter
255
on the given model. Initially, the name parameter is a string,
256
of the field on the given model e.g., 'point', 'the_geom'.
257
Related model field strings like 'address__point', may also be
260
If a GeometryField exists according to the given name parameter
261
it will be returned, otherwise returns False.
263
if isinstance(name_param, basestring):
264
# This takes into account the situation where the name is a
265
# lookup to a related geographic field, e.g., 'address__point'.
266
name_param = name_param.split(sql.constants.LOOKUP_SEP)
267
name_param.reverse() # Reversing so list operates like a queue of related lookups.
268
elif not isinstance(name_param, list):
271
# Getting the name of the field for the model (by popping the first
272
# name from the `name_param` list created above).
273
fld, mod, direct, m2m = model._meta.get_field_by_name(name_param.pop())
274
except (FieldDoesNotExist, IndexError):
276
# TODO: ManyToManyField?
277
if isinstance(fld, GeometryField):
279
elif isinstance(fld, ForeignKey):
280
# ForeignKey encountered, return the output of this utility called
281
# on the _related_ model with the remaining name parameters.
282
return self._check_geo_field(fld.rel.to, name_param) # Recurse to check ForeignKey relation.
286
def _field_column(self, field, table_alias=None):
288
Helper function that returns the database column for the given field.
289
The table and column are returned (quoted) in the proper format, e.g.,
290
`"geoapp_city"."point"`. If `table_alias` is not specified, the
291
database table associated with the model of this `GeoQuery` will be
294
if table_alias is None: table_alias = self.model._meta.db_table
295
return "%s.%s" % (self.quote_name_unless_alias(table_alias),
296
self.connection.ops.quote_name(field.column))
298
def _geo_field(self, field_name=None):
300
Returns the first Geometry field encountered; or specified via the
301
`field_name` keyword. The `field_name` may be a string specifying
302
the geometry field on this GeoQuery's model, or a lookup string
303
to a geometry field via a ForeignKey relation.
305
if field_name is None:
306
# Incrementing until the first geographic field is found.
307
for fld in self.model._meta.fields:
308
if isinstance(fld, GeometryField): return fld
311
# Otherwise, check by the given field name -- which may be
312
# a lookup to a _related_ geographic field.
313
return self._check_geo_field(self.model, field_name)
315
### Field Classes for `convert_values` ####
316
class AreaField(object):
317
def __init__(self, area_att):
318
self.area_att = area_att
320
class DistanceField(object):
321
def __init__(self, distance_att):
322
self.distance_att = distance_att
324
# Rather than use GeometryField (which requires a SQL query
325
# upon instantiation), use this lighter weight class.
326
class GeomField(object):