From 8ef0b01e721f47fc1a9489dec6d0a54010defa1d Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Sat, 31 Mar 2007 21:25:29 +0000 Subject: [PATCH] gis: two big changes: (1) the addition of the GeoMixin class, which gives geometry fields contributed functions (e.g. get_GEOM_area). (2) geo_filter() is no more, all queries use filter() now. git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@4884 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/gis/db/models/GeoMixin.py | 25 +++++++++++++++++++ django/contrib/gis/db/models/__init__.py | 4 ++- .../contrib/gis/db/models/fields/__init__.py | 23 ++++++++--------- django/contrib/gis/db/models/manager.py | 7 +----- django/contrib/gis/db/models/postgis.py | 17 +++++++------ django/contrib/gis/db/models/query.py | 21 ++++++++-------- 6 files changed, 59 insertions(+), 38 deletions(-) create mode 100644 django/contrib/gis/db/models/GeoMixin.py diff --git a/django/contrib/gis/db/models/GeoMixin.py b/django/contrib/gis/db/models/GeoMixin.py new file mode 100644 index 0000000000..f0accb69b4 --- /dev/null +++ b/django/contrib/gis/db/models/GeoMixin.py @@ -0,0 +1,25 @@ +# GEOS Routines +from django.contrib.gis.geos import hex_to_wkt, centroid, area + +# Until model subclassing is a possibility, a mixin class is used to add +# the necessary functions that may be contributed for geographic objects. +class GeoMixin: + "The Geographic Mixin class, provides routines for geographic objects." + + # A subclass of Model is specifically needed so that these geographic + # routines are present for instantiations of the models. + def _get_GEOM_wkt(self, field): + "Gets the WKT of the geometry." + hex = getattr(self, field.attname) + return hex_to_wkt(hex) + + def _get_GEOM_centroid(self, field): + "Gets the centroid of the geometry, in WKT." + hex = getattr(self, field.attname) + return centroid(hex) + + def _get_GEOM_area(self, field): + hex = getattr(self, field.attname) + return area(hex) + + diff --git a/django/contrib/gis/db/models/__init__.py b/django/contrib/gis/db/models/__init__.py index 5117ac60f6..89d17d6dbd 100644 --- a/django/contrib/gis/db/models/__init__.py +++ b/django/contrib/gis/db/models/__init__.py @@ -1,7 +1,7 @@ # Want to get everything from the 'normal' models package. from django.db.models import * -# The GeoManager class. +# The GeoManager from django.contrib.gis.db.models.manager import GeoManager # The various PostGIS/OpenGIS enabled fields. @@ -10,3 +10,5 @@ from django.contrib.gis.db.models.fields import \ MultiPointField, MultiLineStringField, MultiPolygonField, \ GeometryCollectionField +# The geographic mixin class. +from GeoMixin import GeoMixin diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py index 50c8bb6ef0..0ff5a3ef8a 100644 --- a/django/contrib/gis/db/models/fields/__init__.py +++ b/django/contrib/gis/db/models/fields/__init__.py @@ -1,8 +1,8 @@ # The Django base Field class. from django.db.models.fields import Field -from django.oldforms import LargeTextField from django.contrib.gis.db.models.postgis import POSTGIS_TERMS -from geos import geomToWKT, geomFromHEX +from django.contrib.gis.oldforms import WKTField +from django.utils.functional import curry #TODO: add db.quotename. @@ -35,15 +35,6 @@ def _geom_index(geom, style, model, field, style.SQL_KEYWORD(index_opts) + ' );' return sql -class WKTField(LargeTextField): - "An oldforms LargeTextField for editing WKT text in the admin." - - def render(self, data): - # PostGIS uses EWKBHEX to store its values internally, converting - # to WKT for the admin first. - wkt = geomToWKT(geomFromHEX(data)) - return super(WKTField, self).render(wkt) - class GeometryField(Field): "The base GIS field -- maps to the OpenGIS Geometry type." @@ -65,6 +56,14 @@ class GeometryField(Field): super(GeometryField, self).__init__(**kwargs) + def contribute_to_class(self, cls, name): + super(GeometryField, self).contribute_to_class(cls, name) + + # Adding the WKT accessor function for geometry + setattr(cls, 'get_%s_wkt' % self.name, curry(cls._get_GEOM_wkt, field=self)) + setattr(cls, 'get_%s_centroid' % self.name, curry(cls._get_GEOM_centroid, field=self)) + setattr(cls, 'get_%s_area' % self.name, curry(cls._get_GEOM_area, field=self)) + def get_internal_type(self): return "NoField" @@ -98,7 +97,7 @@ class GeometryField(Field): def get_manipulator_field_objs(self): "Using the WKTField (defined above) to be our manipulator." return [WKTField] - + # The OpenGIS Geometry Type Fields class PointField(GeometryField): _geom = 'POINT' diff --git a/django/contrib/gis/db/models/manager.py b/django/contrib/gis/db/models/manager.py index 10c6ec7018..80a2ca49f0 100644 --- a/django/contrib/gis/db/models/manager.py +++ b/django/contrib/gis/db/models/manager.py @@ -2,12 +2,7 @@ from django.db.models.manager import Manager from django.contrib.gis.db.models.query import GeoQuerySet class GeoManager(Manager): + "Overrides Manager to return Geographic QuerySets." def get_query_set(self): return GeoQuerySet(model=self.model) - - def geo_filter(self, *args, **kwargs): - return self.get_query_set().geo_filter(*args, **kwargs) - - def geo_exclude(self, *args, **kwargs): - return self.get_query_set().geo_exclude(*args, **kwargs) diff --git a/django/contrib/gis/db/models/postgis.py b/django/contrib/gis/db/models/postgis.py index 34455dc85c..c19d690d4e 100644 --- a/django/contrib/gis/db/models/postgis.py +++ b/django/contrib/gis/db/models/postgis.py @@ -2,7 +2,7 @@ # django.db.models.query objects to be customized for PostGIS. from copy import copy from django.db import backend -from django.db.models.query import LOOKUP_SEPARATOR, find_field, FieldFound +from django.db.models.query import LOOKUP_SEPARATOR, find_field, FieldFound, QUERY_TERMS, get_where_clause from django.utils.datastructures import SortedDict # PostGIS-specific operators. The commented descriptions of these @@ -41,7 +41,6 @@ POSTGIS_GEOMETRY_FUNCTIONS = { 'distance' : 'Distance', 'equals' : 'Equals', 'disjoint' : 'Disjoint', - 'intersects' : 'Intersects', 'touches' : 'Touches', 'crosses' : 'Crosses', 'within' : 'Within', @@ -55,6 +54,7 @@ POSTGIS_GEOMETRY_FUNCTIONS = { # and the geometry functions. POSTGIS_TERMS = list(POSTGIS_OPERATORS.keys()) # Getting the operators first POSTGIS_TERMS.extend(list(POSTGIS_GEOMETRY_FUNCTIONS.keys())) # Adding on the Geometry Functions +POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable def get_geo_where_clause(lookup_type, table_prefix, field_name, value): if table_prefix.endswith('.'): @@ -72,17 +72,17 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value): return '%s(%s%s, %%s)' % (POSTGIS_GEOMETRY_FUNCTIONS[lookup_type], table_prefix, field_name) except KeyError: pass - + # For any other lookup type if lookup_type == 'isnull': return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or '')) raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type) -def geo_parse_lookup(kwarg_items, opts): +def parse_lookup(kwarg_items, opts): # Helper function that handles converting API kwargs # (e.g. "name__exact": "tom") to SQL. - # Returns a tuple of (tables, joins, where, params). + # Returns a tuple of (joins, where, params). # 'joins' is a sorted dictionary describing the tables that must be joined # to complete the query. The dictionary is sorted because creation order @@ -112,10 +112,11 @@ def geo_parse_lookup(kwarg_items, opts): # If there is only one part, or the last part is not a query # term, assume that the query is an __exact lookup_type = path.pop() + if lookup_type == 'pk': lookup_type = 'exact' path.append(None) - elif len(path) == 0 or lookup_type not in POSTGIS_TERMS: + elif len(path) == 0 or not ((lookup_type in QUERY_TERMS) or (lookup_type in POSTGIS_TERMS)): path.append(lookup_type) lookup_type = 'exact' @@ -200,6 +201,7 @@ def lookup_inner(path, lookup_type, value, opts, table, column): # Does the name belong to a one-to-one, many-to-one, or regular field? field = find_field(name, current_opts.fields, False) + if field: if field.rel: # One-to-One/Many-to-one field new_table = current_table + '__' + name @@ -293,8 +295,7 @@ def lookup_inner(path, lookup_type, value, opts, table, column): if hasattr(field, '_geom'): where.append(get_geo_where_clause(lookup_type, current_table + '.', column, value)) else: - raise TypeError, 'Field "%s" (%s) is not a Geometry Field.' % (column, field.__class__.__name__) + where.append(get_where_clause(lookup_type, current_table + '.', column, value)) params.extend(field.get_db_prep_lookup(lookup_type, value)) return joins, where, params - diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index c2a2e66677..4b2f274a43 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -1,25 +1,24 @@ -from django.db.models.query import * -from django.contrib.gis.db.models.postgis import geo_parse_lookup +from django.db.models.query import Q, QNot, QuerySet +from django.contrib.gis.db.models.postgis import parse_lookup +import operator class GeoQ(Q): "Geographical query encapsulation object." def get_sql(self, opts): - "Overloaded to use the geo_parse_lookup() function instead of parse_lookup()" - return geo_parse_lookup(self.kwargs.items(), opts) + "Overloaded to use our own parse_lookup() function." + return parse_lookup(self.kwargs.items(), opts) class GeoQuerySet(QuerySet): "Geographical-enabled QuerySet object." - def geo_filter(self, *args, **kwargs): - "Returns a new GeoQuerySet instance with the args ANDed to the existing set." - return self._geo_filter_or_exclude(None, *args, **kwargs) + def __init__(self, model=None): + super(GeoQuerySet, self).__init__(model=model) - def geo_exclude(self, *args, **kwargs): - "Returns a new GeoQuerySet instance with NOT (args) ANDed to the existing set." - return self._geo_filter_or_exclude(QNot, *args, **kwargs) + # We only want to use the GeoQ object for our queries + self._filters = GeoQ() - def _geo_filter_or_exclude(self, mapper, *args, **kwargs): + def _filter_or_exclude(self, mapper, *args, **kwargs): # mapper is a callable used to transform Q objects, # or None for identity transform if mapper is None: