1
0
mirror of https://github.com/django/django.git synced 2025-07-04 17:59:13 +00:00

gis: checked in latest from jbronn from Mar 19.

git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@4785 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jeremy Dunck 2007-03-23 16:32:00 +00:00
parent 68309c8a33
commit 8b279b63be
4 changed files with 351 additions and 71 deletions

View File

@ -1,71 +0,0 @@
from geos import geomFromWKT, geomToWKT
from decimal import Decimal
from django.db import models
from django.db.models.query import QuerySet
class GeoQuerySet(QuerySet):
# The list of valid query terms
# override the local QUERY_TERMS in the namespace
# not sure how to do that locals() hackery
# possibly in the init change its locals() variables
# not sure if that will work
QUERY_TERMS = (
'exact', 'iexact', 'contains', 'icontains', 'overlaps',
'gt', 'gte', 'lt', 'lte', 'in',
'startswith', 'istartswith', 'endswith', 'iendswith',
'range', 'year', 'month', 'day', 'isnull', 'search',
)
def dprint(arg):
import re
import inspect
print re.match("^\s*dprint\(\s*(.+)\s*\)", inspect.stack()[1][4][0]).group(1) + ": " + repr(arg)
class GeometryManager(models.Manager):
#def filter(self, *args, **kwargs):
# super(Manager, self).filter(*args, **kwargs)
# return self.get_query_set().filter(*args, **kwargs)
def get_query_set(self):
return GeoQuerySet(self.model)
class BoundingBox:
def _geom(self):
return geomToWKT(self._g)
geom = property(_geom)
def _area(self):
return self._g.area()
area = property(_area)
def __init__(self, ne, sw):
"""
Create a bounding box using two points
This points come from a JSON request, so they are strings
"""
ne = [Decimal(i.strip(' ')) for i in ne[1:-1].split(',')]
sw = [Decimal(i.strip(' ')) for i in sw[1:-1].split(',')]
ne_lat = ne[0]
ne_lng = ne[1]
sw_lat = sw[0]
sw_lng = sw[1]
bb = 'POLYGON(('
bb += str(ne_lng) + " " + str(ne_lat) + ","
bb += str(ne_lng) + " " + str(sw_lat) + ","
bb += str(sw_lng) + " " + str(sw_lat) + ","
bb += str(sw_lng) + " " + str(ne_lat) + ","
bb += str(ne_lng) + " " + str(ne_lat)
bb += '))'
self._g = geomFromWKT(bb)

View File

@ -0,0 +1,13 @@
from django.db.models.manager import Manager
from django.contrib.gis.db.models.query import GeoQuerySet
class GeoManager(Manager):
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)

View File

