| from itertools import izip |
| from django.db.models.query import sql |
| from django.db.models.fields import FieldDoesNotExist |
| from django.db.models.fields.related import ForeignKey |
| |
| from django.contrib.gis.db.backend import SpatialBackend |
| from django.contrib.gis.db.models.fields import GeometryField |
| from django.contrib.gis.db.models.sql.where import GeoWhereNode |
| from django.contrib.gis.measure import Area, Distance |
| |
| # Valid GIS query types. |
| ALL_TERMS = sql.constants.QUERY_TERMS.copy() |
| ALL_TERMS.update(SpatialBackend.gis_terms) |
| |
| class GeoQuery(sql.Query): |
| """ |
| A single spatial SQL query. |
| """ |
| # Overridding the valid query terms. |
| query_terms = ALL_TERMS |
| |
| #### Methods overridden from the base Query class #### |
| def __init__(self, model, conn): |
| super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode) |
| # The following attributes are customized for the GeoQuerySet. |
| # The GeoWhereNode and SpatialBackend classes contain backend-specific |
| # routines and functions. |
| self.aggregate = False |
| self.custom_select = {} |
| self.transformed_srid = None |
| self.extra_select_fields = {} |
| |
| def clone(self, *args, **kwargs): |
| obj = super(GeoQuery, self).clone(*args, **kwargs) |
| # Customized selection dictionary and transformed srid flag have |
| # to also be added to obj. |
| obj.aggregate = self.aggregate |
| obj.custom_select = self.custom_select.copy() |
| obj.transformed_srid = self.transformed_srid |
| obj.extra_select_fields = self.extra_select_fields.copy() |
| return obj |
| |
| def get_columns(self, with_aliases=False): |
| """ |
| Return the list of columns to use in the select statement. If no |
| columns have been specified, returns all columns relating to fields in |
| the model. |
| |
| If 'with_aliases' is true, any column names that are duplicated |
| (without the table names) are given unique aliases. This is needed in |
| some cases to avoid ambiguitity with nested queries. |
| |
| This routine is overridden from Query to handle customized selection of |
| geometry columns. |
| """ |
| qn = self.quote_name_unless_alias |
| qn2 = self.connection.ops.quote_name |
| result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias)) |
| for alias, col in self.extra_select.iteritems()] |
| aliases = set(self.extra_select.keys()) |
| if with_aliases: |
| col_aliases = aliases.copy() |
| else: |
| col_aliases = set() |
| if self.select: |
| # This loop customized for GeoQuery. |
| for col, field in izip(self.select, self.select_fields): |
| if isinstance(col, (list, tuple)): |
| r = self.get_field_select(field, col[0]) |
| if with_aliases and col[1] in col_aliases: |
| c_alias = 'Col%d' % len(col_aliases) |
| result.append('%s AS %s' % (r, c_alias)) |
| aliases.add(c_alias) |
| col_aliases.add(c_alias) |
| else: |
| result.append(r) |
| aliases.add(r) |
| col_aliases.add(col[1]) |
| else: |
| result.append(col.as_sql(quote_func=qn)) |
| if hasattr(col, 'alias'): |
| aliases.add(col.alias) |
| col_aliases.add(col.alias) |
| elif self.default_cols: |
| cols, new_aliases = self.get_default_columns(with_aliases, |
| col_aliases) |
| result.extend(cols) |
| aliases.update(new_aliases) |
| # This loop customized for GeoQuery. |
| if not self.aggregate: |
| for (table, col), field in izip(self.related_select_cols, self.related_select_fields): |
| r = self.get_field_select(field, table) |
| if with_aliases and col in col_aliases: |
| c_alias = 'Col%d' % len(col_aliases) |
| result.append('%s AS %s' % (r, c_alias)) |
| aliases.add(c_alias) |
| col_aliases.add(c_alias) |
| else: |
| result.append(r) |
| aliases.add(r) |
| col_aliases.add(col) |
| |
| self._select_aliases = aliases |
| return result |
| |
| def get_default_columns(self, with_aliases=False, col_aliases=None, |
| start_alias=None, opts=None, as_pairs=False): |
| """ |
| Computes the default columns for selecting every field in the base |
| model. |
| |
| Returns a list of strings, quoted appropriately for use in SQL |
| directly, as well as a set of aliases used in the select statement. |
| |
| This routine is overridden from Query to handle customized selection of |
| geometry columns. |
| """ |
| result = [] |
| if opts is None: |
| opts = self.model._meta |
| if start_alias: |
| table_alias = start_alias |
| else: |
| table_alias = self.tables[0] |
| root_pk = self.model._meta.pk.column |
| seen = {None: table_alias} |
| aliases = set() |
| for field, model in opts.get_fields_with_model(): |
| try: |
| alias = seen[model] |
| except KeyError: |
| alias = self.join((table_alias, model._meta.db_table, |
| root_pk, model._meta.pk.column)) |
| seen[model] = alias |
| if as_pairs: |
| result.append((alias, field.column)) |
| continue |
| # This part of the function is customized for GeoQuery. We |
| # see if there was any custom selection specified in the |
| # dictionary, and set up the selection format appropriately. |
| field_sel = self.get_field_select(field, alias) |
| if with_aliases and field.column in col_aliases: |
| c_alias = 'Col%d' % len(col_aliases) |
| result.append('%s AS %s' % (field_sel, c_alias)) |
| col_aliases.add(c_alias) |
| aliases.add(c_alias) |
| else: |
| r = field_sel |
| result.append(r) |
| aliases.add(r) |
| if with_aliases: |
| col_aliases.add(field.column) |
| if as_pairs: |
| return result, None |
| return result, aliases |
| |
| def get_ordering(self): |
| """ |
| This routine is overridden to disable ordering for aggregate |
| spatial queries. |
| """ |
| if not self.aggregate: |
| return super(GeoQuery, self).get_ordering() |
| else: |
| return () |
| |
| def resolve_columns(self, row, fields=()): |
| """ |
| This routine is necessary so that distances and geometries returned |
| from extra selection SQL get resolved appropriately into Python |
| objects. |
| """ |
| values = [] |
| aliases = self.extra_select.keys() |
| index_start = len(aliases) |
| values = [self.convert_values(v, self.extra_select_fields.get(a, None)) |
| for v, a in izip(row[:index_start], aliases)] |
| if SpatialBackend.oracle: |
| # This is what happens normally in Oracle's `resolve_columns`. |
| for value, field in izip(row[index_start:], fields): |
| values.append(self.convert_values(value, field)) |
| else: |
| values.extend(row[index_start:]) |
| return values |
| |
| def convert_values(self, value, field): |
| """ |
| Using the same routines that Oracle does we can convert our |
| extra selection objects into Geometry and Distance objects. |
| TODO: Laziness. |
| """ |
| if SpatialBackend.oracle: |
| # Running through Oracle's first. |
| value = super(GeoQuery, self).convert_values(value, field) |
| if isinstance(field, DistanceField): |
| # Using the field's distance attribute, can instantiate |
| # `Distance` with the right context. |
| value = Distance(**{field.distance_att : value}) |
| elif isinstance(field, AreaField): |
| value = Area(**{field.area_att : value}) |
| elif isinstance(field, GeomField): |
| value = SpatialBackend.Geometry(value) |
| return value |
| |
| #### Routines unique to GeoQuery #### |
| def get_extra_select_format(self, alias): |
| sel_fmt = '%s' |
| if alias in self.custom_select: |
| sel_fmt = sel_fmt % self.custom_select[alias] |
| return sel_fmt |
| |
| def get_field_select(self, fld, alias=None): |
| """ |
| Returns the SELECT SQL string for the given field. Figures out |
| if any custom selection SQL is needed for the column The `alias` |
| keyword may be used to manually specify the database table where |
| the column exists, if not in the model associated with this |
| `GeoQuery`. |
| """ |
| sel_fmt = self.get_select_format(fld) |
| if fld in self.custom_select: |
| field_sel = sel_fmt % self.custom_select[fld] |
| else: |
| field_sel = sel_fmt % self._field_column(fld, alias) |
| return field_sel |
| |
| def get_select_format(self, fld): |
| """ |
| Returns the selection format string, depending on the requirements |
| of the spatial backend. For example, Oracle and MySQL require custom |
| selection formats in order to retrieve geometries in OGC WKT. For all |
| other fields a simple '%s' format string is returned. |
| """ |
| if SpatialBackend.select and hasattr(fld, '_geom'): |
| # This allows operations to be done on fields in the SELECT, |
| # overriding their values -- used by the Oracle and MySQL |
| # spatial backends to get database values as WKT, and by the |
| # `transform` method. |
| sel_fmt = SpatialBackend.select |
| |
| # Because WKT doesn't contain spatial reference information, |
| # the SRID is prefixed to the returned WKT to ensure that the |
| # transformed geometries have an SRID different than that of the |
| # field -- this is only used by `transform` for Oracle backends. |
| if self.transformed_srid and SpatialBackend.oracle: |
| sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt) |
| else: |
| sel_fmt = '%s' |
| return sel_fmt |
| |
| # Private API utilities, subject to change. |
| def _check_geo_field(self, model, name_param): |
| """ |
| Recursive utility routine for checking the given name parameter |
| on the given model. Initially, the name parameter is a string, |
| of the field on the given model e.g., 'point', 'the_geom'. |
| Related model field strings like 'address__point', may also be |
| used. |
| |
| If a GeometryField exists according to the given name parameter |
| it will be returned, otherwise returns False. |
| """ |
| if isinstance(name_param, basestring): |
| # This takes into account the situation where the name is a |
| # lookup to a related geographic field, e.g., 'address__point'. |
| name_param = name_param.split(sql.constants.LOOKUP_SEP) |
| name_param.reverse() # Reversing so list operates like a queue of related lookups. |
| elif not isinstance(name_param, list): |
| raise TypeError |
| try: |
| # Getting the name of the field for the model (by popping the first |
| # name from the `name_param` list created above). |
| fld, mod, direct, m2m = model._meta.get_field_by_name(name_param.pop()) |
| except (FieldDoesNotExist, IndexError): |
| return False |
| # TODO: ManyToManyField? |
| if isinstance(fld, GeometryField): |
| return fld # A-OK. |
| elif isinstance(fld, ForeignKey): |
| # ForeignKey encountered, return the output of this utility called |
| # on the _related_ model with the remaining name parameters. |
| return self._check_geo_field(fld.rel.to, name_param) # Recurse to check ForeignKey relation. |
| else: |
| return False |
| |
| def _field_column(self, field, table_alias=None): |
| """ |
| Helper function that returns the database column for the given field. |
| The table and column are returned (quoted) in the proper format, e.g., |
| `"geoapp_city"."point"`. If `table_alias` is not specified, the |
| database table associated with the model of this `GeoQuery` will be |
| used. |
| """ |
| if table_alias is None: table_alias = self.model._meta.db_table |
| return "%s.%s" % (self.quote_name_unless_alias(table_alias), |
| self.connection.ops.quote_name(field.column)) |
| |
| def _geo_field(self, field_name=None): |
| """ |
| Returns the first Geometry field encountered; or specified via the |
| `field_name` keyword. The `field_name` may be a string specifying |
| the geometry field on this GeoQuery's model, or a lookup string |
| to a geometry field via a ForeignKey relation. |
| """ |
| if field_name is None: |
| # Incrementing until the first geographic field is found. |
| for fld in self.model._meta.fields: |
| if isinstance(fld, GeometryField): return fld |
| return False |
| else: |
| # Otherwise, check by the given field name -- which may be |
| # a lookup to a _related_ geographic field. |
| return self._check_geo_field(self.model, field_name) |
| |
| ### Field Classes for `convert_values` #### |
| class AreaField(object): |
| def __init__(self, area_att): |
| self.area_att = area_att |
| |
| class DistanceField(object): |
| def __init__(self, distance_att): |
| self.distance_att = distance_att |
| |
| # Rather than use GeometryField (which requires a SQL query |
| # upon instantiation), use this lighter weight class. |
| class GeomField(object): |
| pass |