@ -0,0 +1,302 @@
# This module is meant to re-define the helper routines used by the
# 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, QUERY_TERMS, \
find_field, FieldFound, get_where_clause
from django.utils.datastructures import SortedDict
# 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.
'overlapsleft' : '&< %s',
# The "&>" operator returns true if A's bounding box overlaps or is to the right of B's bounding box.
'overlapsright' : '&> %s',
# The "<<" operator returns true if A's bounding box is strictly to the left of B's bounding box.
'left' : '<< %s',
# The ">>" operator returns true if A's bounding box is strictly to the right of B's bounding box.
'right' : '>> %s',
# The "&<|" operator returns true if A's bounding box overlaps or is below B's bounding box.
'overlapsbelow' : '&<| %s',
# The "|&>" operator returns true if A's bounding box overlaps or is above B's bounding box.
'overlapsabove' : '|&> %s',
# The "<<|" operator returns true if A's bounding box is strictly below B's bounding box.
'strictlybelow' : '<<| %s',
# The "|>>" operator returns true if A's bounding box is strictly above B's bounding box.
'strictlyabove' : '|>> %s',
# 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.
'sameas' : '~= %s',
# The "@" operator returns true if A's bounding box is completely contained by B's bounding box.
'contained' : '@ %s',
# The "~" operator returns true if A's bounding box completely contains B's bounding box.
'bbcontains' : '~ %s',
# The "&&" operator is the "overlaps" operator. If A's bounding boux overlaps B's bounding box the
# operator returns true.
'bboverlaps' : '&& %s',
}
# PostGIS Geometry Functions -- most of these use GEOS.
POSTGIS_GEOMETRY_FUNCTIONS = {
'distance' : 'Distance',
'equals' : 'Equals',
'disjoint' : 'Disjoint',
'intersects' : 'Intersects',
'touches' : 'Touches',
'crosses' : 'Crosses',
'within' : 'Within',
'overlaps' : 'Overlaps',
'contains' : 'Contains',
'intersects' : 'Intersects',
'relate' : 'Relate',
}
# These are the PostGIS-customized QUERY_TERMS, combines both the operators
# 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
def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
if table_prefix.endswith('.'):
table_prefix = backend.quote_name(table_prefix[:-1])+'.'
field_name = backend.quote_name(field_name)
# See if a PostGIS operator matches the lookup type first
try:
return '%s%s %s' % (table_prefix, field_name, (POSTGIS_OPERATORS[lookup_type] % '%s'))
except KeyError:
pass
# See if a PostGIS Geometry function matches the lookup type next
try:
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):
# Helper function that handles converting API kwargs
# (e.g. "name__exact": "tom") to SQL.
# Returns a tuple of (tables, 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
# is significant; it is a dictionary to ensure uniqueness of alias names.
#
# Each key-value pair follows the form
# alias: (table, join_type, condition)
# where
# alias is the AS alias for the joined table
# table is the actual table name to be joined
# join_type is the type of join (INNER JOIN, LEFT OUTER JOIN, etc)
# condition is the where-like statement over which narrows the join.
# alias will be derived from the lookup list name.
#
# At present, this method only every returns INNER JOINs; the option is
# there for others to implement custom Q()s, etc that return other join
# types.
joins, where, params = SortedDict(), [], []
for kwarg, value in kwarg_items:
path = kwarg.split(LOOKUP_SEPARATOR)
# Extract the last elements of the kwarg.
# The very-last is the lookup_type (equals, like, etc).
# The second-last is the table column on which the lookup_type is
# to be performed. If this name is 'pk', it will be substituted with
# the name of the primary key.
# 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:
path.append(lookup_type)
lookup_type = 'exact'
if len(path) < 1:
raise TypeError, "Cannot parse keyword query %r" % kwarg
if value is None:
# Interpret '__exact=None' as the sql '= NULL'; otherwise, reject
# all uses of None as a query value.
if lookup_type != 'exact':
raise ValueError, "Cannot use None as a query value"
joins2, where2, params2 = lookup_inner(path, lookup_type, value, opts, opts.db_table, None)
joins.update(joins2)
where.extend(where2)
params.extend(params2)
return joins, where, params
def lookup_inner(path, lookup_type, value, opts, table, column):
qn = backend.quote_name
joins, where, params = SortedDict(), [], []
current_opts = opts
current_table = table
current_column = column
intermediate_table = None
join_required = False
name = path.pop(0)
# Has the primary key been requested? If so, expand it out
# to be the name of the current class' primary key
if name is None or name == 'pk':
name = current_opts.pk.name
# Try to find the name in the fields associated with the current class
try:
# Does the name belong to a defined many-to-many field?
field = find_field(name, current_opts.many_to_many, False)
if field:
new_table = current_table + '__' + name
new_opts = field.rel.to._meta
new_column = new_opts.pk.column
# Need to create an intermediate table join over the m2m table
# This process hijacks current_table/column to point to the
# intermediate table.
current_table = "m2m_" + new_table
intermediate_table = field.m2m_db_table()
join_column = field.m2m_reverse_name()
intermediate_column = field.m2m_column_name()
raise FieldFound
# Does the name belong to a reverse defined many-to-many field?
field = find_field(name, current_opts.get_all_related_many_to_many_objects(), True)
if field:
new_table = current_table + '__' + name
new_opts = field.opts
new_column = new_opts.pk.column
# Need to create an intermediate table join over the m2m table.
# This process hijacks current_table/column to point to the
# intermediate table.
current_table = "m2m_" + new_table
intermediate_table = field.field.m2m_db_table()
join_column = field.field.m2m_column_name()
intermediate_column = field.field.m2m_reverse_name()
raise FieldFound
# Does the name belong to a one-to-many field?
field = find_field(name, current_opts.get_all_related_objects(), True)
if field:
new_table = table + '__' + name
new_opts = field.opts
new_column = field.field.column
join_column = opts.pk.column
# 1-N fields MUST be joined, regardless of any other conditions.
join_required = True
raise FieldFound
# 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
new_opts = field.rel.to._meta
new_column = new_opts.pk.column
join_column = field.column
raise FieldFound
elif path:
# For regular fields, if there are still items on the path,
# an error has been made. We munge "name" so that the error
# properly identifies the cause of the problem.
name += LOOKUP_SEPARATOR + path[0]
else:
raise FieldFound
except FieldFound: # Match found, loop has been shortcut.
pass
else: # No match found.
raise TypeError, "Cannot resolve keyword '%s' into field" % name
# Check whether an intermediate join is required between current_table
# and new_table.
if intermediate_table:
joins[qn(current_table)] = (
qn(intermediate_table), "LEFT OUTER JOIN",
"%s.%s = %s.%s" % (qn(table), qn(current_opts.pk.column), qn(current_table), qn(intermediate_column))
)
if path:
# There are elements left in the path. More joins are required.
if len(path) == 1 and path[0] in (new_opts.pk.name, None) \
and lookup_type in ('exact', 'isnull') and not join_required:
# If the next and final name query is for a primary key,
# and the search is for isnull/exact, then the current
# (for N-1) or intermediate (for N-N) table can be used
# for the search. No need to join an extra table just
# to check the primary key.
new_table = current_table
else:
# There are 1 or more name queries pending, and we have ruled out
# any shortcuts; therefore, a join is required.
joins[qn(new_table)] = (
qn(new_opts.db_table), "INNER JOIN",
"%s.%s = %s.%s" % (qn(current_table), qn(join_column), qn(new_table), qn(new_column))
)
# If we have made the join, we don't need to tell subsequent
# recursive calls about the column name we joined on.
join_column = None
# There are name queries remaining. Recurse deeper.
joins2, where2, params2 = lookup_inner(path, lookup_type, value, new_opts, new_table, join_column)
joins.update(joins2)
where.extend(where2)
params.extend(params2)
else:
# No elements left in path. Current element is the element on which
# the search is being performed.
if join_required:
# Last query term is a RelatedObject
if field.field.rel.multiple:
# RelatedObject is from a 1-N relation.
# Join is required; query operates on joined table.
column = new_opts.pk.name
joins[qn(new_table)] = (
qn(new_opts.db_table), "INNER JOIN",
"%s.%s = %s.%s" % (qn(current_table), qn(join_column), qn(new_table), qn(new_column))
)
current_table = new_table
else:
# RelatedObject is from a 1-1 relation,
# No need to join; get the pk value from the related object,
# and compare using that.
column = current_opts.pk.name
elif intermediate_table:
# Last query term is a related object from an N-N relation.
# Join from intermediate table is sufficient.
column = join_column
elif name == current_opts.pk.name and lookup_type in ('exact', 'isnull') and current_column:
# Last query term is for a primary key. If previous iterations
# introduced a current/intermediate table that can be used to
# optimize the query, then use that table and column name.
column = current_column
else:
# Last query term was a normal field.
column = field.column
# If the field is a geometry field, then the WHERE clause will need to be obtained
# with the get_geo_where_clause()
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__)
params.extend(field.get_db_prep_lookup(lookup_type, value))
return joins, where, params

View File

@ -0,0 +1,36 @@
from django.db.models.query import *
from django.contrib.gis.db.models.postgis import geo_parse_lookup
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)
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 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)
def _geo_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:
mapper = lambda x: x
if len(args) > 0 or len(kwargs) > 0:
assert self._limit is None and self._offset is None, \
"Cannot filter a query once a slice has been taken."
clone = self._clone()
if len(kwargs) > 0:
clone._filters = clone._filters & mapper(GeoQ(**kwargs)) # Using the GeoQ object for our filters instead
if len(args) > 0:
clone._filters = clone._filters & reduce(operator.and_, map(mapper, args))
return clone